Security

ERC-20 Integration Hell: USDT, Fee-on-transfer, and Others

There is a common misconception in smart contract development: we tend to view ERC-20 as a rigid standard. We import IERC20.sol, write transfer, and expect every token to behave exactly like WETH or USDC.

In reality, ERC-20 is not a strict rulebook but a set of guidelines. Top tokens with billions in market cap violate specifications, contain hidden fees, or possess upgradable logic that can halt your protocol. I see these errors constantly in audits. Let’s break down the three main scenarios where protocols lose money.

1. The USDT Issue: Missing Return Bool USDT is the most popular stablecoin in the world, yet ironically, it does not comply with the ERC-20 standard.

The Core Issue: According to the ERC-20 specification, transfer and transferFrom functions must return a boolean value (true/false) to signal success. However, in the USDT contract (on Ethereum Mainnet), these functions return nothing (void).

What Happens: You use the standard IERC20 interface, which expects a bool. Your contract calls transfer on USDT. The stablecoin executes the transfer but returns no data. Your contract attempts to decode the response, finds emptiness where true should be, interprets this as a failure, and reverts.

The Solution: Never use the standard interface for external tokens. Always use the SafeERC20 library from OpenZeppelin and the safeTransfer / safeTransferFrom methods. They handle the non-standard behavior of USDT correctly.

2. Fee-on-transfer Tokens There is a class of tokens (often meme-coins or deflationary assets like PAXG) that charge a fee on every transfer. You send 100 tokens, but the recipient receives 98. The remaining 2 are burned or sent to the project treasury.

The Disaster Scenario: Imagine a staking contract.

  1. A user calls stake(100).
  2. The contract executes transferFrom for 100 tokens.
  3. The user's balance mapping is updated: balances[user] = 100.
  4. But in reality, the contract only received 98 tokens.

The protocol becomes instantly insolvent. The last user attempting to withdraw funds will fail because the contract physically holds fewer tokens than recorded in its liabilities.

The Solution: Do not trust input parameters. Check the contract balance before and after the transfer.
uint256 balanceBefore = token.balanceOf(address(this));
token.transferFrom(msg.sender, address(this), amount);
uint256 balanceAfter = token.balanceOf(address(this));
uint256 actualAmount = balanceAfter - balanceBefore;
// Record actualAmount, not amount
3. Inflation Attack on Vaults (ERC-4626) This is a classic vulnerability in liquidity pools and vaults, based on rounding errors.

The Mechanics: In most vaults, users receive "shares" in exchange for assets. The formula typically looks like this: Shares = (Assets * TotalShares) / TotalAssets

An attacker (the first depositor) does the following:

  1. Deposits 1 wei of the asset (receiving 1 share).
  2. Directly sends a massive amount (e.g., 1000 ETH) to the contract via transfer, bypassing the deposit logic.
  3. The pool now holds 1000 ETH + 1 wei, but only 1 share exists. The price of a single share becomes astronomical.

The Impact: A regular user arrives and deposits 20 ETH. The formula divides their deposit by the inflated pool value. Due to integer division in Solidity, the result rounds down to 0. The user sends 20 ETH and receives 0 shares. The attacker (holding the only share) withdraws everything.

The Solution: This issue is solved in two ways:

  1. Dead Shares: On the very first mint, send a small portion of shares (e.g., 1000 wei) to the 0xdead address, permanently locking them. This makes the attack economically unviable.
  2. Internal Offset: Use virtual offsets in the asset calculation formula (as implemented in recent OpenZeppelin ERC4626 versions) so the denominator is never too small.

Integrating third-party tokens always means working with untrusted code. You cannot control how another token is written, but you must protect your architecture from its quirks. Using SafeERC20, validating real balances, and protecting against rounding errors is the hygienic minimum for any DeFi protocol.