SC08:2026 - Reentrancy Attacks

Description

Reentrancy describes any situation where a smart contract performs an external call (to another contract or address), and the callee can call back into the original contract before the first invocation has completed and state has been fully updated. If the caller is not designed to be reentrancy-safe, the callback can observe stale state and exploit it—e.g., withdrawing more than the caller’s balance, double-counting rewards, or manipulating accounting across complex multi-step flows.

This affects all contract types that perform external calls: DeFi (token transfers, DEX swaps, vault deposits/withdrawals, flash loan callbacks), NFTs (transfers with ERC-721/ERC-1155 receiver hooks, marketplace payouts), DAOs (proposal execution that invokes external contracts), bridges (message relay, asset transfers), and composable protocols (ERC-777 hooks, ERC-4626 deposit/withdraw hooks). Reentrancy can be single-function (same function called recursively), cross-function (callback into a different function), or cross-contract (callback traverses multiple contracts). On non-EVM chains, analogous patterns exist wherever cross-program invocations can recurse.

Few areas to focus on:

Attackers exploit:

Example (Vulnerable Reentrancy Pattern)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

interface IToken {
    function transfer(address to, uint256 amount) external returns (bool);
}

contract VulnerableVault {
    IToken public immutable token;
    mapping(address => uint256) public balances;

    constructor(IToken _token) {
        token = _token;
    }

    function deposit(uint256 amount) external {
        // Assume token already transferred in for brevity
        balances[msg.sender] += amount;
    }

    function withdraw(uint256 amount) external {
        require(balances[msg.sender] >= amount, "insufficient");

        // External call before state update – reentrancy window
        token.transfer(msg.sender, amount);

        balances[msg.sender] -= amount;
    }
}

Issues:

Example (Reentrancy-Safe Withdraw)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

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

interface ITokenSafe {
    function transfer(address to, uint256 amount) external returns (bool);
}

contract SafeVault is ReentrancyGuard {
    ITokenSafe public immutable token;
    mapping(address => uint256) public balances;

    error InsufficientBalance();
    error TransferFailed();

    constructor(ITokenSafe _token) {
        token = _token;
    }

    function deposit(uint256 amount) external {
        // Assume token already transferred in for brevity
        balances[msg.sender] += amount;
    }

    function withdraw(uint256 amount) external nonReentrant {
        uint256 bal = balances[msg.sender];
        if (bal < amount) revert InsufficientBalance();

        // Effects first
        balances[msg.sender] = bal - amount;

        // Then interaction
        bool ok = token.transfer(msg.sender, amount);
        if (!ok) revert TransferFailed();
    }
}

Security Improvements:

2025 Case Study

Best Practices & Mitigations