Smart Contract Security

Unchecked Return Values: The Overlooked Vulnerability That Keeps Costing DeFi

Kennedy OwiroJanuary 16, 20267 min read

In Solidity, failed external calls don't always revert the transaction. Some return false silently. If your contract doesn't check that return value, it continues execution as if the call succeeded — leading to inconsistent state, lost funds, and exploitable logic. USDT's non-standard transfer() that returns nothing instead of a boolean has broken hundreds of contracts.

The Problem: Silent Failures

// VULNERABLE: Low-level call doesn't revert on failure
(bool success, ) = recipient.call{value: amount}("");
// 'success' is false but we never check it!
balance -= amount;  // State updated even though transfer failed

// VULNERABLE: Some tokens don't return a boolean
IERC20(token).transfer(to, amount);  // USDT returns nothing → silent fail

The ERC-20 standard says transfer() should return bool, but many popular tokens (USDT, BNB, OMG) either return nothing or return false instead of reverting. If your code expects a boolean return, these tokens break silently.

Common Unchecked Return Patterns

1. Unchecked call()

Low-level .call() returns a boolean but doesn't revert on failure. You must check it.

2. Non-Standard ERC-20 Tokens

USDT, USDC (partially), BNB, and ~300 other tokens have non-standard return behavior.

3. Unchecked send() and transfer()

.send() returns false on failure rather than reverting. .transfer() does revert but is gas-limited.

The SafeERC20 Solution

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

contract SecureVault {
    using SafeERC20 for IERC20;

    function deposit(IERC20 token, uint256 amount) external {
        // SafeERC20 handles non-standard tokens and checks return values
        token.safeTransferFrom(msg.sender, address(this), amount);
    }

    function withdraw(IERC20 token, uint256 amount) external {
        token.safeTransfer(msg.sender, amount);  // Reverts if transfer fails
    }
}

// For low-level calls:
(bool success, ) = recipient.call{value: amount}("");
require(success, "Transfer failed");  // Always check!
  • ✅ Always use SafeERC20 for token interactions
  • ✅ Always check return values from .call()
  • ✅ Use require(success) after low-level calls
  • ✅ Test with USDT, BNB, and other non-standard tokens
  • ✅ Avoid .send() — use .call() with require

How Vultbase Detects Unchecked Returns

  1. Slither — Detects unchecked low-level calls, unused return values, and non-SafeERC20 token interactions
  2. Pattern DB — 21 unchecked return patterns from real vulnerabilities
  3. Semgrep — Custom rules for USDT-compatibility issues

Silent failures lead to loud exploits. Audit your external call handling before a non-standard token breaks your protocol.

unchecked returnsolidityexternal callsERC-20token transfersmart contract security
Share

Written by

Kennedy Owiro

Founder & CTO, Vultbase

14+ years building security and QA systems at scale. Background in fintech security and Web3 smart contract testing. Built Vultbase's Intelligence Engine with 1,200+ exploit patterns from $40B+ in historical DeFi losses.

Protect your protocol before launch.

Submit your smart contracts for automated security analysis powered by 1,200+ real exploit patterns.

Start Your Audit →