Skip to main content

Overview

Unchecked low-level calls occur when Solidity’s low-level functions (call, delegatecall, staticcall) are used without properly verifying their return values.
Failed low-level calls return false instead of reverting. If not checked, transactions appear successful while silently failing.

The Risk

Low-level calls in Solidity don’t automatically revert on failure. If the return value isn’t checked:
  • Funds can be “sent” but never arrive
  • State changes occur based on failed operations
  • Users lose assets without error messages

Vulnerable Patterns

Unchecked Call

// VULNERABLE: Return value ignored
function withdraw(uint256 amount) public {
    balances[msg.sender] -= amount;
    payable(msg.sender).call{value: amount}("");  // Silent failure!
}

Unchecked Delegatecall

// VULNERABLE: Delegatecall without check
function upgrade(address impl) public onlyOwner {
    impl.delegatecall(abi.encodeWithSignature("initialize()"));
}

Unchecked Transfer

// VULNERABLE: ERC20 transfer without check
function distribute(address token, address[] memory recipients) public {
    for (uint i = 0; i < recipients.length; i++) {
        IERC20(token).transfer(recipients[i], 100);  // May fail silently
    }
}

Safe Patterns

Check Return Value

// SAFE: Explicit check
function withdraw(uint256 amount) public {
    balances[msg.sender] -= amount;
    (bool success, ) = payable(msg.sender).call{value: amount}("");
    require(success, "Transfer failed");
}

Use SafeERC20

// SAFE: SafeERC20 library
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

using SafeERC20 for IERC20;

function distribute(address token, address[] memory recipients) public {
    for (uint i = 0; i < recipients.length; i++) {
        IERC20(token).safeTransfer(recipients[i], 100);  // Reverts on failure
    }
}

Use Transfer/Send (Limited)

// SAFE: transfer() reverts on failure (but limited gas)
function withdraw(uint256 amount) public {
    balances[msg.sender] -= amount;
    payable(msg.sender).transfer(amount);  // Reverts if fails
}

Detection Tags

TagDescription
unchecked_callcall() return value not verified
unchecked_delegatecalldelegatecall() return value not verified
unchecked_sendsend() return value not verified
unchecked_transferERC20 transfer() return not verified

API Response Example

{
  "issues": [
    {
      "tag": "unchecked_call",
      "severity": "high",
      "description": "Low-level call without return value check",
      "location": "withdraw(uint256):L45"
    }
  ]
}

Why Low-Level Calls?

Despite risks, low-level calls are sometimes necessary:
MethodGas ForwardedReverts on Failure
transfer()2300Yes
send()2300No (returns bool)
call()All availableNo (returns bool)
transfer() and send() forward only 2300 gas, which may not be enough for receiving contracts with complex fallback functions.