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
}
| Tag | Description |
|---|
unchecked_call | call() return value not verified |
unchecked_delegatecall | delegatecall() return value not verified |
unchecked_send | send() return value not verified |
unchecked_transfer | ERC20 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:
| Method | Gas Forwarded | Reverts on Failure |
|---|
transfer() | 2300 | Yes |
send() | 2300 | No (returns bool) |
call() | All available | No (returns bool) |
transfer() and send() forward only 2300 gas, which may not be enough for receiving contracts with complex fallback functions.