Crypto Training

Audit Checklist: Safe ERC-20 Integration in a Hostile World

Treat token transfers as adversarial. Measure deltas, expect lies, and design accounting that survives hooky or weird tokens.

Crypto Training2026-02-062 min read

If you build DeFi, you integrate tokens. If you integrate tokens, you inherit their behavior.

In 2026, “ERC-20” describes a surface area, not a guarantee.

This post is a practical integration guide that aims to keep you alive when reality diverges from the spec.

The taxonomy of weirdness#

Here’s a compact table I use when reviewing token integrations:

Token behaviorWhat breaksWhat to do
fee-on-transferexact in/out mathcompute deltas; use adapters
rebasingfixed balance assumptionsuse shares; avoid absolute invariants
hooks/callbacksreentrancy + gas griefingisolate external calls; guards
blacklist / pausablelivenessavoid pushing transfers; pull patterns
non-standard returnstransfer calls lieuse SafeERC20

Delta-based accounting (default)#

When exact amounts matter, compute what arrived.

SOLIDITY
uint256 beforeBal = token.balanceOf(address(this));
SafeERC20.safeTransferFrom(token, msg.sender, address(this), amount);
uint256 received = token.balanceOf(address(this)) - beforeBal;
require(received > 0, "no tokens received");

Then base shares/credit on received, not on amount.

Don’t mix external calls with critical accounting#

A good rule:

  • if an external call can revert, it should not be able to freeze unrelated state transitions

This is why “transfer inside accounting” is suspicious.

If you must do it, structure the logic so you can safely resume.

Approvals and allowance hazards#

Allowances are their own attack surface:

  • tokens that require resetting allowance to 0 first
  • infinite approvals that become permanent risk
  • permit flows that can be front-run to cause DoS

A practical integration stance:

  • prefer exact approvals with bounded scope
  • handle “permit already used” as a normal state

A short scenario (what fails in production)#

You deploy a router that assumes amountIn is what arrives.

A fee-on-transfer token arrives with amountIn - fee.

Your router now:

  • misprices swaps
  • breaks invariant checks
  • reverts unpredictably

The bug isn’t in the AMM. It’s in the integration assumption.

Integration checklist#

Instead of a long list, these are the questions that catch most issues:

  1. Can the user choose the token (adversarial surface)?
  2. Does accounting assume exact transfer amounts?
  3. Can a token revert and freeze a liveness-critical function?
  4. Can a token reenter through hooks during an invariant window?
  5. What is the failure mode: revert, queue, or safe partial progress?

Further reading#