Definition: Reentrancy is a vulnerability where a contract calls an external contract before updating its own state, allowing the external contract to call back recursively and drain funds. Reentrancy attacks have stolen hundreds of millions from DeFi protocols (The DAO, Cream Finance, and others). Detection involves analyzing call graphs and state mutation ordering. Prevention uses checks-effects-interactions pattern or reentrancy guards. Firepan detects reentrancy patterns continuously across deployed contracts.
On June 17, 2016, The DAO was exploited for $60M via reentrancy. A decade later, reentrancy remains a top vulnerability class. Why? Because it's subtle—the code looks normal until you trace the execution order. This guide explains how reentrancy works, shows vulnerable code patterns, details detection methods, and demonstrates continuous monitoring that catches reentrancy before it's exploited.
Reentrancy occurs when a contract calls an external contract (typically to transfer funds) before updating its internal state. The external contract can then call back into the original contract, re-entering functions that assume state hasn't changed.
The DAO attack (2016):
function withdraw(uint amount) public {
require(balances[msg.sender] >= amount);
(bool success,) = msg.sender.call{value: amount}(""); // External call
require(success);
balances[msg.sender] -= amount; // State update happens AFTER external call
}
An attacker contract receives the transfer, then immediately calls withdraw() again. The check balances[msg.sender] >= amount passes because the state hasn't updated yet. The attacker drains the contract by calling withdraw() recursively until the balance is exhausted.
Cream Finance attack (Feb 2021): $37.5M lost
function redeemUnderlying(uint amount) public returns (uint) {
require(balanceOfUnderlying(msg.sender) >= amount);
transfer underlying to msg.sender; // Reentrancy point
updateUserBalance(msg.sender, amount); // State update too late
}
The attacker supplied collateral, then called redeemUnderlying(). The transfer triggered their contract, which immediately borrowed more collateral (now double-counted), then withdrew it again, and so on. Cream Finance was exploited again in October 2021 for $130M via a related flash loan attack.
Pattern 1: External Call Before State Update (Checks-Effects-Interactions Violation)
function withdraw(uint amount) public {
require(balances[msg.sender] >= amount); // CHECKS
(bool success,) = msg.sender.call{value: amount}(""); // INTERACTIONS (wrong order)
require(success);
balances[msg.sender] -= amount; // EFFECTS (should be here)
}
Correct order: CHECKS → EFFECTS → INTERACTIONS
function withdraw(uint amount) public {
require(balances[msg.sender] >= amount); // CHECKS
balances[msg.sender] -= amount; // EFFECTS (before external call)
(bool success,) = msg.sender.call{value: amount}(""); // INTERACTIONS (last)
require(success);
}
Pattern 2: Low-Level Call Without Guard
function transfer(address to, uint amount) public {
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount;
(bool success, bytes memory data) = to.call{value: amount}(""); // Callable reverts/reenters
}
The external call might not just transfer; it might execute arbitrary code. Prefer transfer() (reverts on reentrancy attempt) or use ReentrancyGuard.
Pattern 3: Call to Unknown Contract
function swap(address token, uint amount) public {
uint balanceBefore = IERC20(token).balanceOf(address(this));
IERC20(token).transferFrom(msg.sender, address(this), amount); // Untrusted token
// ... swapping logic ...
uint balanceAfter = IERC20(token).balanceOf(address(this));
}
If token is a malicious contract, its transferFrom() can call back into swap(). This is a reentrancy vector in multi-function protocols.
Firepan's HOUND AI detects reentrancy using multiple techniques:
1. Call Graph Analysis Builds a graph of function calls. Flags functions that: (a) make external calls, then (b) update state that affects reentrancy checks.
2. Pattern Matching Detects well-known patterns:
3. Deep Analysis HOUND AI analyzes execution paths to determine whether invariants hold across recursive calls.
4. Behavioral Analysis HOUND AI evaluates transaction sequences that could trigger reentrancy. If the invariant "balance never exceeds supply" could break, it flags the vulnerability.
5. AI-Driven Heuristics Firepan's AI identifies novel reentrancy patterns by learning from known exploits.
Example Firepan Detection Output:
CRITICAL: Reentrancy vulnerability in withdraw()
Location: Contract.sol:42
Severity: CRITICAL
Description: External call (msg.sender.call) at line 42 occurs before state update (balances[msg.sender] -= amount) at line 44.
Vulnerable Path:
1. Call withdraw(100)
2. msg.sender.call triggers attacker contract
3. Attacker calls withdraw(100) again
4. Check passes (balances[attacker] still = 100)
5. Repeat until balance = 0
Recommendation:
1. Move state update (balances -= amount) before external call
2. Or: Use ReentrancyGuard from OpenZeppelin
3. Test with echidna property: balances.sum() == totalSupply
Solution 1: Checks-Effects-Interactions (CEI) Pattern
function withdraw(uint amount) public {
require(balances[msg.sender] >= amount); // CHECKS
balances[msg.sender] -= amount; // EFFECTS
(bool success,) = msg.sender.call{value: amount}(""); // INTERACTIONS
require(success);
}
Move all state updates before external calls. This is the simplest and most efficient solution.
Solution 2: Reentrancy Guard (Mutex)
contract WithReentrancyGuard {
uint private locked = 1;
modifier nonReentrant() {
require(locked == 1, "reentrancy");
locked = 2;
_;
locked = 1;
}
function withdraw(uint amount) public nonReentrant {
require(balances[msg.sender] >= amount);
(bool success,) = msg.sender.call{value: amount}("");
require(success);
balances[msg.sender] -= amount;
}
}
OpenZeppelin's ReentrancyGuard blocks recursive calls. Cost: one SLOAD + one SSTORE per guarded function.
Solution 3: Use Transfer Instead of Call
// Before (vulnerable):
(bool success,) = payable(recipient).call{value: amount}("");
// After (safer):
payable(recipient).transfer(amount); // Reverts on reentrancy attempt
transfer() sends only 2,300 gas, preventing complex reentrancy. Downside: breaks contracts that need more gas.
Solution 4: State Checks on External Contract Results
function swap(address token, uint amount) public {
uint balanceBefore = IERC20(token).balanceOf(address(this));
IERC20(token).transferFrom(msg.sender, address(this), amount);
uint balanceAfter = IERC20(token).balanceOf(address(this));
require(balanceAfter == balanceBefore + amount);
}
Verify that external calls had expected effects. This catches both reentrancy and token misbehavior.
Traditional reentrancy is preventable. Modern attacks combine reentrancy with flash loans (uncollateralized loans that must be repaid in the same transaction).
Flash Loan Reentrancy Attack:
deposit($1M) on vulnerable DeFi protocolwithdraw($1M)—reentrancy triggeredThis is why reentrancy guards and proper state updates are essential even if you use call() for gas efficiency.
Q: Is reentrancy still a threat in 2026?
A: Yes. While awareness has improved, reentrancy remains a common vulnerability class. Modern variants (flash loan reentrancy, cross-function reentrancy) are subtle and easy to miss. Continuous monitoring is essential.
Q: Should I always use ReentrancyGuard?
A: If you make any external calls, yes. The gas cost (<5K) is trivial compared to exploit risk. Use it defensively on every function that touches external contracts or user funds.
Q: Is Checks-Effects-Interactions sufficient?
A: For traditional reentrancy, yes. For flash loan attacks, no—CEI doesn't prevent borrowing the same funds again. Use CEI + ReentrancyGuard for defense-in-depth.
Q: Can I detect reentrancy myself?
A: Pattern-based detection is easy (Slither catches it). But novel reentrancy patterns require deep analysis. Firepan's HOUND AI engine catches reentrancy variants that pattern-matching alone misses. Start scanning at https://app.firepan.com/
Q: How does Firepan detect reentrancy?
A: Firepan's HOUND AI engine combines pattern matching, call graph analysis, and AI-driven heuristics. It detects traditional reentrancy, flash loan reentrancy, and novel cross-function variants. Start scanning at https://app.firepan.com/
Reentrancy is preventable. Use checks-effects-interactions, add ReentrancyGuard, and continuously monitor. The $1B+ in reentrancy losses is entirely avoidable with proper patterns and ongoing security monitoring.
Start scanning at https://app.firepan.com/
Firepan
12,453 contracts secured. 2,851 vulnerabilities blocked. 236 exploits prevented. Run a free surface scan — results in minutes, no credit card required.
Run Free Scan →