Reentrancy Auditor
1. Purpose
Detect all variants of reentrancy vulnerabilities in Solidity contracts. Reentrancy is the #1 cause of DeFi exploits historically, responsible for The DAO ($60M), Rari Capital ($80M), and many others.
2. Reentrancy Variants
ETH-001: Single-function Reentrancy (CRITICAL)
State update occurs after external call within the same function.
// VULNERABLE
function withdraw(uint amount) external {
require(balances[msg.sender] >= amount);
(bool success, ) = msg.sender.call{value: amount}("");
require(success);
balances[msg.sender] -= amount; // STATE AFTER CALL
}
// SECURE (CEI Pattern)
function withdraw(uint amount) external nonReentrant {
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount; // STATE BEFORE CALL
(bool success, ) = msg.sender.call{value: amount}("");
require(success);
}
ETH-002: Cross-function Reentrancy (CRITICAL)
Multiple functions share state, and reentrancy through one corrupts another.
// VULNERABLE — attacker reenters transfer() during withdraw()
function withdraw(uint amount) external {
require(balances[msg.sender] >= amount);
(bool success, ) = msg.sender.call{value: amount}("");
balances[msg.sender] -= amount;
}
function transfer(address to, uint amount) external {
require(balances[msg.sender] >= amount); // Stale during reentrancy!
balances[msg.sender] -= amount;
balances[to] += amount;
}
ETH-003: Cross-contract Reentrancy (HIGH)
External call in Contract A allows reentry into Contract B that reads A's stale state.
ETH-004: Read-only Reentrancy (HIGH)
View functions return incorrect values during reentrancy, affecting other protocols.
// Contract A
function withdraw() external {
uint shares = balanceOf(msg.sender);
(bool s, ) = msg.sender.call{value: sharesToETH(shares)}("");
_burn(msg.sender, shares); // Burns AFTER call
}
// Contract B reads A's state during A's reentrancy
function getPrice() external view returns (uint) {
return contractA.totalAssets() / contractA.totalSupply(); // Wrong during reentrancy!
}
ETH-044: ERC-777 Reentrancy Hook (CRITICAL)
ERC-777 tokens call tokensReceived hook on recipient, allowing reentrancy.
ETH-081: Transient Storage Slot Collision (CRITICAL)
Multiple contracts share the same transient storage slot via delegatecall, corrupting reentrancy guards.
// VULNERABLE — two libraries use same TSTORE slot
library LibA {
bytes32 constant LOCK_SLOT = 0x01;
function lock() internal { assembly { tstore(LOCK_SLOT, 1) } }
}
library LibB {
bytes32 constant LOCK_SLOT = 0x01; // COLLISION!
function lock() internal { assembly { tstore(LOCK_SLOT, 1) } }
}
// SECURE — use namespaced transient slots
library LibA {
bytes32 constant LOCK_SLOT = keccak256("LibA.reentrancy.lock");
function lock() internal { assembly { tstore(LOCK_SLOT, 1) } }
}
ETH-083: TSTORE Reentrancy Bypass (CRITICAL)
Reentrancy lock implemented via TSTORE can be bypassed if attacker enters with low gas that causes the TSTORE to fail silently or via cross-contract paths.
// VULNERABLE — TSTORE-based lock without TLOAD check
modifier nonReentrant() {
assembly {
if tload(0x00) { revert(0, 0) }
tstore(0x00, 1)
}
_;
assembly { tstore(0x00, 0) }
}
// Cross-contract call may bypass if callee uses delegatecall
// SECURE — use OpenZeppelin's ReentrancyGuardTransient
import "@openzeppelin/contracts/utils/ReentrancyGuardTransient.sol";
3. Detection Workflow
Step 1: Find All External Calls
rg -n "\.call\{|\.call\(|\.transfer\(|\.send\(|\.delegatecall\(" contracts/
rg -n "safeTransfer|safeTransferFrom" contracts/
rg -n "IERC20\(.*\)\.(transfer|transferFrom)" contracts/
rg -n "tstore|tload|TSTORE|TLOAD" contracts/ # Transient storage reentrancy guards
Step 2: For Each External Call
- •Identify state variables modified in the same function
- •Check if state updates happen BEFORE the external call (CEI)
- •Check if
nonReentrantmodifier is applied - •Map other functions that read/write the same state variables
Step 3: Check for Cross-function Paths
# Find shared state variables rg "mapping.*balances|mapping.*deposits|mapping.*shares" contracts/ # Then check all functions that read/write these
Step 4: Check for Read-only Reentrancy
# Find view functions that calculate based on contract state rg "function.*view.*returns" contracts/ # Check if these are used by external protocols
4. Secure Patterns
ReentrancyGuard (OpenZeppelin)
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract Vault is ReentrancyGuard {
function withdraw(uint amount) external nonReentrant {
// Protected
}
}
CEI Pattern (Checks-Effects-Interactions)
function withdraw(uint amount) external {
// 1. CHECKS
require(balances[msg.sender] >= amount, "Insufficient");
// 2. EFFECTS (state changes)
balances[msg.sender] -= amount;
// 3. INTERACTIONS (external calls)
(bool success, ) = msg.sender.call{value: amount}("");
require(success);
}
5. Finding Format
## [CRITICAL] ETH-001: Reentrancy in withdraw()
**Location**: `contracts/Vault.sol:45`
**Confidence**: 0.95
### Evidence
State update at line 48 occurs AFTER external call at line 46:
```solidity
function withdraw(uint amount) external {
require(balances[msg.sender] >= amount);
(bool success, ) = msg.sender.call{value: amount}(""); // line 46
require(success);
balances[msg.sender] -= amount; // line 48 — AFTER call!
}
Attack Scenario
- •Attacker deposits 1 ETH
- •Attacker calls withdraw(1 ether)
- •Vault sends ETH via .call{} — triggers attacker's receive()
- •receive() re-enters withdraw() — balance still shows 1 ETH
- •Repeat until vault drained
Recommendation
function withdraw(uint amount) external nonReentrant {
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount; // Update BEFORE call
(bool success, ) = msg.sender.call{value: amount}("");
require(success);
}
## 6. Quick Checklist - [ ] ALL external calls (.call, .transfer, .send) have state updated BEFORE - [ ] nonReentrant modifier on ALL state-changing functions with external calls - [ ] No cross-function paths share mutable state with external calls - [ ] ERC-777 token interactions use ReentrancyGuard - [ ] Flash loan callbacks follow CEI pattern - [ ] View functions not affected by incomplete state during calls - [ ] TSTORE-based reentrancy locks use namespaced slots (ETH-081) - [ ] TSTORE lock not bypassable via delegatecall (ETH-083, ETH-084) - [ ] If using ReentrancyGuardTransient, verify OZ version >= 5.1