3.10.1 The DAO (June 2016)
The DAO attack is the foundational case study of smart contract security. The bug it exposed — reentrancy — became the canonical first lesson taught to every Solidity developer. The community's response — a hard fork that split Ethereum into ETH and ETC — set a precedent for how the ecosystem handles catastrophic exploits that no protocol-level mechanism can otherwise reverse. Every defense in Section 3.7.1 (Control Flow Patterns) and every variant catalogued in Section 3.8.2 (The Reentrancy Family) traces back to this single incident.
The DAO is also a useful reminder that simple bugs can produce enormous consequences. The vulnerability fit inside a few lines of code. The fix was minor — reorder two statements, or add a guard. But the bug existed in a contract holding nearly 14% of all ETH then in circulation, and exploiting it took two weeks to recover from at the social and political level even though the attacker's actions occupied just hours.
Context
The DAO ("Decentralized Autonomous Organization") was a venture-fund-as-smart-contract launched on the Ethereum mainnet in April 2016. Token holders pooled ETH into a single contract, then voted on which projects to fund. Returns from funded projects would flow back to token holders proportional to their stake.
The launch was extraordinary by any standard:
- Token sale duration: 28 days, ending May 28, 2016
- Total raised: ~12.7M ETH, then worth roughly $150M (ETH was trading around $12-15)
- Participants: ~11,000 individual addresses
- Share of total ETH supply: approximately 14% of all ETH in existence
The DAO was, briefly, the largest crowdfunding event in human history. It was also the largest accumulation of value ever placed under the control of a single smart contract — a contract that had been written quickly, audited informally, and deployed to mainnet while researchers were still publicly debating its vulnerabilities.
The attack began on June 17, 2016, three weeks after the token sale ended. By the time it was identified and stopped, approximately 3.6M ETH (worth roughly $60M at the time, depending on the source) had been drained into a "child DAO" controlled by the attacker. A 28-day waiting period built into The DAO's withdrawal mechanism prevented the attacker from immediately moving the funds — which gave the Ethereum community time to act.
What they did was unprecedented and remains controversial. On July 20, 2016, at block 1,920,000, the Ethereum network executed a hard fork that effectively rolled back the DAO transactions, allowing investors to withdraw their original ETH. A minority of the community rejected this fork on principle (immutability is supposed to be absolute) and continued operating the original chain as Ethereum Classic. The two chains have existed independently ever since.
Vulnerable Code
The DAO contract contained roughly 1,200 lines of Solidity. The exploitable bug lived in the interaction between two functions: splitDAO (which let token holders fork off into a "child DAO" with their share of funds) and withdrawRewardFor (which paid out accumulated rewards).
The simplified pattern at the heart of the bug:
// Simplified rendering of the vulnerable pattern
contract DAO {
mapping(address => uint256) public balances;
function withdraw(uint256 amount) public {
require(balances[msg.sender] >= amount);
// BUG: external call happens BEFORE state update
(bool ok, ) = msg.sender.call{value: amount}("");
require(ok);
// State update happens AFTER external call
balances[msg.sender] -= amount;
}
}
This is the canonical reentrancy bug, exactly as Section 3.8.2 describes it. The actual DAO's splitDAO function was substantially more complex — it created a new child DAO, transferred token balances, and reset the original holding — but the core flaw was the same: external value was sent before internal balances were updated.
In the actual code, the attack required interaction between splitDAO and withdrawRewardFor. The attacker:
- Held DAO tokens
- Called
splitDAO, which under the hood made an external call (paying out rewards viawithdrawRewardFor) - The external call landed in the attacker's malicious contract
- The malicious contract's fallback function called
splitDAOagain before the first call had updated the attacker's balance - The recursive call paid out the same rewards a second time
The cross-function interaction made the bug harder to spot than a single-function reentrancy. Reviewers reading splitDAO in isolation, and withdrawRewardFor in isolation, did not necessarily see how they combined. This is precisely the pattern Section 3.8.2 covers as "cross-function reentrancy."
The Attack
The attacker deployed a malicious contract designed to exploit the recursion. The attack flow:
Step 1: Acquire DAO tokens. The attacker purchased a modest balance of DAO tokens through normal channels.
Step 2: Deploy attack contract. The attacker deployed a contract whose fallback() function would recursively call splitDAO. When ETH arrived at this contract, the fallback would automatically execute, re-entering the DAO.
Step 3: Initiate splitDAO. The attacker called splitDAO from the attack contract, requesting to move their balance to a "child DAO" that they controlled.
Step 4: The recursion begins. Inside splitDAO, before updating the attacker's balance, the DAO's logic paid out rewards. The reward payment was an external call to the attacker's contract.
Step 5: Re-entry. The attacker's fallback() function received the call, then immediately called splitDAO again. The DAO's balances mapping had not yet been updated, so the recursive call saw the same balance as the original call and authorized the same payout.
Step 6: Repeat. The recursion continued for as many iterations as gas would allow — typically dozens of layers deep. Each level of recursion drained an additional chunk of the DAO's ETH.
Step 7: Stop and repeat. Each recursive sequence was bounded by gas. The attacker simply submitted a new transaction, repeating steps 4-6, accumulating drained funds across many transactions.
The drained ETH accumulated in the attacker's child DAO. The 28-day withdrawal delay built into The DAO meant the attacker could not immediately move the funds — they sat in a child DAO controlled by the attacker but locked by the delay.
The timeline:
- June 17, 2016: Attack begins; drainage transactions accumulate
- June 17, 2016 (later same day): Community identifies the attack in progress
- June 18, 2016: Mitigation attempts, including a "white-hat" counter-attack that drained the remaining DAO funds into safe child DAOs before the attacker could continue
- June 20-July 19, 2016: Public debate over how to respond
- July 20, 2016: Ethereum hard fork at block 1,920,000 returns the drained funds to investors
The drained ETH could never have been moved to the attacker's external wallet because of the withdrawal delay. The hard fork preempted that withdrawal entirely.
Root Cause
The DAO failure had several compounding causes:
1. Violation of Checks-Effects-Interactions (Section 3.7.1). The fundamental bug. splitDAO (and withdrawRewardFor) performed external calls before completing state updates. The canonical reentrancy mistake.
2. No reentrancy guard (Section 3.7.1). OpenZeppelin's ReentrancyGuard did not exist in 2016 — OpenZeppelin's contracts library was created largely in response to The DAO incident. But even without a library, manual locking patterns existed and were not applied.
3. Cross-function reentrancy (Section 3.8.2). The bug spanned splitDAO and withdrawRewardFor. Reviewers reading either function in isolation might miss the issue. This is precisely the variant of reentrancy that requires reasoning about the entire contract's state machine rather than function-local reasoning.
4. Insufficient pre-deployment review. The DAO had been deployed to mainnet with $150M of value while researchers were still publicly debating its vulnerabilities. The discrepancy between value-at-risk and review-depth has subsequently become a focus for the security community (Section 3.9 covers audit practices that emerged in response).
5. Solidity behavior of address.call.value. The pre-0.4.0 behavior of address.call.value(...)() forwarded all available gas to the recipient, allowing the recursive depth that made the attack possible. Subsequent Solidity guidance — and ultimately the transfer() and send() functions with their 2300-gas stipend — were reactions to this. Section 3.7.7 covers the subsequent obsolescence of the 2300-gas pattern after EIP-2929.
Each root cause has its corresponding defense in modern Solidity practice. None of those defenses existed as standard practice in 2016. The DAO is where they came from.
Lessons
The DAO produced more lessons than any other single smart contract incident in Ethereum's history:
1. Reentrancy as a first-class vulnerability class. Before The DAO, reentrancy was an academic concern. After, every Solidity tutorial leads with it. Every audit checks for it. Every reentrancy-eligible function is reviewed under the lens of "could this be re-entered?" The Section 3.8.2 family of variants — direct, cross-function, cross-contract, read-only, cross-chain — all trace conceptually back to this incident.
2. Checks-Effects-Interactions as a non-negotiable pattern. The CEI ordering is now taught as the default shape of any function that handles value. Section 3.7.1 covers it as the foundational control-flow pattern. The pattern is so universal that violating it is considered a code smell even when no reentrancy is possible.
3. Reentrancy guards as standard library infrastructure. OpenZeppelin's ReentrancyGuard (introduced shortly after the DAO incident) became the canonical defense. The library's existence is itself a direct response to The DAO — and Section 3.7.1 covers its modern variants including transient storage guards (Solidity 0.8.24+).
4. The hard fork precedent and its limits. Ethereum's response to The DAO — rolling back the transactions — has not been repeated for any subsequent exploit, despite many being much larger in absolute dollar terms. The Parity multi-sig freeze ($280M) was not rolled back; the Ronin bridge hack ($625M) was not rolled back; Wormhole ($325M) was not rolled back. The DAO precedent appears to have been specific to The DAO's specific circumstances — the size of the contract's value relative to total ETH supply, the lock-up window that gave the community time to act, and the existential threat that a $60M loss represented to the early Ethereum ecosystem. The fork is now widely understood as a one-time event, not a repeatable mechanism.
5. The audit-before-deployment discipline. The DAO went live on mainnet despite published warnings about its security. The post-incident industry consensus that contracts holding substantial value must be audited by independent reviewers before deployment is a direct response. Section 3.9 (Audits for Developers) codifies this discipline.
6. The danger of complexity in security-critical code. The DAO contract was approximately 1,200 lines. The interaction between splitDAO and withdrawRewardFor was non-obvious. Modern security guidance treats lines-of-code as a security cost: every line is one more place a bug can live. Minimal, focused contracts are preferred over feature-rich ones for value-handling components.
7. The community-response question. The fork-versus-immutability debate from 2016 has not been resolved; it has been deferred. Subsequent exploits are now handled through different mechanisms — protocol-level pausing, insurance funds, social pressure for white-hat returns. The DAO established that the option of intervention exists; subsequent practice has established that the option will rarely be used.
Modern Reproduction
For pedagogical purposes, the DAO bug reproduces straightforwardly in modern Solidity:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
// Vulnerable: simplified DAO pattern
contract VulnerableDAO {
mapping(address => uint256) public balances;
function deposit() external payable {
balances[msg.sender] += msg.value;
}
function withdraw(uint256 amount) external {
require(balances[msg.sender] >= amount, "insufficient");
// BUG: external call before state update
(bool ok, ) = msg.sender.call{value: amount}("");
require(ok);
balances[msg.sender] -= amount;
}
}
// Attacker contract
contract Attacker {
VulnerableDAO public target;
uint256 public attackAmount;
constructor(VulnerableDAO _target) {
target = _target;
}
function attack() external payable {
require(msg.value >= 1 ether);
attackAmount = msg.value;
target.deposit{value: msg.value}();
target.withdraw(msg.value);
}
receive() external payable {
if (address(target).balance >= attackAmount) {
target.withdraw(attackAmount);
}
}
}
A Foundry test demonstrating the attack:
function test_DAOReentrancy() public {
VulnerableDAO dao = new VulnerableDAO();
// Other users deposit to give the DAO funds to drain
vm.deal(address(this), 10 ether);
dao.deposit{value: 10 ether}();
assertEq(address(dao).balance, 10 ether);
// Attacker drains the DAO
Attacker attacker = new Attacker(dao);
vm.deal(address(attacker), 1 ether);
attacker.attack{value: 1 ether}();
// DAO is drained; attacker has more than they deposited
assertEq(address(dao).balance, 0);
assertGt(address(attacker).balance, 1 ether);
}
The fix is two lines of code: move the state update before the external call, or add nonReentrant. The same defenses apply in 2026 as would have prevented the original $60M loss in 2016.
Cross-References
- Reentrancy mechanics — Section 3.8.2 covers the full reentrancy family (direct, cross-function, cross-contract, read-only, cross-chain) in technical depth
- Defenses — Section 3.7.1 covers Checks-Effects-Interactions, Reentrancy Guards, and Pull-over-Push payments
- Anti-patterns — Section 3.7.7 lists the related anti-patterns including the obsolete
transfer()2300-gas stipend - Audit practices — Section 3.9 covers the audit-before-deployment discipline that emerged from this incident
- Subsequent reentrancy incidents — Section 3.8.10 (Case Study Walkthroughs) covers the Curve Finance Vyper reentrancy of July 2023, which demonstrates that the lessons must be reapplied to every new codebase, not assumed learned
- Ethereum hard fork — Section 1.3.2 covers the high-profile breach from the network-history perspective