SMART CONTRACTS, DEFI & WEB3 SECURITY - Chapter 3, Exercise 1 Solution ========================================================== Tracing Exactly Why the Vulnerable withdraw() Drains More Than Its Balance PROBLEM ------- Using this chapter's own real DAO hack mechanism, explain step by step why the vulnerable withdraw() function in this chapter allows a malicious contract to drain far more than its own actual balance, while the fixed version doesn't. SOLUTION -------- VULNERABLE VERSION, STEP BY STEP: 1. A malicious contract, with a genuine balance of, say, 1 ETH, calls withdraw(1 ether). 2. The require check passes: balances[attacker] (1 ETH) is indeed >= amount (1 ETH). 3. The INTERACTION happens next, before the EFFECT: the contract sends 1 ETH to msg.sender (the attacker's own contract). 4. Because the attacker's contract received ETH, its own fallback function automatically executes - and that fallback function is written to immediately call withdraw(1 ether) again, recursively, before the original call has finished. 5. Crucially, balances[attacker] still shows 1 ETH at this point, because the line that would have reduced it (balances[msg.sender] -= amount) hasn't executed yet - it comes after the external call in the vulnerable version. So the require check on this second, recursive call passes again, exactly like the first time. 6. Step 3 through step 5 repeat, over and over, each time sending another 1 ETH to the attacker, as long as the contract's own overall ETH balance holds out and the transaction's own gas allows it. 7. Only once every recursive call has finally unwound does execution ever reach balances[attacker] -= amount - and by then, many multiples of the attacker's real original 1 ETH balance have already been paid out. FIXED VERSION, WHY THIS CAN'T HAPPEN: In the fixed version, balances[msg.sender] -= amount runs as the EFFECT, immediately after the check and before any external call. So by the time the external call (and therefore any recursive re-entry into withdraw()) happens, the attacker's own recorded balance has already been reduced to 0. Any recursive call to withdraw() now hits the very first require check with balances[attacker] equal to 0, which fails immediately - the recursion simply can't get started a second time. ANSWER: The vulnerable version pays out ETH before updating the attacker's balance, so a recursive callback triggered by receiving that ETH sees an unchanged, still-sufficient balance and can repeat the withdrawal many times before the balance is ever actually reduced. The fixed version updates the balance first, so any recursive re-entry immediately fails the balance check, since the balance has already been reduced to zero by the time any external call - and any possible callback - happens. ---- WHY THIS WORKS AS AN ANSWER This traces the exact sequence of operations in both versions, identifying the precise moment (state update happening before vs. after the external call) that determines whether a recursive callback sees a stale, exploitable balance or an already-corrected one.