Reproduced Exploit
Devve — settlement pays the claimed amount, not the ETH received
investInProjects adds msg.value to investorInvestedAmountPerProjectInETH and never checks that it equals users.amount. settleProjectOwnerFunds sets _amountToWithdraw to users.amount and transfers that.
Chain
Other
Category
logic
Date
Jun 2024
Source
AuditVault
EVM Playground
Source-level debugger — step opcodes and Solidity in sync
The attack is replayed in an in-browser EVM preloaded with the exact dumped fork state. The execution tree shows every call; step by Solidity line or by opcode across all depths — source, Stack, Memory, Storage, Balances (native / ERC-20 / NFT), Transient storage and Return value stay in sync. Click a tree node, opcode, or source line to jump. No backend, no live RPC.
Source & credit. Reproduction of a public audit finding curated by AuditVault — the original finding: 37615-incorrect-handling-of-investment-amounts-zokyo-none-devve-ma. Standalone Foundry PoC and full write-up: 37615-incorrect-handling-of-investment-amounts-zokyo-none-devve-ma_exp in the
evm-hack-registrymirror.
Vulnerability classes: vuln/logic/missing-check · vuln/logic/state-update
Reproduction: a self-contained Foundry PoC that compiles and runs offline with only
forge-std— no fork and no RPC. Trace: output.txt. PoC: test/37615-incorrect-handling-of-investment-amounts-zokyo-none-devve-ma_exp.sol.
Key info#
| Impact | HIGH — the investor sends 40 ETH and is credited 10 ETH. The owner is paid 10 ETH. 30 ETH stays in the treasury. The control records and pays the real 40 ETH |
| Protocol | Devve Treasury.investInProjects / settleProjectOwnerFunds |
| Vulnerable code | msg.value is not checked against users.amount, and settlement transfers users.amount |
| Bug class | Recorded investment and the ETH actually sent diverge |
| Finding | Zokyo — Devve, 30 June 2024 · id 37615 |
| Report | Zokyo Devve, 2024-06-30 |
| Source | AuditVault finding |
| Status | Resolved. Reproduced here as a local synthetic. |
| Compiler | solc 0.8.27, cancun, via IR |
TL;DR#
investInProjects adds msg.value to investorInvestedAmountPerProjectInETH and never checks that it equals users.amount. settleProjectOwnerFunds sets _amountToWithdraw to users.amount and transfers that.
An investor sends 40 ETH with users.amount of 10 ETH. The real deposit stored is 40 ETH. The credit stored is 10 ETH. Settlement pays the project owner 10 ETH. The treasury still holds 30 ETH.
The control requires msg.value == users.amount and transfers investorInvestedAmountPerProjectInETH. The same 40 ETH is credited and paid. The treasury ends at 0.
run() does not warp. The 40 ETH is msg.value on the single call.
The vulnerable code#
function investInProjects(Whitelist memory users) external payable {
// @> VULN: msg.value is not required to equal users.amount
investorInvestedAmountPerProjectInETH[users.projectId][msg.sender] += msg.value;
creditedAmount[users.projectId][msg.sender] = users.amount;
}
function settleProjectOwnerFunds(Whitelist memory users) external {
uint256 _amountToWithdraw = users.amount;
// @> VULN: pays users.amount, not investorInvestedAmountPerProjectInETH
payable(users.userAddress).transfer(_amountToWithdraw);
}
The fee check uses projectTotalCollectedInETH, which was increased by the real 40 ETH, so a 10 ETH withdrawal passes. The comment on the transfer in the report says that value should be the invested ETH.
Root cause#
Two fields describe one investment and nothing forces them to be equal. The balance mapping follows the ETH that arrived. The payout follows the amount in the calldata. Settlement then moves the calldata amount.
Preconditions#
users.amountis 10 ETH.msg.valueis 40 ETH.platformFeeis 0, so the fee does not hide the gap.- The owner address is the
userAddresson the whitelist passed to settlement. - Settlement runs once.
- No timestamp change happens inside
run().
Attack walkthrough#
- The investor calls
investInProjectswith 40 ETH andamount10 ETH. investorInvestedAmountPerProjectInETHbecomes 40 ETH.creditedAmountbecomes 10 ETH.settleProjectOwnerFundssets_amountToWithdrawto 10 ETH and transfers it to the owner.- The owner holds 10 ETH. The treasury holds 30 ETH.
Control: amount is 40 ETH and msg.value is 40 ETH. The credit is 40 ETH. Settlement transfers the invested balance, 40 ETH. The treasury holds 0.
Diagrams#
Impact#
The investor paid 40 ETH and is credited 10 ETH. The owner is paid 10 ETH instead of 40 ETH. 30 ETH remains in the treasury.
Remediation#
Require msg.value == users.amount in investInProjects. In settleProjectOwnerFunds, transfer investorInvestedAmountPerProjectInETH for that investor, not users.amount.
Sources#
Taxonomy#
severity/highvuln/logic/missing-checkvuln/logic/state-update
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 37615-incorrect-handling-of-investment-amounts-zokyo-none-devve-ma_exp (evm-hack-registry mirror).
- AuditVault finding: 37615-incorrect-handling-of-investment-amounts-zokyo-none-devve-ma.
Alerts & third-party analyses
These dashboards index community alerts tweets, post-mortems, and independent write-ups. Reach them through the protocol name above to cross-check this reproduction against other analyses.