Reentrancy Attacks: Detection, Prevention & Continuous Monitoring Guide

Firepan Security TeamApril 1, 2026

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.

Introduction

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.

What Is Reentrancy?

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.

Vulnerable Code Patterns

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.

Detection: How Firepan Finds Reentrancy

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:

  • External calls before state updates
  • Low-level calls without guards
  • Unprotected loops calling external contracts

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

Prevention: Solutions for Reentrancy

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.

Reentrancy in Modern DeFi: Flash Loans

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:

  1. Borrow $1M via flash loan (no collateral required)
  2. Call deposit($1M) on vulnerable DeFi protocol
  3. Protocol credits you with $1M
  4. Immediately call withdraw($1M)—reentrancy triggered
  5. Flash loan callback re-enters protocol again
  6. Repeat until protocol balance is drained
  7. Repay flash loan + fee (e.g., 0.05% = $500)
  8. Net profit: Millions

This is why reentrancy guards and proper state updates are essential even if you use call() for gas efficiency.

Frequently Asked Questions

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/

Conclusion

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

Scan Your Contracts Now

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 →