The DAO Hack
The Story
In the early hours of June 17, 2016, a mysterious entity began draining funds from The DAO, a decentralized autonomous organization that had raised over $150 million in what was then the largest crowdfunding event in history.
The attacker moved methodically, exploiting a vulnerability in The DAO's code that allowed them to repeatedly withdraw ETH before the contract could update its balance. Like a phantom in the dark forest of blockchain, they extracted value while leaving no trace of their identity.
As the community watched in horror, the price of ETH plummeted. The attack eventually led to a contentious hard fork of the Ethereum blockchain, splitting it into Ethereum (ETH) and Ethereum Classic (ETC).
Technical Analysis
The DAO hack exploited a classic reentrancy vulnerability in the contract's code. Here's how it worked:
-
The attacker created a malicious contract that interacted with The DAO's
splitDAO()function. -
When the victim contract sent funds to the attacker contract, it triggered the attacker's fallback function before updating its own balance.
-
This fallback function would call
splitDAO()again, and since the balance hadn't been updated yet, the attacker could withdraw funds multiple times.
The key vulnerability in the code:
// Vulnerable code in The DAO
function splitDAO(
uint _proposalID,
address _newCurator
) noEther onlyTokenholders returns (bool _success) {
// ... [code omitted for brevity]
// The critical vulnerability - transfer happens before state update
if (!_newCurator.call.value(ethBalance)()) {
throw;
}
// State update happens after the external call
totalSupply -= balances[msg.sender];
balances[msg.sender] = 0;
// ... [code omitted for brevity]
}
The proper implementation would follow the Checks-Effects-Interactions pattern, updating the state variables before making any external calls.
The attacker's contract might have looked something like this:
contract Attacker {
address dao = 0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413; // The DAO address
address owner;
constructor() {
owner = msg.sender;
}
// Fallback function that gets called when receiving ETH
function() payable {
// Call splitDAO again before the state update occurs
if (msg.sender == dao) {
dao.call(bytes4(sha3("splitDAO(uint256,address)")), 1, this);
}
}
// Withdraw the stolen funds
function withdraw() {
require(msg.sender == owner);
owner.transfer(address(this).balance);
}
}
Lessons Learned
- Always follow the 'Checks-Effects-Interactions' pattern in smart contracts.
- External calls should be made after state variables are updated.
- Consider using mutex locks to prevent reentrancy.
- Implement withdrawal patterns instead of direct transfers when possible.
- Thorough auditing and formal verification are essential for high-value contracts.