Skip to main content
Sigvex

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:

  1. A vault calls token.transferFrom(msg.sender, address(this), amount) without wrapping in a require.
  2. When used with USDT, the call succeeds (USDT has no return value) but depositSafe cannot detect failure.
  3. If USDT’s internal require reverts (e.g., insufficient allowance), the vault’s state update is also reverted.
  4. With a token that returns false instead of reverting, the vault would credit the user despite receiving nothing.

Malicious token returning false:

  1. A malicious ERC20 token’s transferFrom always returns false without performing any transfer.
  2. A vault calls token.transferFrom(...) without checking the return value.
  3. The vault credits balances[msg.sender] += amount — the user is credited tokens they never sent.
  4. 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_compatibility issue confirmed (confidence 0.85).
  • Scenario 1 succeeds and Scenario 3 succeeds → unchecked_return_value issue confirmed (confidence 0.85).

The generated PoC demonstrates:

  • VulnerableVault: Silent failures when token transfers are not checked
  • MaliciousToken: Returns false without transferring
  • USDTLike: Transfer functions with no return value
  • SecureVault: Using SafeERC20.safeTransferFrom
  • ManualSafeVault: 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

Use OpenZeppelin SafeERC20 for all token operations. For fee-on-transfer compatibility, measure actual received amounts:

import "@audited/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: transferFrom returns false on failure
  • BNB: Can revert with or without return value depending on context

References