Reentrancy Attack

Smart Contract Updated Apr 2026

What is a Reentrancy Attack?

A reentrancy attack is a smart contract vulnerability where an attacker causes a contract to call itself repeatedly during a single transaction, draining funds before the contract can update its internal balance. It’s the most famous smart contract exploit and caused The DAO hack in 2016 — the event that led to Ethereum’s contentious hard fork creating ETH and ETC.

The name comes from “re-entering” a function: the attacker’s contract calls the vulnerable function, and during the external call, the vulnerable function is called again (re-entered) before the first call completes.

Reentrancy has caused over $700 million in total losses across all DeFi protocols. Despite being well-understood, new reentrancy vulnerabilities are still discovered regularly.

How Reentrancy Works

The Vulnerable Pattern

// VULNERABLE CONTRACT
contract VulnerableBank {
    mapping(address => uint256) public balances;
    
    function withdraw() public {
        uint256 amount = balances[msg.sender];
        
        // 1. Send ETH to caller FIRST (external call)
        (bool ok, ) = msg.sender.call{value: amount}("");
        require(ok);
        
        // 2. Update balance AFTER sending (too late!)
        balances[msg.sender] = 0;
    }
}

The Attack

Attacker Contract:
    function attack() public {
        bank.deposit{value: 1 ether}();      // Deposit 1 ETH
        bank.withdraw();                      // Call withdraw()
    }
    
    receive() external payable {
        if (address(bank).balance >= 1 ether) {
            bank.withdraw();                  // Re-enter withdraw() before balance = 0
        }
    }

Execution flow:
1. Attacker deposits 1 ETH
2. Attacker calls withdraw()
3. Bank sends 1 ETH to attacker (step 1 of withdraw)
4. Attacker's receive() triggers → calls withdraw() again
5. Bank sends 1 ETH again (balance not yet updated!)
6. Repeat until bank is empty
7. Eventually: balances[attacker] = 0 (but funds already gone)

Why It Works

The vulnerability is in the ordering of operations:

  1. External call (send ETH) happens first
  2. State update (set balance to 0) happens second

Between steps 1 and 2, the attacker can re-enter the function. The balance hasn’t been updated yet, so the contract thinks the attacker still has funds to withdraw.

Types of Reentrancy

1. Single-Function Reentrancy

The attacker re-enters the same function (classic example above). This is the simplest form.

2. Cross-Function Reentrancy

The attacker re-enters a different function that shares state:

function deposit() public payable {
    balances[msg.sender] += msg.value;
}

function withdraw() public {
    // Attacker calls deposit() via receive() to add balance
    // Then withdraw() sees increased balance
    uint256 amount = balances[msg.sender];
    (bool ok, ) = msg.sender.call{value: amount}("");
    balances[msg.sender] -= amount;  // Vulnerable to cross-function reentrancy
}

3. Cross-Contract Reentrancy

The attacker exploits state shared between multiple contracts. For example, a lending protocol’s supply and borrow functions share a single balance mapping.

4. Read-Only Reentrancy

The attacker re-enters a view function that reads outdated state. This doesn’t drain funds directly but can manipulate prices or oracle values.

How to Prevent Reentrancy

1. Checks-Effects-Interactions Pattern (Most Important)

Always update state BEFORE making external calls:

// SECURE
function withdraw() public {
    uint256 amount = balances[msg.sender];
    
    // EFFECT: Update state FIRST
    balances[msg.sender] = 0;
    
    // INTERACTION: External call LAST
    (bool ok, ) = msg.sender.call{value: amount}("");
    require(ok);
}

This is the #1 rule of smart contract development: Effects before Interactions.

2. ReentrancyGuard (OpenZeppelin)

Use a mutex lock that prevents re-entry:

import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

contract MyContract is ReentrancyGuard {
    function withdraw() public nonReentrant {
        uint256 amount = balances[msg.sender];
        (bool ok, ) = msg.sender.call{value: amount}("");
        require(ok);
        balances[msg.sender] = 0;
    }
}

The nonReentrant modifier sets a lock before execution and clears it after. If the function is re-entered, the lock prevents execution.

3. Pull Over Push Pattern

Instead of sending ETH, let users withdraw themselves:

// Instead of sending ETH:
contract Vendor {
    mapping(address => uint256) public pendingWithdrawals;
    
    function purchase() external payable {
        pendingWithdrawals[msg.sender] += msg.value;
    }
    
    // User calls this to claim their ETH
    function claim() external {
        uint256 amount = pendingWithdrawals[msg.sender];
        pendingWithdrawals[msg.sender] = 0;  // State update first
        payable(msg.sender).transfer(amount);
    }
}

4. Static Calls for View Operations

When reading from other contracts during a transaction, use staticcall (read-only) instead of call to prevent the called contract from modifying state.

Famous Reentrancy Attacks

AttackYearLossVector
The DAO2016$60M (3.6M ETH)Classic single-function reentrancy
Fei Protocol2022$80M (exploited by DEFI Saver)Cross-function reentrancy
Curve Pool2023$40M+Read-only reentrancy via Vyper compiler bug
WEMIX2023$1.4MReentrancy in bridge contract
Paraswap2023$900KRead-only reentrancy in router

The DAO hack was so significant it resulted in Ethereum’s only contentious hard fork — the creation of Ethereum (ETH) and Ethereum Classic (ETC).

Frequently Asked Questions

Q: Is reentrancy still a problem in 2025? A: Yes. While most developers know about it, new reentrancy vectors are still discovered. The Curve reentrancy in 2023 was caused by a Vyper compiler bug that affected how the reentrancy guard worked. Always audit your contracts.

Q: does ReentrancyGuard completely prevent reentrancy? A: It prevents re-entry into the same function. Cross-function and cross-contract reentrancy require different mitigations (proper state management across functions).

Q: How do auditors find reentrancy? A: Automated tools (Slither, Mythril) detect basic patterns. Manual code review catches cross-function variants. The most dangerous reentrancy bugs are those that hide in complex protocol interactions.