Weird ERC-20 Tokens: 12 Edge Cases and a Checklist
Quick answer. Weird ERC-20 tokens break protocols that assume standard behavior. Check for fee-on-transfer and rebasing tokens, callbacks, false returns, missing return values, blacklists, approval rules, multiple addresses, upgrades, flash minting, max-amount transfers and unusual decimals.
ERC-20 tokens are used in practically every DeFi protocol. Weird ERC-20 tokens, with their many implementations, bring intricate edge cases.
Countless findings in security reviews stem from them.
These edge cases turn into vulnerabilities because people do not know about them. A protocol never handles a behavior its developers never heard of.
Below are 12 weird ERC-20 token behaviors, from the common to the esoteric, plus two bonus ones. Keep a running list of any you did not know. A checklist at the end sums them up.
Balances and Callbacks
1. Fee-on-transfer and rebasing tokens
The crux here is tokens that change balances on transfer.
A fee-on-transfer token takes a cut. Transfer 100 tokens with a fee of 6, and the recipient gets 94. A rebasing token can trigger a rebase on transfer, so the recipient may get more than was sent.
Many systems do not account for this. If a protocol accepts arbitrary tokens, or plans to support these ones, it must.
Do not transfer amount and assume you received amount. Measure what actually arrived with a balance check, and use that in your accounting.
// Do not do this:
token.transferFrom(msg.sender, address(this), amount);
_credit(msg.sender, amount); // assumes the full amount arrived
// Measure what actually arrived:
uint256 balBefore = token.balanceOf(address(this));
token.transferFrom(msg.sender, address(this), amount);
uint256 received = token.balanceOf(address(this)) - balBefore;
_credit(msg.sender, received);
2. Tokens with callbacks (ERC-777)
Some tokens call out to an account during a transfer. That creates reentrancy potential.
Depending on the implementation, an arbitrary token could call the sender or the receiver, before or after the balances update. If a protocol accepts arbitrary tokens, assume it could.
Then ask: is the sender trusted? Is the receiver trusted? Could either one exploit the system if it received control at that point, possibly catching it in an invalid state?
If tokens with callbacks are in scope, make sure there is no reentrancy potential. Our guide to reentrancy attacks covers every type that can follow.
Return Values and Reverts
3. Returning false vs reverting on failure
When a transfer, transferFrom or approve fails, some tokens return false and others revert. These are two completely different behaviors.
This is the whole reason safeTransfer exists. It wraps the tokens that return false and makes them revert too, so every token fails the same way.
If you are not using safeTransfer, you had better check the return value. Even better, use safeTransfer.
4. Reverts on the zero address or a zero amount
Many tokens do not allow transfers to or from the zero address. Some also revert when you transfer an amount of zero.
Even OpenZeppelin's ERC-20 implementation reverts on transfers from or to the zero address.
Either behavior can be a knob for a denial of service attack, or for something like a risk-free trade in a more complex system.
Work out which tokens the system expects and whether they behave this way. If the system accepts arbitrary tokens, handle both cases.
5. No return values
The IERC20 interface says transfer, transferFrom and approve return a boolean. On Ethereum mainnet, USDT's versions of all three return nothing.
That breaks any contract whose interface expects return data to decode as a boolean.
interface IToken {
function approve(address spender, uint256 amount) external returns (bool);
}
// USDT's approve returns nothing, so decoding the bool reverts:
return IToken(usdt).approve(spender, amount);
Call a token with no return value through that interface and the call reverts. There is no return data to decode. Give the token a return false and the same call passes.
So a protocol that interacts with tokens through a standard interface can be unusable with a token like USDT.
Blacklists and Approvals
6. Blacklists
This one is a personal favorite, found while auditing the GMX V2 system. An overly complex attack needed a token transfer to revert, and blacklisted addresses turned out to be the answer.
Plenty of tokens have a blacklist. USDT's transfer requires that the sender is not blacklisted, and its transferFrom checks the from address.
USDC's implementation applies a notBlacklisted modifier to the sender, from and to of transfers, and to approvals too. If any of them is blacklisted, the transaction reverts.
That becomes an exploit whenever a protocol needs a transaction to go through, but you can force it to send tokens to a blacklisted address.
Liquidations are a key case: they must happen fast, or the position goes insolvent and the protocol takes on bad debt.
If leftover collateral goes to a receiver that is blacklisted for the collateral token, the liquidation reverts. See our guide to liquidation vulnerabilities.
7. Subsequent nonzero approvals
USDT's approve will not change a nonzero allowance to another nonzero value. To go from 5 to 10, you must approve 0 first, then 10.
The reason is the approval front-running attack. Say you approve Bob for 5 tokens, then owe him 5 more, so you send a transaction to approve 10.
- The expected outcome: Bob's allowance goes from 5 to 10, and he transfers 10. He gets 10 in total.
- The attack: Bob front-runs your approval and transfers the 5 he is already allowed. Then your approval lands and sets his allowance to 10, which he also takes. Bob gets 15.
Requiring a reset to zero stops this. After setting the allowance to 0, you can see whether Bob already took 5, and approve only 5 more. Bob ends up with the 10 you intended.
But if your protocol uses USDT, you must handle this rule. A nonzero to nonzero approval reverts, which can be a denial of service knob for a much worse exploit.
Addresses, Upgrades and Minting
8. Multiple addresses for one token
This applies to a narrow set of systems. Never assume a token's balances can only change through one address.
Imagine a rule like "this token cannot be called after a transfer of it into the system". It is convoluted, maybe a sign of bad design, and it relies on that assumption.
Some tokens, reportedly including TrueUSD, have multiple contract addresses that control the same balances mapping. Calling transfer on either one changes the same balance.
At a fundamental level this applies to any token. Anyone can write a wrapper around DAI whose functions simply make external calls to DAI. For all intents and purposes, the wrapper is another way to interact with DAI.
9. Upgradeable tokens
Never assume an upgradeable token's implementation will stay the same forever.
USDC, for example, is upgradeable. Technically, its owners could add any new feature, even callbacks.
Some tokens also carry latent features that are not switched on, such as a transfer fee. The behavior today might not be the behavior tomorrow.
10. Flash-mintable tokens
Some tokens, such as DAI, can be flash minted. You borrow a very large amount within a single transaction, then repay it plus a little interest at the end.
It is much like a flash loan from a swap, without the swap. That is a powerful knob for manipulating any system that uses the token.
Amounts and Decimals
11. Transfers of less than the amount
Some tokens treat an amount of type(uint256).max as "the sender's whole balance" and transfer that instead.
It is not fee-on-transfer or rebasing, but the effect is similar: the amount received differs from the amount specified. If such tokens are in scope, that can easily become a critical vulnerability.
12. Tokens with lots of decimals
USDC has 6 decimals, and some tokens have as few. Others have more than 18.
The YAM V2 token has 24 decimals. Decimal accounting that only handles tokens up to 18, or even 22, decimals can break on it.
Two Bonus Edge Cases
Pausable tokens. Some tokens let a centralized, trusted address pause transfers for a period, so every transaction reverts. It looks much like a blacklist, and it is a counterparty risk you take on when you integrate the token.
Reverts on large approvals or transfers. UNI and COMP revert if the approval or transfer amount is larger than uint96. A huge transfer would fail anyway, but a failing "approve a really large amount" call can surprise a protocol.
Weird ERC-20 Tokens Checklist
For every token a protocol touches, or for arbitrary tokens, check:
- Accounting uses the balance actually received, not the amount sent.
- Callbacks to the sender or receiver cannot re-enter the system.
- Transfers use
safeTransfer, or check the return value. - Zero amounts and zero addresses cannot block a critical path.
- Tokens with no return values, such as USDT, work with your interfaces.
- A blacklisted recipient cannot make a must-succeed transaction revert.
- Approvals reset to zero before changing a nonzero allowance.
- Nothing assumes a token's balances change through only one address.
- The design survives a token upgrade or a latent feature being switched on.
- Flash-minted supply cannot manipulate the system.
- A
type(uint256).maxamount cannot desync accounting. - Decimal math handles tokens with more than 18 decimals.
- Pausable tokens and
uint96limits are accounted for.
Fee-on-transfer tokens also appear among the sneaky Solidity bugs that keep reaching audits.
How Guardian Helps
Guardian's smart contract audits test integrations, accounting and edge states, with invariant suites that pressure-test every reachable state.
With Vanguard, two independent teams review the same scope in parallel.
Frequently Asked Questions
What are weird ERC-20 tokens?
They are tokens whose behavior departs from what protocols assume. Examples include fees on transfer, callbacks, missing return values, blacklists, strict approval rules and unusual decimals.
How do you handle fee-on-transfer tokens?
Record the contract's token balance before the transfer, transfer, then subtract to get the amount actually received. Use that received amount in all accounting.
Why does USDT break some integrations?
USDT's transfer, transferFrom and approve return no value, so interfaces that expect a boolean revert. It also has a blacklist, and it rejects changing one nonzero allowance to another.
Why use safeTransfer?
Some tokens return false on failure while others revert. safeTransfer makes tokens that return false revert too, so every failure behaves the same way.
Sources
Review your token integrations with Guardian
Guardian's researchers test every token behavior your protocol could meet before it goes live.