The DAO splitDAO Recursive Child-DAO Drain (2016-06-17)
- Name: The DAO
splitDAOrecursive child-DAO drain - Date: 2016-06-17
- Protocol: The DAO
- Chain: Ethereum
- Category: reentrancy / recursive-call vulnerability / delayed balance burn in split path
- Loss: about 3.6M ETH moved into an attacker-controlled child DAO, commonly described as roughly 60M at the time
- Summary: The DAO’s split mechanism let a token holder exit into a child DAO, but it performed the most dangerous actions in the wrong order. In
splitDAO, The DAO first calculatedfundsToBeMovedfrombalances[msg.sender]and the proposal’s cached split ratios, then callednewDAO.createTokenProxy.value(fundsToBeMoved)(msg.sender), and only later tried to burn the sender’s DAO tokens and zero their balance. Before those final balance writes,splitDAOalso calledwithdrawRewardFor(msg.sender), which flowed intorewardAccount.payOut(_account, reward)and finally into_recipient.call.value(_amount)(). That callback let the attacker re-entersplitDAOwhilebalances[msg.sender],totalSupply, andpaidOut[msg.sender]still reflected the pre-split state, so the same split claim could be executed again and again inside one transaction family. The result was not just an early ETH-send bug in isolation; it was a child-DAO creation path that kept honoring the same parent-DAO token balance until millions of ETH had been reassigned to the attacker’s child DAO. - Trigger / entry point:
- The DAO exposed a token-holder exit path through
splitDAO(uint _proposalID, address _newCurator). - The attacker created split proposal
#59(“Lonely, so Lonely”) and waited out the mandatory split-debate period. - When the split was executed, the path transferred ETH and child-DAO token rights before finalizing the sender’s balance burn in the parent DAO.
- The same path also invoked
withdrawRewardFor(msg.sender), which routed throughManagedAccount.payOut()and used_recipient.call.value(_amount)()to hand control to the attacker.
- The DAO exposed a token-holder exit path through
- Root cause:
- The core failure was checks-effects-interactions inversion across a governance exit path: The DAO externalized value and control before it finalized the sender’s token burn and accounting updates.
splitDAOdepended onbalances[msg.sender]and proposal snapshot values to decide how much ETH to move, but it left those inputs intact while calling out to attacker-controlled code.withdrawRewardFor()compounded the problem by making an unbounded low-level value call before updatingpaidOut[_account], providing the callback window used to recurse.- The exploit therefore sat at the intersection of two ordering mistakes: split accounting that finished too late, and reward payout that treated an external call as if it were a harmless transfer.
- Exploit path:
- The attacker accumulated DAO tokens and created split proposal
#59, pointing to an attacker-controlled child DAO curator structure. - After the minimum split debate period elapsed, the attacker called
splitDAO(...)from execution account0xf35e2cc8e6523d683ed44870f5b7cc785051a77d. splitDAOcomputedfundsToBeMovedfrom the still-livebalances[msg.sender]and then callednewDAO.createTokenProxy.value(fundsToBeMoved)(msg.sender), moving ETH and child-DAO token entitlement before burning the attacker’s parent-DAO position.- Still inside the same outer
splitDAOflow, The DAO calledwithdrawRewardFor(msg.sender). withdrawRewardFor()calledrewardAccount.payOut(msg.sender, reward), andpayOut()used_recipient.call.value(_amount)()to send ETH to the attacker-controlled recipient contract.- The attacker’s fallback function re-entered
splitDAO(...)with the same proposal context before the outer call had executedtotalSupply -= balances[msg.sender],balances[msg.sender] = 0, andpaidOut[msg.sender] = 0. - Because the parent-DAO accounting inputs were still stale, the re-entrant call recomputed the same
fundsToBeMovedand sent another child-DAO allocation. - Repeating that loop recursively let the attacker keep reusing the same parent-DAO balance until about 3.6M ETH had been shifted into child DAO
0x304a554a310c7e546dfe434669c62820b7d83490.
- The attacker accumulated DAO tokens and created split proposal
- What the attacker needed to be true:
- They needed enough DAO tokens to use the split mechanism legitimately at the outermost layer.
- A split proposal had to be created and survive the mandatory waiting period before execution.
- The split path had to keep
balances[msg.sender]and related accounting unchanged across the callback window. rewardAccount.payOut()had to forward control with enough gas for attacker logic to recurse.- The child DAO creation mechanism had to honor repeated calls under the same stale parent-DAO state.
- Technical anatomy worth remembering:
- The exploit is often taught as “The DAO reentrancy,” but the money path was specifically a recursive split into a child DAO, not a simple repeated
withdraw()from a vault. - The decisive invariant failure was: one parent-DAO token balance should only authorize one child-DAO migration. The DAO let the same balance authorize many.
- The
withdrawRewardFor()leg mattered because it supplied the callback point, but the payout amount itself was not the economic prize. The prize was repeated execution of the split before balance burn. - Contemporary analysis noted that even a zero-reward path could still invoke the dangerous payout flow; the callback window existed because the contract performed the external call, not because the reward was large.
- The incident also showed that governance and exit machinery are asset-movement machinery. A proposal-driven split path was effectively a privileged withdrawal path with more moving parts and a larger attack surface.
- The 27-day child-DAO creation window prevented the attacker from immediately cashing out the drained ETH, which is why the exploit quickly became an Ethereum social-consensus and fork crisis instead of a purely contract-local recovery story.
- The exploit is often taught as “The DAO reentrancy,” but the money path was specifically a recursive split into a child DAO, not a simple repeated
- Representative transactions and artifacts:
- The DAO main contract:
0xbb9bc244d798123fde783fcc1c72d3bb8c189413 - Split proposal creation tx for proposal
#59(“Lonely, so Lonely”):0x5798fbc45e3b63832abc4984b0f3574a13545f415dd672cd8540cd71f735db56 - Execution account behind the malicious split:
0xf35e2cc8e6523d683ed44870f5b7cc785051a77d - Attacker-controlled child DAO:
0x304a554a310c7e546dfe434669c62820b7d83490 - Proposal-creator account highlighted in later reconstruction:
0xb656b2a9c3b2416437a811e07466ca712f5a5b5a - Curator multisig recovered from the child-DAO constructor arguments:
0xda4a4626d3e16e094de3225a751aab7128e96526 - DAO reward account discussed in technical reconstructions:
0xd2e16a20dd7b1ae54fb0312209784478d069c7b0 - Attacker helper contracts repeatedly receiving tiny reward payouts during recursion:
0xf835a0247b0063c04ef22006ebe57c5f11977cc4and0xc0ee9db1a9e07ca63e4ff0d5fb6f86bf68d47b89
- The DAO main contract:
- Response / mitigation:
- The Ethereum Foundation publicly confirmed the recursive-call exploit on 2016-06-17 and highlighted that the drained ETH sat in a child DAO subject to a waiting period before withdrawal.
- A soft-fork proposal was briefly advanced to freeze outflows from The DAO and its children, though that path later proved unsafe.
- The crisis ultimately led to Ethereum’s hard fork, which moved the drained ETH into a refund contract for original DAO token holders; Ethereum Classic preserved the unforked history.
- Long-term mitigation lessons centered on checks-effects-interactions, safer value-transfer patterns, and the realization that high-value onchain governance machinery had become systemically important infrastructure.
Pattern tags
- reentrancy
- callback-frame-authority-reuse
- The same parent-DAO token balance kept authorizing fresh child-DAO migration because
splitDAO()handed control away before it burned the balance that defined the claim withdrawRewardFor()looked like payout plumbing, but its low-level value send was really a privileged re-entry edge back into the still-live split frame- Governance exit machinery, reward-account payout, and child-DAO creation together formed one security object; once those surfaces were allowed to observe different settlement stages, recursive withdrawal became possible
Why the exploit worked
splitDAO()computedfundsToBeMovedfrombalances[msg.sender]while that balance still represented full parent-DAO ownership, then called into child-DAO creation before the parent side executed the balancing writes that should have consumed that ownership. The crucial failure was therefore not merelyexternal call happened; it wasthe external call happened while the one-shot split right was still economically live.- The callback window came from a second ordering mistake in the same flow.
withdrawRewardFor(msg.sender)eventually reachedrewardAccount.payOut(_account, reward), which used_recipient.call.value(_amount)()and let attacker-controlled code resume execution beforepaidOut[msg.sender],balances[msg.sender], andtotalSupplyhad been settled for the outer split. - That meant the parent DAO still told the same story on every recursive entry: the attacker still appeared to own the same DAO-token balance, proposal
#59still looked like a valid split context for that balance, and the protocol still recalculated another child-DAO migration from already-spent governance capital. - The reusable lesson is sharper than the folklore version of
fallback reentrancy. The DAO failed because one parent balance was allowed to authorize many child-DAO claims. The ETH send only mattered because it reopened the path before the protocol had madesplit already consumedtrue in storage. - This also explains why the exploit sat on the governance / exit seam instead of a plain vault-withdraw seam. The dangerous object was not a simple cash balance alone, but a still-live bundle of split proposal context, DAO-token ownership, reward payout path, and child-DAO mint authority that had never been committed as one settled frame before control left the contract.
Three callback-boundary promotions this incident collapsed
-
A callback-capable edge became live before the split had settled
- The DAO’s
splitDAO()path was not yet done proving one final parent-side accounting story when it reachedwithdrawRewardFor(msg.sender)and thenrewardAccount.payOut(_account, reward). _recipient.call.value(_amount)()therefore did more than send a tiny side payout: it made attacker-controlled code live while the same split frame still had unburned parent-DAO balance and an unconsumed child-DAO migration right.
- The DAO’s
-
That half-finished state was still treated as canonical split authority
- On re-entry, The DAO still answered all the important questions as if nothing had been spent yet:
balances[msg.sender]still reflected full parent ownership,totalSupplystill had not been reduced, and proposal#59still looked like a valid basis for anotherfundsToBeMovedcalculation. - The dangerous promotion was from
callback can run during splittothis still counts as the same money-good split authority story the contract may use again right now.
- On re-entry, The DAO still answered all the important questions as if nothing had been spent yet:
-
The resumed path still spent against that stale authority and minted fresh child-DAO claims
- Once the attacker could re-enter before burn and payout checkpoints landed, the protocol kept reusing one parent-DAO balance to authorize repeated
createTokenProxy.value(fundsToBeMoved)(msg.sender)executions. - That is why the exploit is sharper than
fallback reentrancy: the real loss came from letting one still-live governance exit right inherit repeated treasury-moving authority across recursive frames.
- Once the attacker could re-enter before burn and payout checkpoints landed, the protocol kept reusing one parent-DAO balance to authorize repeated
Detection and audit pivots
- On any exit or migration path, ask whether
one position / balance / vote weight should only authorize one transfer of backing. If that answer is yes, the storage that consumes that right must be finalized before any callback-capable transfer, payout, or helper call runs. - Treat reward payouts, refunds, and sidecar distribution calls as part of the main withdrawal threat model. In The DAO, the reward payment was not the economic prize, but it was still the callback edge that reopened the more valuable split authority.
- Review governance and escape-hatch code with the same hostility used for lending or vault withdraws.
Proposal executionandragequit / splitpaths are balance-sheet code whenever they can move treasury assets or mint withdrawal rights elsewhere. - Write the invariant in explicit accounting language: a consumed parent balance must not remain visible to any path that can compute another child-DAO allocation, and proposal-linked migration should become unspendable before external control can resume.
- Test recursive entry through every external leg in the path, not only the obvious transfer. If a function touches balances, supply, payout records, and child-object creation, fuzz nested execution at each handoff until the system proves that no second claim can emerge from the same pre-state.
Failure-prevention checklist
- Consume split, ragequit, migration, or withdrawal rights in storage before any payout, reward distribution, child-object creation, or other callback-capable helper runs. If one parent balance should only authorize one exit, make
already consumedtrue before control can leave the contract. - Treat governance exits and payout sidecars as one security object. Proposal context, token balance, child-DAO target, payout record, and treasury outflow should settle together or not at all.
- Prefer staged or pull-based payout design. Record the post-split state first, then let users claim residual rewards or side payouts through a separate path that cannot reopen the original exit authority.
- Avoid low-level value sends inside unsettled governance or migration flows. If value must be delivered, do it only after the exit right, supply changes, and accounting checkpoints are finalized.
- Make one-shot execution frames explicit. The actor, proposal id, child target, amount, and consumption status should be committed as a single spendable object rather than inferred from still-live balances and loosely related storage.
- Fuzz recursive entry through every external leg in the path, including reward payouts, helper contracts, child-object constructors, and refund rails; do not stop at the most obvious ETH transfer.
- Add bounded-loss controls around high-value exit machinery: pauses, per-proposal outflow caps, rate limits, or delay windows that still matter even if one callback bug survives review.
Audit prompts
- Which exact storage fields together mean
this account may still split / exit once, and are all of them consumed before any external call or value send occurs? - Can a reward payout, refund, child-contract creation step, or helper callback reopen the same exit path while balances, supply, or payout checkpoints still reflect the pre-exit state?
- Does any migration or split flow compute the amount to move from balances or proposal snapshots that remain economically live across the callback window?
- Are governance / ragequit / split paths reviewed with the same rigor as vault withdrawals and bridge releases, or are they still mentally classified as administrative logic?
- If the external call succeeds and recursion happens immediately, what invariant proves the same parent position cannot authorize a second allocation, second withdrawal, or second child-object mint?
- Are sidecar payout functions documented as part of the main custody path, or could a supposedly tiny reward/refund transfer quietly be the control-flow edge that reopens the valuable action?
- What operational control limits damage if one exit path fails anyway: emergency pause, proposal cancellation, per-epoch treasury outflow ceilings, or nothing until assets are already gone?
- Do tests cover recursive execution against realistic attacker-controlled recipients and helper contracts, not just same-function toy recursion with one mock fallback?
Lessons that should already be standard
- Governance exit code is treasury code.
Split,ragequit,migrate, andclaim then exitpaths deserve the same suspicion as any direct withdraw function because they are withdraw functions with more moving parts. - A one-shot right must be consumed before value leaves. If a balance, vote-weight snapshot, or exit entitlement remains visible during a callback, the protocol is still telling the attacker that the right has not been spent yet.
- Sidecar payout plumbing is not peripheral. Tiny reward or refund transfers often become the callback edge that reopens a much larger authority surface.
- The clean design target is not merely
fallback cannot recurse. It isno callback can ever observe a state from which the same governance capital or exit right still authorizes another transfer of backing. - Social or operational recovery windows are not substitutes for contract-level safety. The DAO’s child-DAO lock bought time for a fork debate, but that was crisis response, not sound protocol design.
- High-value contract failures can become chain-governance failures. If the blast radius of one callback bug is
the ecosystem must coordinate a fork, then bounded-loss design was missing long before the exploit.
Why this incident matters
- It remains the historical reentrancy incident everyone remembers, but the important reusable lesson is narrower and sharper: recursive control-flow only paid because The DAO reused stale split accounting to mint a second, third, and hundredth child-DAO claim from one parent balance.
- Modern exploits like Lendf.Me, C.R.E.A.M., Rari Fuse, and Curve/Vyper are mechanically cleaner for studying today’s callback surfaces, but The DAO is still the foundational example of delayed accounting plus external control handoff detonating a flagship protocol.
- It also demonstrated that sufficiently large smart-contract failures can escape the contract layer and become governance, client, miner, exchange, and chain-coordination events.
Internal linkages
- Core recurring pattern: reentrancy.
- Sharper execution-frame lens: callback-frame-authority-reuse — the reward payout callback did not just reopen control; it reused still-live split authority against the same parent-DAO balance before burn and final accounting.
- Positive-design companion: callback-frame-truth-security-object — useful when the sharper design question becomes what exact split proposal, actor, child-DAO target, and payout frame should have been committed before
rewardAccount.payOut()handed control away. - Positive-design companion: exit-right-consumed-state-binding — useful when the sharper healthy-side question is how a parent-DAO exit entitlement should become unspendable before any reward, refund, or child-DAO delivery path can resume execution.
Control surface
-
The leverage sat in proposal-driven exit logic, child-DAO creation, reward payout plumbing, and the assumption that these steps could safely interleave before parent balances were burned.
-
The DAO did not mainly fail because an attacker found “a clever fallback.” It failed because its split pipeline treated unsettled accounting as if it were already final.
-
Sources:
-
Last reviewed: 2026-08-01 UTC