☠ Case Studies
← All Case Studies

The DAO Hack

💸 $60,000,000 🔒 Reentrancy Attack 📅 2016-06-01 00:00:00 +0000

The original reentrancy attack — drained 3.6M ETH from a decentralized venture fund holding 15% of all ETH in existence.

Background

The DAO was a decentralized venture fund built on Ethereum. In 2016 it completed one of the largest crowdfunding campaigns in history, raising $150M (at the time). It held roughly 15% of all ETH in existence. The idea: token holders voted on which projects to fund. No company. No executives. Just code.

The Vulnerability

The splitDAO() function sent ETH to the caller before updating their balance in storage. An attacker's malicious contract could call back into splitDAO() from its fallback function before the balance was zeroed — draining ETH each time.

🚨 Vulnerable Code
// VULNERABLE: sends ETH before updating balance function splitDAO(uint _proposalID, address _newCurator) { // ... // ETH sent to attacker here — attacker re-enters splitDAO() if (!msg.sender.call.value( p.splitData[0].balanceInTheDAO * actualBalance / p.splitData[0].totalSupply )()) throw; // Balance zeroed AFTER ETH sent — too late! p.splitData[0].balanceInTheDAO = 0; }

The Attack — Step by Step

  1. Attacker deposits ETH into The DAO
  2. Attacker deploys a malicious contract with a fallback function that calls splitDAO() again
  3. Attacker calls splitDAO() — ETH is sent to the malicious contract
  4. The malicious contract's fallback fires, calling splitDAO() again before the balance is zeroed
  5. Each recursive call drains more ETH. The loop continues until the gas limit is hit
  6. Attacker drains ~3.6M ETH (~$60M at the time)

Impact

This was the most significant event in Ethereum's history. The community faced an impossible choice: accept the theft and uphold immutability, or fork the blockchain to restore the funds.

The controversial decision to hard-fork led to the split of Ethereum into:

  • ETH (Ethereum): The forked chain where funds were restored
  • ETC (Ethereum Classic): The original chain preserving immutability

Lessons Learned

🛡️ The Fix: Checks-Effects-Interactions Pattern

Always update state (effects) before making external calls (interactions). Check preconditions first. This ordering prevents reentrancy by ensuring state is already updated when a re-entrant call occurs.

✅ Fixed Pattern
function withdraw(uint amount) public { // 1. CHECKS: verify conditions require(balances[msg.sender] >= amount); // 2. EFFECTS: update state FIRST balances[msg.sender] -= amount; // 3. INTERACTIONS: external call last payable(msg.sender).transfer(amount); }

Additionally: use OpenZeppelin's ReentrancyGuard modifier on sensitive functions as a defense-in-depth measure.