Track 7 · The Breach Lab · lesson 1
Reentrancy
20 min
Everything until now, you built. This track, you break.
Below is a vault holding about 100 ETH — 99 that other people deposited, plus the 1 ETH you are about to seed it with. You have one ETH to your name and one instruction.
Empty it.
The victim
Read it before you touch anything. The bug is in withdraw, and it is a single
line in the wrong place.
function withdraw() public {
uint256 bal = balances[msg.sender];
require(bal > 0, "nothing to withdraw");
(bool ok, ) = msg.sender.call{value: bal}(""); // sends ETH...
require(ok, "send failed");
balances[msg.sender] = 0; // ...then updates
}
The vault sends your ETH, and only afterwards sets your balance to zero.
Between those two lines, control is yours. When call sends ETH to your
contract, your receive() function runs — and at that instant, the vault still
thinks you have your full balance, because it has not reached the zeroing line.
So you call withdraw again. The check passes. It sends again.
Drain it
Write the attacker. attack() seeds the vault and pulls the first thread;
receive() keeps pulling until the vault is empty.
VulnerableVault
? ETH
Nudge
Show me the approach
Show me the code
Explain the exploit
What you just did
You did not break the cryptography. You did not forge a signature or guess a key. You used a public function exactly as written, in an order the author did not expect.
That is what almost every real exploit is. Not broken math — broken assumptions.
The author assumed withdraw would run start to finish before anything else
happened. External calls violate that assumption, and you met exactly this
warning in track 4: control genuinely leaves your contract on an external
call.
In 2016 this precise bug drained $60 million from The DAO. The response split Ethereum into two chains — the one you use, and Ethereum Classic, whose holders refused to reverse it.
Check
What single change to the vault would have prevented this?
Worth remembering
- Reentrancy: a victim calls out to you before updating its own state, and you call back in.
- The window is between the external call and the state update.
- The exploit uses public functions exactly as written — the assumption is what breaks, not the code.
- The fix is checks-effects-interactions: update state before calling out.
- This bug drained $60M from The DAO and split Ethereum from Ethereum Classic.