Unchecked return value vulnerabilities occur when contracts ignore the return status of external calls, assuming success and proceeding to modify state. If the external call fails, the contract continues with incorrect assumptions, breaking invariants. Common in token transfers and low-level calls. Firepan's HOUND AI detects unchecked return value vulnerabilities automatically during development and post-deployment monitoring, covering continuous analysis across the full contract lifecycle.
Unchecked return value vulnerability arises when:
The vulnerability is critical because:
Unchecked return value exploitation involves:
// VULNERABLE — example only
// Demonstrates: Unchecked Return Value
// Do NOT use in production
pragma solidity ^0.8.0;
contract VulnerableToken {
mapping(address => uint256) public balances;
// VULNERABLE: Doesn't check transfer return value
function transferTokens(address token, address to, uint256 amount) public {
// Low-level call, return value ignored
token.call(abi.encodeWithSignature("transfer(address,uint256)", to, amount));
// Contract assumes transfer succeeded
// But if call failed (e.g., invalid token), execution continues
}
// VULNERABLE: transfer() return value ignored
function withdrawToken(address token, uint256 amount) public {
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount; // Update state BEFORE checking success
// VULNERABLE: Doesn't check return value
IERC20(token).transfer(msg.sender, amount);
// If transfer fails, balance is still decremented!
}
// VULNERABLE: approve return value ignored
function approveSpender(address token, address spender, uint256 amount) public {
require(balances[msg.sender] >= amount);
// VULNERABLE: Doesn't check return value
IERC20(token).approve(spender, amount);
// Assume approved; but if approve returned false, contract thinks spender is approved
}
// VULNERABLE: transferFrom return value ignored
function receiveTokens(address token, address from, uint256 amount) public {
// Low-level call, return value ignored
(bool success, ) = token.call(
abi.encodeWithSignature("transferFrom(address,address,uint256)", from, address(this), amount)
);
// Ignore return value!
balances[address(this)] += amount;
// If transferFrom failed, balance increased without funds received
}
}
contract UncheckedReturnAttacker {
address public vulnerable;
address public fakeToken;
function exploitTransfer() public {
VulnerableToken target = VulnerableToken(vulnerable);
// Create fake token that returns false on transfer
FakeToken fake = new FakeToken();
// Call target.transferTokens with fake token
// transferTokens calls fake.transfer(), which fails
// But target doesn't check return value, so execution continues as if successful
target.transferTokens(address(fake), address(this), 1000 ether);
}
function exploitWithdraw() public {
// Send 100 ether to target's balance
// Call withdraw()
// withdraw() decrements balance but transfer fails
// Balance is corrupted; attacker can re-withdraw
target.withdrawToken(fakeToken, 100 ether);
target.withdrawToken(fakeToken, 100 ether); // Withdraw again! Balance already at 0
}
}
contract FakeToken {
function transfer(address to, uint256 amount) public pure returns (bool) {
// Always return false (failure)
return false;
}
function transferFrom(address from, address to, uint256 amount) public pure returns (bool) {
return false;
}
function approve(address spender, uint256 amount) public pure returns (bool) {
return false;
}
}
| Protocol | Date | Loss | Root Cause | |----------|------|------|-----------| | Audius | 2022 | $18M | Unchecked token transfer return in delegation | | Compound | 2020 | $19M | Unchecked cToken.transfer return value | | SIlvergate | 2021 | $1M+ | Unchecked approve return in wrapped tokens |
Manual detection requires return value verification:
Red flags:
Firepan's HOUND AI performs return value analysis:
Firepan's HOUND AI engine identifies unchecked return vulnerabilities across all monitored contracts.
1. Always Check Low-Level Call Return Values
Never ignore (bool, bytes) from .call():
// VULNERABLE
function sendEther(address to, uint256 amount) public {
to.call{value: amount}(""); // Return value ignored!
}
// SECURE
function sendEther(address to, uint256 amount) public {
(bool success, ) = to.call{value: amount}("");
require(success, "ETH transfer failed");
}
2. Use SafeERC20 Wrapper
OpenZeppelin's SafeERC20 checks return values:
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
contract SecureToken {
using SafeERC20 for IERC20;
function withdraw(address token, uint256 amount) public {
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount;
// SafeERC20.transfer checks return value and reverts on failure
IERC20(token).safeTransfer(msg.sender, amount);
}
}
3. Check Token Transfer Return Values
Always verify transfer() and transferFrom() return true:
// SECURE
function receiveTokens(address token, address from, uint256 amount) public {
require(
IERC20(token).transferFrom(from, address(this), amount),
"Transfer failed"
);
balances[address(this)] += amount;
}
4. Verify Approve Return Values
Check that approve() returns true:
// SECURE
function approveSpender(address token, address spender, uint256 amount) public {
require(
IERC20(token).approve(spender, amount),
"Approval failed"
);
}
5. Update State After Verified Calls
Always modify state AFTER confirming external call succeeded:
// SECURE: Check BEFORE state update
function withdraw(address token, uint256 amount) public {
require(balances[msg.sender] >= amount);
// Verify transfer succeeds
require(
IERC20(token).transfer(msg.sender, amount),
"Transfer failed"
);
// Update state AFTER verified success
balances[msg.sender] -= amount;
}
Q: What is unchecked return value in smart contracts?
A: Unchecked return value occurs when contracts ignore the return status of external calls, assuming success and modifying state. If the call fails, state is corrupted, causing financial loss or authorization bypass.
Q: Which protocols have been exploited via unchecked return?
A: Audius ($18M, 2022) suffered unchecked token transfer in delegation. Compound ($19M, 2020) had unchecked cToken transfer return. Silvergate ($1M+, 2021) had unchecked approve return in wrapped tokens.
Q: How does Firepan detect unchecked return value?
A: Firepan enumerates external calls, traces return value usage, verifies returned values are checked, confirms state updates occur after return checks, detects ERC20 patterns, and models silent failure scenarios.
Q: Can unchecked return value be exploited after deployment?
A: Yes, unchecked return vulnerabilities are immediately exploitable post-deployment. Attackers use fake tokens or non-standard ERC20 implementations to trigger silent failures.
Q: How do I prevent unchecked return value?
A: Always check (bool, bytes) from low-level calls. Use SafeERC20 wrapper for token operations. Verify transfer() and transferFrom() return values. Verify approve() return values. Update state after calls, not before.
Unchecked return values have cost DeFi over $50M, particularly in token operations. Audius's $18M loss and Compound's $19M loss demonstrated the impact. Using SafeERC20 and verifying all return values eliminates this vulnerability class. Firepan's HOUND AI detects missing return value checks across all monitored contracts.
Start securing your smart contracts at https://app.firepan.com/
Firepan
12,453 contracts secured. 2,851 vulnerabilities blocked. 236 exploits prevented. Run a free surface scan — results in minutes, no credit card required.
Run Free Scan →