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.
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 behavior | What breaks | What to do |
|---|---|---|
| fee-on-transfer | exact in/out math | compute deltas; use adapters |
| rebasing | fixed balance assumptions | use shares; avoid absolute invariants |
| hooks/callbacks | reentrancy + gas griefing | isolate external calls; guards |
| blacklist / pausable | liveness | avoid pushing transfers; pull patterns |
| non-standard returns | transfer calls lie | use SafeERC20 |
Delta-based accounting (default)#
When exact amounts matter, compute what arrived.
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:
- Can the user choose the token (adversarial surface)?
- Does accounting assume exact transfer amounts?
- Can a token revert and freeze a liveness-critical function?
- Can a token reenter through hooks during an invariant window?
- What is the failure mode: revert, queue, or safe partial progress?
Further reading#
- OpenZeppelin
SafeERC20docs: https://docs.openzeppelin.com/contracts/5.x/api/token/erc20 - “What happens when you send one DAI”: https://www.notonlyowner.com/learn/what-happens-when-you-send-one-dai