Unchecked Return Values

Solidity's low-level functions (.call(), .send(), .delegatecall()) return a boolean indicating success or failure — they do not automatically revert. If you ignore this return value, a failed transfer or call will go undetected and your contract will keep running with incorrect state.

How it works

Consider a contract that sends Ether using .send(). If the recipient is a contract with a reverting fallback, .send() returns false — but if you don't check it, the contract records the transfer as complete while the user never received the funds.

Vulnerable pattern

// VULNERABLE — return value ignored
function withdraw() public {
    uint amount = balances[msg.sender];
    balances[msg.sender] = 0;
    msg.sender.send(amount);  // may return false, but we don't check
    // Contract thinks withdrawal succeeded regardless
}

// Also common with token transfers:
IERC20(token).transfer(recipient, amount);  // non-reverting ERC-20s return false

Safe pattern

// SAFE — always check return values or use require
function withdraw() public {
    uint amount = balances[msg.sender];
    balances[msg.sender] = 0;
    (bool ok,) = msg.sender.call{value: amount}("");
    require(ok, "Transfer failed");
}

// For ERC-20: use SafeERC20 from OpenZeppelin
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
using SafeERC20 for IERC20;
IERC20(token).safeTransfer(recipient, amount);  // reverts on failure

Real-world impacts

  • Many early Ethereum contracts used .send() without checking the return value, leading to silent stuck Ether.
  • Non-standard ERC-20 tokens (like USDT) that return false instead of reverting have broken protocols that don't use SafeERC20.

How to prevent it

  • Always check the return value of .call(), .send(), and .delegatecall().
  • Use require(success, "reason") immediately after any low-level call.
  • Use OpenZeppelin's SafeERC20 for all ERC-20 transfers.
  • Enable Solhint or Slither linting — they flag unchecked return values.
← Back to Glossary