Reproduced Exploit
Burve: incorrect netting zeroes the deposit and over-withdraws
Chain
Other
Category
untagged
Date
Jan 1970
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: 174. Standalone Foundry PoC and full write-up: 56952-burve-incorrect-netting-logic-leads-to-excessive-withdrawal-amounts_exp in the
evm-hack-registrymirror.
Vulnerability classes: vuln/logic · vuln/accounting
Reproduction: a faithful minimal reproduction of the vulnerable finding — the vulnerable
commitnetting branch is reproduced verbatim (marked@>) with faithful minimal ERC20/ERC4626 doubles; local deploy, no fork.
Root cause#
When commit has both a pending deposit and a pending withdrawal, it tries to net them so the vault only moves (and pays fees on) the difference. In the assetsToWithdraw > assetsToDeposit branch it sets assetsToDeposit = 0 before the next line subtracts it, so assetsToWithdraw -= assetsToDeposit subtracts 0 — the netting is a no-op and the full withdrawal is executed. The verbatim vulnerable source:
function commit(VaultE4626 storage self, VaultTemp memory temp) internal {
uint256 assetsToDeposit = temp.vars[1];
uint256 assetsToWithdraw = temp.vars[2];
if (assetsToDeposit > 0 && assetsToWithdraw > 0) {
// We can net out and save ourselves some fees.
if (assetsToDeposit > assetsToWithdraw) {
assetsToDeposit -= assetsToWithdraw;
assetsToWithdraw = 0;
} else if (assetsToWithdraw > assetsToDeposit) {
assetsToDeposit = 0; // @> zeroes the deposit BEFORE the next line subtracts it → netting is a no-op
assetsToWithdraw -= assetsToDeposit;
} else {
// Perfect net!
return;
}
}
if (assetsToDeposit > 0) {
// Temporary approve the deposit.
SafeERC20.forceApprove(
self.token,
address(self.vault),
assetsToDeposit
);
self.totalVaultShares += self.vault.deposit(
assetsToDeposit,
address(this)
);
SafeERC20.forceApprove(self.token, address(self.vault), 0);
} else if (assetsToWithdraw > 0) {
// We don't need to hyper-optimize the receiver.
self.totalVaultShares -= self.vault.withdraw(
assetsToWithdraw,
address(this),
address(this)
);
}
}
Because assetsToDeposit is 0 after the swap-ordered lines, the if (assetsToDeposit > 0) deposit branch is skipped and the else if (assetsToWithdraw > 0) branch withdraws the full assetsToWithdraw, never the net amount.
Why it's exploitable here#
Following the finding's preconditions — both a pending deposit and a larger pending withdrawal at commit time — with the reproduction's concrete values:
- The shared ERC4626 vault holds
BACKING = 1000e18assets owned by the closure. - A pending deposit of
100e18(temp.vars[1]) and a pending withdrawal of300e18(temp.vars[2]) are both queued —assetsToWithdraw > assetsToDeposit. - Correct netting would withdraw only
300e18 - 100e18 = 200e18. Instead the bug leavesassetsToWithdraw = 300e18and withdraws the full amount. commitpulls300e18out of the shared vault and over-decrementstotalVaultSharesby300e18— an excess of100e18(exactly the pending-deposit size) that should have been netted against the queued deposit rather than drained. On a vault that charges withdrawal fees, the protocol also pays fees on the full300e18instead of the200e18net.
Attack path#
Marked-line walkthrough (Playground)#
The EVM Playground pins each step to the exact executed source line in 0xce01759b…:
- L82 — Deposit vs withdrawal check: commit() has a pending deposit (100e18) and a larger pending withdrawal (300e18), and first tests whether the deposit is the bigger side.
- L83 — Correct-netting branch skipped: This branch would net correctly by subtracting the withdrawal from the deposit, but the withdrawal (300e18) is larger, so it is skipped.
- L86 — Deposit zeroed before subtraction: Root cause: assetsToDeposit is set to 0 BEFORE the next line runs assetsToWithdraw -= assetsToDeposit, so it subtracts 0 and no netting occurs.
- L103 — Deposit branch bypassed: With the deposit now 0, the deposit-to-address(this) branch is skipped and commit falls through to withdraw the full 300e18 instead of the net 200e18.
- L127 — Token double moves real assets: Setup: the MiniToken ERC20 double performs real transfers, so the un-netted withdrawal pulls 100e18 of genuine assets out of the shared vault.
- L218 — Excess drain measured at sink: The 100e18 excess — the pending deposit that should have netted away — is minted to the SINK address, quantifying the over-withdrawal drain.
PoC#
Registry (Foundry, local deploy — verbatim vulnerable source + harm-asserting test):
cd 56952-burve-incorrect-netting-logic-leads-to-excessive-withdrawal-amounts_exp && forge test -vvv
The browser Playground replays the same synthetic opcode-for-opcode and measures the harm: a queued 100e18 deposit + 300e18 withdrawal net to nothing, so commit withdraws the full 300e18 and over-drains the shared vault by 100e18. Both gates are green (registry forge test PASS + Playground _verify-poc VERDICT: PASS).
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 56952-burve-incorrect-netting-logic-leads-to-excessive-withdrawal-amounts_exp (evm-hack-registry mirror).
- AuditVault finding: 174.
- Upstream DeFiHackLabs PoC directory: src/test.
Alerts & third-party analyses
- DeFiHackLabs incident explorer: search "Burve: incorrect netting zeroes the deposit and over-withdraws".
- Web3Sec X hacked database: search.
- Rekt leaderboard: search.
- Solodit incident search: search.
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.