Medium severity
ERC-20 functions that return nothing: the incorrect ERC-20 interface
Updated 2026-10-24
The ERC-20 standard says transfer, transferFrom and approve each return a bool. Many early tokens shipped without it. Tether’s USDT contract on Ethereum returns nothing from all three; BNB on Ethereum returns nothing from transfer but a bool from the other two; and a June 2018 check of the tokens listed on the EtherDelta exchange found 130 whose transfer returns no bool. That used to go unnoticed. Code built with Solidity 0.4.22 or later reverts when a call returns less data than the declared return type needs, so a token that returns nothing cannot be moved by any contract that calls it through the standard IERC20 interface. We reproduced the split on a local EVM: callers built with solc 0.4.19 and 0.4.21 went through, while callers built with 0.4.22, 0.4.24 and 0.8.19 reverted with empty revert data.
Vulnerable pattern
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
contract Token {
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
constructor(uint256 supply) {
totalSupply = supply;
balanceOf[msg.sender] = supply;
}
// BUG: ERC-20 says these return bool. They return nothing, so a
// contract that calls them through IERC20 reverts on every call.
function transfer(address to, uint256 amount) external {
balanceOf[msg.sender] -= amount;
balanceOf[to] += amount;
}
function approve(address spender, uint256 amount) external {
allowance[msg.sender][spender] = amount;
}
function transferFrom(address from, address to, uint256 amount) external {
allowance[from][msg.sender] -= amount;
balanceOf[from] -= amount;
balanceOf[to] += amount;
}
}Picture a vault, a DEX pair or a bridge that moves tokens with require(token.transfer(to, amount)) through IERC20. Against this token the call reverts even though the transfer itself would have worked, and it still reverts when the caller ignores the result (token.transfer(to, amount);), because the compiler checks the returned data either way. The plain case fails at the first deposit, so it is found early. The costly case is a mix: if the deposit path tolerates the missing value but the withdrawal path does not, tokens go in and never come out. The weird-erc20 catalogue notes that BNB’s mix of returning and non-returning functions “resulted in stuck BNB tokens in Uniswap v1”.
The fix
If you write the token, declare the standard return types and return true on success; let failures revert, which is what most callers expect.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
contract Token {
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
constructor(uint256 supply) {
totalSupply = supply;
balanceOf[msg.sender] = supply;
}
// FIX: declare the standard return types and return true on success.
function transfer(address to, uint256 amount) external returns (bool) {
balanceOf[msg.sender] -= amount;
balanceOf[to] += amount;
return true;
}
function approve(address spender, uint256 amount) external returns (bool) {
allowance[msg.sender][spender] = amount;
return true;
}
function transferFrom(address from, address to, uint256 amount) external returns (bool) {
allowance[from][msg.sender] -= amount;
balanceOf[from] -= amount;
balanceOf[to] += amount;
return true;
}
}If you integrate tokens, you cannot fix one that is already deployed, so accept both shapes. OpenZeppelin’s SafeERC20 does this: “Tokens that return no value (and instead revert or throw on failure) are also supported, non-reverting calls are assumed to be successful.” Uniswap V2 uses the same idea in a low-level call, require(success && (data.length == 0 || abi.decode(data, (bool)))). The bare form has one gap: a call to an address with no code also succeeds with empty data, so check that the token has code, as SafeERC20 does before it accepts an empty return. The other half of this bug class, a caller that ignores the returned bool or checks it through the interface, is covered in unchecked ERC-20 transfer.
A note on scope
Slither’s erc20-interface detector looks at each most-derived contract that has transfer(address,uint256), transferFrom(address,address,uint256) or approve(address,uint256), skips any contract that also looks like an ERC-721 (for example one with ownerOf(uint256)), and reports each ERC-20 function whose return types are not exactly bool (transfer, transferFrom, approve) or uint256 (allowance, balanceOf, totalSupply). It checks signatures only. We ran the variants: a function inherited from a base contract is reported on the token that inherits it; a transfer that declares returns (bool) but never returns true, so it always returns false, is not reported; a public state variable such as mapping(address => uint128) public balanceOf is not checked, because its getter is not a declared function; and a contract that also has ownerOf is skipped. An interface that declares the non-standard shape on purpose, to call such tokens, is flagged by Slither as well; pactlint treats that as the integrator’s deliberate choice and demotes it to informational.
Scan your contract for this
pactlint flags erc20-interface 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.
An ERC-20 function with the wrong return type is reported by Slither’s stock
erc20-interface detector (“Incorrect erc20 interface”),
rated medium impact at high confidence, which pactlint surfaces at medium severity: it is not downgraded or denied in policy and
no description rule touches it; its one triage rule demotes a flagged interface, as opposed to a token, to
informational. We confirmed this on solc 0.8.19 with Slither 0.11.6 through the full scanner: the Token contract
above is reported at medium severity, the fixed version scans with zero findings, and a contract that only declares a
USDT-style interface to call such a token shows no finding at the default level. Token facts are taken from the
verified Ethereum source of USDT and BNB, the weird-erc20 catalogue and the June 2018 list of tokens missing return values.
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.