The DAO Reentrancy (2016)
Smart contract security audit by TierZero · 2023-01-15
Independent research. This is an unsolicited security analysis published by TierZero — not a commissioned audit, and no certificate is issued. It reflects our own review of public code and on-chain events.
Findings summary
What the protocol did
The DAO ("Decentralized Autonomous Organization") was a venture-fund contract deployed on Ethereum in April 2016, built on a framework Slock.it published as a template for token-holder-governed investment pools. Participants sent ETH to the contract and received DAO tokens in proportion, carrying voting rights over which proposals the pooled ether would fund. By the close of its token sale it held roughly 12.7 million ETH — at the time upward of $150 million and close to 14% of all ETH in circulation — the largest crowdfunded organization built on a blockchain up to that point.
Beyond proposal funding, the contract included a "split" mechanism: a token holder unhappy with the DAO's direction could invoke splitDAO to move their proportional share of ether into a newly spawned child DAO under their own control. That payout path — ether sent out around balance accounting touching an external call — is where the bug lived.
The vulnerability
This is the textbook case that gave "reentrancy" its name in smart-contract security. The flawed pattern: the contract sends ether before it updates the internal ledger tracking how much the recipient is entitled to. Solidity's low-level call forwards remaining gas and executes the recipient's code before returning control to the caller. If the recipient is a contract, its fallback runs synchronously inside that call — and if that fallback calls back into the same withdrawal function, it finds the stale, not-yet-decremented balance and gets paid again.
// Simplified illustration of the flawed pattern
mapping(address => uint) public balances;
function withdraw() public {
uint amount = balances[msg.sender];
require(amount > 0);
// external call happens BEFORE state is updated
(bool ok, ) = msg.sender.call{value: amount}("");
require(ok);
balances[msg.sender] = 0; // too late — already re-entered above
}
Every recursive call re-reads balances[msg.sender], sees the same non-zero figure, and triggers another payout. The loop only stops when it runs out of gas, hits a call-depth limit, or the contract's ether is exhausted. This is a checks-effects-interactions violation: state guarding a resource must be finalized before yielding control to code you don't own. The same class of handoff-to-external-control mistake shows up in approval flows too — see our breakdown of gasless approvals and EVM sniping-bot risk around Permit2 for a non-reentrancy variant of "the contract trusted the wrong step."
How the exploit worked
- The attacker acquired DAO tokens, gaining standing to call the split/withdrawal path.
- They deployed a contract whose fallback, on receiving ether, immediately called back into the DAO.
- The DAO computed the attacker's payable balance and issued an external call transferring ether to that contract — before zeroing the balance.
- The fallback re-entered the vulnerable function mid-call. The ledger still showed the original balance, so the DAO authorized and sent another payout.
- This recursed many times per outer transaction, compounding withdrawals faster than the ledger could catch up, and repeated across multiple transactions.
- Roughly 3.6 million ETH — on the order of a third of the DAO's holdings — was drained into a child DAO identical in structure to the parent, subject to the same withdrawal delay that gave the community a window to respond.
- The fallout was not only financial: after intense public debate, the Ethereum community executed an irregular state-transition hard fork to move the funds to a recovery contract — why Ethereum and Ethereum Classic exist as separate chains today.
How an audit catches this
This is one of the first things a competent review checks, and it's mechanical enough to be made systematic:
- Control-flow review of every function that sends value or calls an external address. For each one, trace whether state affecting the caller's entitlement is written before or after the external call.
- Checks-effects-interactions enforcement: flag any function where a
.call,.send,.transfer, or arbitrary external invocation precedes the storage write that would prevent re-invocation from repeating the same effect. - Reentrancy guards as a backstop, not a substitute: confirm mutex-style modifiers sit on withdrawal and payout paths, but don't accept a modifier as proof of safety — a guard on the wrong function, or omitted on a sibling function sharing the same state, is still exploitable.
- Cross-function and cross-contract reentrancy tests: a function can be safe in isolation and still be reachable through a second function touching the same balance before the first one's mutex releases. We write proof-of-concept fallback contracts that attempt recursive re-entry against every payable and external-calling path, not just the obvious one.
- Read-only reentrancy checks on view functions another contract relies on mid-callback, a variant seen repeatedly in post-2016 incidents.
This is standard-issue coverage in a proper smart contract audit; we apply the same rigor during ongoing code review, since a reentrancy bug is as easily reintroduced in a small patch as it is present in an initial build. Teams building comparable external-call surfaces on other chains should read our note on Cross-Program Invocation risk in Solana DeFi — the trust-boundary problem is identical, only the calling convention differs.
Remediation
- Reorder to checks-effects-interactions: validate, write state, then perform the external call — never the reverse.
- Prefer pull-payment patterns over push transfers: let recipients withdraw their own balance rather than pushing funds to arbitrary addresses; this collapses the attack surface for anything beyond a simple, fixed transfer.
- Apply reentrancy guards on every state-mutating externally-callable function sharing balance state with a payout path, and audit the guard's scope, not just its presence.
- Treat every external call as a full context switch to untrusted code, including calls to addresses that look like plain accounts today — proxying and account abstraction can turn an EOA into arbitrary logic later.
The DAO's bug was understood conceptually within weeks and is now the first thing taught in Solidity security courses, yet the pattern still recurs whenever a team reinvents payout logic without that discipline. If you're shipping anything that moves value on an external call — including work under our smart contract development engagements — it's worth having someone independent trace every external call in the codebase before mainnet: get a smart contract audit scoped to exactly that.
Need your contracts audited?
Manual review + tooling across TON, Solana and EVM — certificate and full report included.
Get an audit