skills/vulnerability-patterns/unsafe-erc20-transfers/SKILL.md
Unsafe ERC20 transfer and approve calls that silently fail on non-standard tokens.
npx skillsauth add apegurus/solidity-argus unsafe-erc20-transfersInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
The standard ERC20 interface specifies that transfer(), transferFrom(), and approve() return a bool indicating success. However, many widely-used tokens deviate from this standard:
transfer/approvefalse on failure instead of revertingContracts that call these functions directly (without SafeERC20) either:
Severity: Low to Medium
Prevalence: Found in 4 independent BailSec audits: Hypertrade V3 Core, Meuna, Robinos, SwapX Exchange.
// VULNERABLE: Direct transfer — no return value check
function withdraw(address token, uint256 amount) external {
IERC20(token).transfer(msg.sender, amount);
// If token returns false instead of reverting, this silently fails
// If token doesn't return bool (USDT), this reverts unexpectedly
balances[msg.sender] -= amount; // State updated even if transfer failed!
}
// VULNERABLE: Direct approve — breaks with USDT
function approveSpender(address token, address spender, uint256 amount) external {
IERC20(token).approve(spender, amount);
// USDT requires setting allowance to 0 before changing to non-zero
// Direct approve also doesn't handle missing return values
}
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
using SafeERC20 for IERC20;
// SECURE: SafeERC20 handles all non-standard token behaviors
function withdraw(address token, uint256 amount) external {
IERC20(token).safeTransfer(msg.sender, amount);
// Reverts on failure for ALL token types
balances[msg.sender] -= amount;
}
// SECURE: forceApprove handles USDT's approve quirk
function approveSpender(address token, address spender, uint256 amount) external {
IERC20(token).forceApprove(spender, amount);
// Sets to 0 first if needed (USDT), handles missing return values
}
false but contract proceeds as if successful — leads to accounting mismatch| Token | Issue | Consequence | |-------|-------|-------------| | USDT | No bool return on transfer/approve | Reverts if caller expects bool return | | USDT | Requires approve(0) before approve(N) | Approve fails for non-zero to non-zero | | BNB | Missing return value | Reverts on standard interface call | | OMG | Missing return value | Reverts on standard interface call | | ZRX | Returns false on failure (no revert) | Silent failure if return unchecked |
IERC20.transfer() or IERC20.transferFrom() directly?SafeERC20 imported and applied via using SafeERC20 for IERC20?safeTransfer, safeTransferFrom, and forceApprove used instead of raw calls?.call(), .send(), .delegatecall() return values — different from ERC20 interface returnsusing SafeERC20 for IERC20 for all ERC20 interactionsapprove() with forceApprove() to handle USDTtesting
Specialist profile for mechanically applying the attack-vector deck and classifying vectors as skip, drop, or investigate.
tools
Specialist profile for libraries, helpers, base contracts, adapters, encoders, wrappers, and integration glue.
testing
Specialist profile for rounding, scale, decimal, downcast, and arithmetic accounting edge cases.
testing
Specialist profile for extracting conservation laws and state couplings, then searching for violating paths.