The original. splitDAO paid the caller out and called withdrawRewardFor — both of which hand control to the caller's contract — and only then zeroed the caller's balance. The attacker's fallback simply called splitDAO again, and again, each pass paid against a balance that was still on the books. 3.6 million ETH left in a few hours. The response was the hard fork that split the network into Ethereum and Ethereum Classic — the single most consequential bug in the industry's history. Ten years later, reentrancy is still in the top three causes of loss.
function splitDAO(uint _proposalID, address _newCurator) noEther onlyTokenholders returns (bool _success) { ... // ❶ move the ether out — control leaves the contract here uint fundsToBeMoved = (balances[msg.sender] * p.splitData[0].splitBalance) / p.splitData[0].totalSupply; if (p.splitData[0].newDAO.createTokenProxy.value(fundsToBeMoved)(msg.sender) == false) throw; ... // ❷ burn the tokens — this is where the balance finally goes to zero Transfer(msg.sender, 0, balances[msg.sender]); withdrawRewardFor(msg.sender); // ← another external call, still before ❸ totalSupply -= balances[msg.sender]; balances[msg.sender] = 0; // ❸ too late paidOut[msg.sender] = 0; return true; } // attacker's fallback: call splitDAO() again. // balances[msg.sender] is still the original number, every single pass. → result: 3,600,000 ETH · the Ethereum / Ethereum Classic hard fork
splitDAO and withdrawRewardFor. Auditing functions in isolation is how this gets missed.Entries in the SAFE database that describe this failure. The first ones name this incident directly.
Every figure on this page comes from the post-mortems above, not from us. Losses are US dollars at the time of the incident.
Reentrancy is one of the 203 classes the SaferICO scanner checks for. It will not review your signing process — but it will read your Solidity.