What is Reentrancy?
A reentrancy attack occurs when a malicious contract exploits the fact that external calls can transfer control to an untrusted contract, which can then call back into the vulnerable contract before the first execution is complete.
This happens because Solidity allows external calls during function execution, and if state changes happen after the external call, an attacker can manipulate the contract state.
Historical Impact
How the Attack Works
The attack follows this general pattern:
- Attacker calls a vulnerable
withdraw()function - The contract sends ETH to the attacker before updating state
- Attacker's
receive()orfallback()function is triggered - Attacker re-enters the
withdraw()function - Since state wasn't updated, the check passes again
- Process repeats until funds are drained
🔄 Attack Flow
1. Attacker → withdraw() → sends ETH
2. ETH transfer → triggers receive()
3. receive() → calls withdraw() again
4. Balance not updated → withdraw succeeds
5. Repeat until contract is drained
Real-World Examples
The DAO Hack (2016)
$60M lost due to reentrancy in the splitDAO function. Led to the ETH/ETC fork.
Curve Finance (2023)
$70M lost due to a Vyper compiler bug that broke reentrancy guards.
Grim Finance (2021)
$30M lost through reentrancy in the depositFor function.
Prevention Techniques
1. Checks-Effects-Interactions Pattern (CEI)
The most fundamental defense. Always follow this order:
- Checks: Validate all conditions and inputs first
- Effects: Update all state variables
- Interactions: Make external calls last
2. Reentrancy Guards
Use OpenZeppelin's ReentrancyGuard modifier or implement your own mutex lock:
1import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
2
3contract SecureVault is ReentrancyGuard {
4 mapping(address => uint256) public balances;
5
6 function withdraw(uint256 amount) external nonReentrant {
7 require(balances[msg.sender] >= amount, "Insufficient");
8
9 // Effects before interactions
10 balances[msg.sender] -= amount;
11
12 // Interaction last
13 (bool success, ) = msg.sender.call{value: amount}("");
14 require(success, "Transfer failed");
15 }
16}3. Pull Over Push Pattern
Instead of sending ETH to users, let them withdraw. This limits the attack surface.
Best Practice
Code Examples
Let's compare vulnerable code with its secure counterpart:
function withdraw() external {
uint256 balance = balances[msg.sender];
require(balance > 0, "No balance");
// ❌ External call BEFORE state update
(bool success, ) = msg.sender.call{value: balance}("");
require(success, "Transfer failed");
// ❌ State updated AFTER external call
balances[msg.sender] = 0;
}function withdraw() external nonReentrant {
uint256 balance = balances[msg.sender];
require(balance > 0, "No balance");
// ✅ State updated BEFORE external call
balances[msg.sender] = 0;
// ✅ External call AFTER state update
(bool success, ) = msg.sender.call{value: balance}("");
require(success, "Transfer failed");
}Detection Tools
Use these tools to detect reentrancy vulnerabilities in your code:
🔧 Slither
Static analysis framework that detects reentrancy patterns.
slither . --detect reentrancy-eth🔧 Mythril
Symbolic execution tool for deep vulnerability analysis.
myth analyze contract.sol🔧 Foundry
Write reentrancy tests with invariant testing.
forge test --match-test testReentrancy