Reentrancy: The Bug That Killed Ethereum (Almost)
The most infamous vulnerability in smart contract history. How calling external contracts before updating state drains millions.

Dive Deeper with AI
Click → prompt copied → paste in AI chat
June 17, 2016.
Someone drains $60 million from The DAO. In one transaction.
The attacker didn't guess a password. Didn't break encryption. Didn't bribe anyone.
They just called a function. Over and over. Before the contract could update its records.
This is reentrancy. The most infamous bug in blockchain history.
What is reentrancy?
Imagine a bank ATM that works like this:
- Check your balance: $1,000
- Dispense cash: gives you $1,000
- Update balance: now $0
Sounds reasonable. But what if you could interrupt step 3?
- Check your balance: $1,000
- Dispense cash: gives you $1,000
- Before balance updates, you trigger another withdrawal
- Check your balance: still $1,000 (not updated yet!)
- Dispense cash: gives you another $1,000
- Repeat until bank is empty
- Only now does balance update: $0
That's reentrancy. Calling back into a contract before it finishes executing.
How it works in Solidity
Here's a vulnerable smart contract:
contract VulnerableBank {
mapping(address => uint) public balances;
function withdraw() public {
uint amount = balances[msg.sender];
// Step 1: Send ETH
(bool success, ) = msg.sender.call{value: amount}("");
require(success);
// Step 2: Update balance
balances[msg.sender] = 0;
}
}
The problem? It sends ETH before updating the balance.
When you send ETH to a contract, that contract's receive() or fallback() function runs. The attacker uses that to call withdraw() again:
contract Attacker {
VulnerableBank bank;
function attack() public {
bank.withdraw();
}
receive() external payable {
// This runs when bank sends ETH
// Balance not updated yet - withdraw again!
if (address(bank).balance > 0) {
bank.withdraw();
}
}
}
Each withdrawal triggers another withdrawal. The loop continues until the bank is empty.
The pattern
Every reentrancy attack follows this pattern:
1. Attacker calls function
2. Contract checks conditions ✓
3. Contract makes external call (sends ETH, calls another contract)
4. External call triggers attacker's code
5. Attacker's code calls back into the original function
6. Contract checks conditions again... still ✓ (state not updated)
7. Repeat until drained
8. State finally updates (too late)
The vulnerability exists whenever:
- External call happens before state update
- The called address is attacker-controlled
- The function can be called recursively
The fix: Checks-Effects-Interactions
The solution is simple. Change the order:
contract SafeBank {
mapping(address => uint) public balances;
function withdraw() public {
uint amount = balances[msg.sender];
// FIRST: Update state (effects)
balances[msg.sender] = 0;
// THEN: Make external call (interactions)
(bool success, ) = msg.sender.call{value: amount}("");
require(success);
}
}
Now even if the attacker calls back, their balance is already 0. Nothing to withdraw.
This pattern is called Checks-Effects-Interactions:
- Checks - verify conditions (require statements)
- Effects - update state (storage changes)
- Interactions - call external contracts
Always in that order. Never deviate.
Reentrancy guards
For extra safety, use a mutex (mutual exclusion lock):
contract GuardedBank {
bool private locked;
mapping(address => uint) public balances;
modifier noReentrant() {
require(!locked, "No reentrancy");
locked = true;
_;
locked = false;
}
function withdraw() public noReentrant {
uint amount = balances[msg.sender];
balances[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: amount}("");
require(success);
}
}
If someone tries to reenter, the locked flag blocks them.
OpenZeppelin provides ReentrancyGuard - use it.
Types of reentrancy
Single-function reentrancy
The classic. Reentering the same function (like our example).
Cross-function reentrancy
Reentering a DIFFERENT function that shares state:
function withdraw() public {
uint amount = balances[msg.sender];
msg.sender.call{value: amount}("");
balances[msg.sender] = 0;
}
function transfer(address to, uint amount) public {
// This can be called during withdraw!
// Balance not updated yet...
balances[msg.sender] -= amount;
balances[to] += amount;
}
Cross-contract reentrancy
Reentering a different contract that trusts the vulnerable one.
Protocol A calls Protocol B. Protocol B calls back to Protocol A through a third path.
This is why composability in DeFi is both powerful and dangerous.
Real-world victims
The DAO (2016) - $60M The attack that started it all. Led to Ethereum's hard fork and creation of Ethereum Classic.
Uniswap/Lendf.Me (2020) - $25M ERC-777 token hooks enabled reentrancy.
Cream Finance (2021) - $18.8M Cross-contract reentrancy through flash loan callbacks.
Curve (2023) - $70M+ Vyper compiler bug in reentrancy locks across multiple pools.
The list goes on. Reentrancy remains in top 3 DeFi vulnerability categories.
When you're at risk
Your contract might be vulnerable if:
- You make external calls (
.call(),.transfer(),.send()) - You interact with unknown tokens (some have callbacks)
- You use flash loans (callbacks are part of the design)
- You integrate with external protocols
Basically: almost every non-trivial contract.
Audit checklist
-
Find all external calls. Search for
.call,.transfer,.send, interface calls. -
Check if state updates happen before calls. If not, potential vulnerability.
-
Look for cross-function issues. Can another function access the same state mid-execution?
-
Check inherited contracts. Parent contracts might have issues.
-
Consider flash loan callbacks. If you support them, you support reentrancy vectors.
The uncomfortable truth
Reentrancy is a known bug. It's been known since 2016. Yet it keeps happening.
Why?
- Code complexity increases
- Composability creates unexpected paths
- Developers assume "someone else" will catch it
- Pressure to ship fast
The bug is simple. Preventing it requires discipline.
Every DeFi hack from reentrancy is a failure of process, not knowledge.
For a detailed breakdown of The DAO hack specifically, see our analysis of the attack that split Ethereum in two.
Further reading:
- SWC-107: Reentrancy
- DeFiHackLabs reentrancy examples
- OpenZeppelin's
ReentrancyGuardimplementation