Unchecked ERC20 Operations Exploit Generator
Sigvex exploit generator that validates unsafe ERC20 token operations by simulating standard, USDT-like, and malicious token interactions to detect missing return value checks.
Unchecked ERC20 Operations Exploit Generator
Overview
The unchecked ERC20 operations exploit generator validates findings from the unchecked-erc20, unchecked_transfer, unsafe_erc20, and erc20_return_value detectors by executing the victim contract under four token type configurations and comparing behavior. If a contract credits or debits balances regardless of whether the token transfer actually succeeded, the return value is not being checked.
Multiple DeFi protocols have suffered fund lockups from unchecked ERC20 operations. USDT does not return a boolean from transfer or transferFrom — protocols that call require(token.transfer(...)) will fail to compile, while those that call token.transfer(...) without checking may silently lose funds when the transfer fails.
Note: Exploit generation in Sigvex is for vulnerability validation purposes only.
Attack Scenario
USDT-compatibility failure:
- A vault calls
token.transferFrom(msg.sender, address(this), amount)without wrapping in arequire. - When used with USDT, the call succeeds (USDT has no return value) but
depositSafecannot detect failure. - If USDT’s internal
requirereverts (e.g., insufficient allowance), the vault’s state update is also reverted. - With a token that returns
falseinstead of reverting, the vault would credit the user despite receiving nothing.
Malicious token returning false:
- A malicious ERC20 token’s
transferFromalways returnsfalsewithout performing any transfer. - A vault calls
token.transferFrom(...)without checking the return value. - The vault credits
balances[msg.sender] += amount— the user is credited tokens they never sent. - The user then calls
withdraw(amount)to extract real tokens from the vault pool, effectively stealing from other depositors.
Exploit Mechanics
The generator runs four scenarios with storage slots 0–3 encoding the token type, transfer success, return-value-checking flag, and user balance respectively:
| Scenario | Token type (slot 0) | Transfer success (slot 1) | Checked (slot 2) | Description |
|---|---|---|---|---|
| 1 — Standard | 1 | 1 (success) | 1 (checked) | Baseline |
| 2 — USDT | 2 | 0 (no return) | 0 (unchecked) | Missing return value |
| 3 — Malicious | 3 | 0 (returns false) | 0 (unchecked) | Silent failure |
| 4 — SafeERC20 | 2 (USDT) | 1 | 1 | Protected with SafeERC20 |
The deposit selector 0xd0e30db0 is used as the fallback function selector.
Verdict:
- Scenario 1 succeeds and Scenario 2 succeeds →
usdt_compatibilityissue confirmed (confidence 0.85). - Scenario 1 succeeds and Scenario 3 succeeds →
unchecked_return_valueissue confirmed (confidence 0.85).
The generated PoC demonstrates:
VulnerableVault: Silent failures when token transfers are not checkedMaliciousToken: Returnsfalsewithout transferringUSDTLike: Transfer functions with no return valueSecureVault: UsingSafeERC20.safeTransferFromManualSafeVault: Balance-verification approach for fee-on-transfer tokens
// ATTACKER: Exploit unchecked transferFrom
contract VaultAttacker {
function attack() external {
// MaliciousToken.transferFrom returns false but vault doesn't check
vault.deposit(1000 ether);
// Vault credited us 1000 but we sent nothing
vault.withdraw(1000 ether); // Steal from real depositors
}
}
// SECURE: SafeERC20 handles missing and false return values
contract SecureVault {
using SafeERC20 for IERC20;
function deposit(uint256 amount) external {
token.safeTransferFrom(msg.sender, address(this), amount);
balances[msg.sender] += amount;
}
}
Remediation
- Detector: Unchecked ERC20 Detector
- Remediation Guide: Unchecked ERC20 Remediation
Use OpenZeppelin SafeERC20 for all token operations. For fee-on-transfer compatibility, measure actual received amounts:
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
// RECOMMENDED: SafeERC20 handles all edge cases
token.safeTransferFrom(msg.sender, address(this), amount);
token.safeTransfer(recipient, amount);
// FOR FEE-ON-TRANSFER TOKENS: Measure actual received amount
uint256 balanceBefore = token.balanceOf(address(this));
token.safeTransferFrom(msg.sender, address(this), amount);
uint256 actualReceived = token.balanceOf(address(this)) - balanceBefore;
balances[msg.sender] += actualReceived; // Not 'amount'
Known non-standard tokens requiring SafeERC20:
- USDT: No return value
- ZRX:
transferFromreturnsfalseon failure - BNB: Can revert with or without return value depending on context