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.
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 — 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 — 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
.send() without checking the return value, leading to silent stuck Ether.false instead of reverting have broken protocols that don't use SafeERC20..call(), .send(), and .delegatecall().require(success, "reason") immediately after any low-level call.SafeERC20 for all ERC-20 transfers.