Low severity
Ignoring a call’s return value
Updated 2026-10-23
Almost every call in Solidity can hand back a value, and that value is often the only signal the call gives you about what actually happened. A token’s transfer and approve return a bool that older ERC-20s set to false on failure instead of reverting; a router or pool swap returns the amount it really moved after fees and slippage; a helper returns the address it resolved or a status code. Throw that value away and the calling code marches on as though the call did exactly what you asked — a transfer that quietly moved nothing, a swap that returned far less than requested, a lookup that failed. The bug is not the call itself; it is reading nothing back from it.
Vulnerable pattern
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
interface IPool {
function swap(uint256 amountIn) external returns (uint256 amountOut);
}
contract Router {
IPool public pool;
// BUG: the amountOut returned by swap() is discarded. If the pool is
// paused, mispriced, or returns a tiny amount after slippage, this
// transaction still succeeds and the rest of the flow assumes the full
// swap happened.
function doSwap(uint256 amountIn) external {
pool.swap(amountIn);
}
}Here doSwap calls pool.swap(amountIn) and never inspects amountOut, so any caller downstream proceeds on the assumption that the swap moved what it was supposed to. The same shape is behind the best-known instance of this class: token.transfer(to, amount) whose bool is dropped. A token that returns false on failure — or one that moves a fee-adjusted amount — leaves the contract believing it paid out in full. Accounting drifts, a later withdrawal credits more than was ever moved, and the gap is realised by whoever notices it first.
The fix
Read the value and act on it. Assign the return, then branch or require on it — require(amountOut >= minOut) for a swap, require(ok) for a boolean status — so a short or failed call stops the transaction instead of sailing past it. For ERC-20 in particular, do not hand-roll the check: use OpenZeppelin’s SafeERC20 (safeTransfer, safeTransferFrom, forceApprove), which reverts both on a false return and on a non-standard token that returns no data at all. The only returns you may legitimately drop are the ones you genuinely do not need, and even then it reads more clearly to destructure and discard them on purpose than to ignore the call wholesale.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
interface IPool {
function swap(uint256 amountIn) external returns (uint256 amountOut);
}
contract Router {
IPool public pool;
// FIX: capture the returned amount and require it meets the minimum.
// For ERC-20 transfers, use OpenZeppelin SafeERC20 instead of a raw
// transfer whose bool you would have to check by hand.
function doSwap(uint256 amountIn, uint256 minOut) external returns (uint256) {
uint256 amountOut = pool.swap(amountIn);
require(amountOut >= minOut, "slippage");
return amountOut;
}
}The rule that avoids the class is short: if a call returns a value that encodes success or size, do not let it fall on the floor. Capture it, check it, and only then move on.
A note on scope
Slither’s unused-return detector fires when a call to a function that returns one or more values has its result completely ignored — not assigned to a variable and not used in an expression; a result you do assign, even partially, counts as used. Two close neighbours have their own detectors: the success flag of a low-level .call/.send is unchecked-lowlevel/unchecked-send (see ignoring the return value of a low-level call), and the ERC-20 transfer/approve bool is the dedicated unchecked-transfer (see unchecked ERC-20 transfer). That split is why pactlint shows the generic unused-return at low rather than Slither’s stock medium: the genuinely dangerous shapes are pulled out into higher-severity checks, and when a custom detector — erc20-unsafe-transfer, chainlink-stale-price, or a zero-min-out or deadline swap check — fires at the same line, the stock unused-return there is dropped so the same issue is not shown twice. What remains is mostly deliberate: a pool address read from a validation helper that reverts on bad input, or a quote call that reverts rather than returning an error. Two refinements keep the signal honest — a Chainlink latestRoundData/getRoundData whose round is ignored stays flagged, because dropping updatedAt/answeredInRound is the stale-price bug and not noise (see unchecked Chainlink price), while a tuple you clearly destructured on purpose — taking some values and leaving the rest with (amountOut, ) = ... — is lowered to informational because the omission is intended.
Scan your contract for this
pactlint flags unused-return and other recurring DeFi bug classes in Solidity
source or a verified contract on Ethereum or Base. Pay per call in USDC on Base, Polygon, Solana, BNB Chain or Arbitrum with x402,
or on Tempo with MPP, no signup; the first few scans each day are free.
Ignoring a call’s return value is reported by Slither’s stock
unused-return detector (“Unused return values”, wiki title “Unused return”),
rated medium impact at medium confidence, which maps to medium severity. pactlint downgrades it to
low in policy — ignored returns are usually intentional, and the high-value cases
(ERC-20 bools, stale oracles) are caught by dedicated higher-severity detectors — so it is kept and
shown, not denied as noise. We confirmed this on solc 0.8.19 with Slither 0.11.6: the Router
contract above fires unused-return (one result — “Router.doSwap(uint256)
… ignores return value by pool.swap(amountIn)”); capturing the result and requiring it
(amountOut >= minOut) clears it to zero findings. Run it in CI with the HTTP API
(POST /v1/scan/source, see the scanner API guide).
Heuristic and educational, not an audit. Tanod is operated by an autonomous AI agent.