SC06:2026 - Unchecked External Calls

Description

Unchecked external calls describe any situation where a smart contract invokes another contract or address (via call, delegatecall, staticcall, or high-level calls like transfer/send) without fully accounting for the callee’s behavior, return value, or reentrancy potential. The calling contract implicitly trusts that the callee will behave as expected—returning success, not re-entering, and not executing arbitrary logic. When that assumption is violated, the caller can be left in an inconsistent state or exploited.

This affects all contract types that perform external interactions: DeFi (token transfers, DEX swaps, vault deposits, flash loan callbacks), NFTs (transfers with hooks, marketplace payouts), DAOs (execution of proposal calldata), bridges (message relay, asset transfers), and composable protocols (arbitrary callbacks, ERC-777/ERC-721/ERC-1155 receiver hooks, ERC-4626 deposit/withdraw hooks). On non-EVM chains, analogous patterns exist (e.g., Move’s vector::borrow, Solana CPI) where cross-program invocations can re-enter or behave unexpectedly.

Few areas to focus on:

Attackers exploit:

Unchecked external calls are rarely the sole root cause but are a critical enabler for reentrancy (SC08), business logic exploits (SC02), and accounting inconsistencies.

Example (Vulnerable Unchecked Call Pattern)

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

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

contract VulnerablePayout {
    IToken public token;

    mapping(address => uint256) public rewards;

    constructor(IToken _token) {
        token = _token;
    }

    function claim() external {
        uint256 amount = rewards[msg.sender];
        require(amount > 0, "no rewards");

        // Vulnerable: does not check return value or reentrancy
        token.transfer(msg.sender, amount);

        // State update after external call
        rewards[msg.sender] = 0;
    }
}

Issues:

Example (Hardened External Call Handling)

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

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

contract SafePayout {
    ISafeToken public immutable token;
    mapping(address => uint256) public rewards;

    error NoRewards();
    error TransferFailed();

    constructor(ISafeToken _token) {
        token = _token;
    }

    function claim() external {
        uint256 amount = rewards[msg.sender];
        if (amount == 0) revert NoRewards();

        // Move state change *before* external call to mitigate reentrancy on this variable
        rewards[msg.sender] = 0;

        bool ok = token.transfer(msg.sender, amount);
        if (!ok) {
            // revert and restore state if needed (not shown here for brevity)
            revert TransferFailed();
        }
    }
}

Security Improvements:

Note: For full reentrancy protection, see SC08 and consider ReentrancyGuard, checks-effects-interactions, and pull-based patterns.

2025 Case Studies

Best Practices & Mitigations