# ERC-20 approval behaviors are mutually incompatible across major tokens making universal approval handling impossible without token-specific branching or Permit2
Four non-standard approval behaviors create a combinatorial incompatibility matrix where fixing one token's requirement breaks another:
| Token | Behavior | Conflicts with |
|-------|----------|---------------|
| USDT, KNC | Revert if `approve(spender, N)` when current allowance != 0 | BNB |
| BNB | Revert if `approve(spender, 0)` | USDT/KNC zero-first reset |
| OpenZeppelin-based | Revert if `approve(address(0), N)` | Sentinel address patterns |
| DAI, RAI, GLM | `permit()` silently returns on bad signature | Permit-then-transferFrom |
The USDT/BNB conflict is irreconcilable at the `approve()` level. USDT requires `approve(spender, 0)` before `approve(spender, newAmount)`. BNB reverts on `approve(spender, 0)`. A protocol calling `approve(spender, 0)` to handle USDT will revert on BNB. A protocol skipping the zero-reset to handle BNB will revert on USDT when an existing allowance is non-zero.
Three resolution strategies exist, ordered by increasing generality:
1. **SafeERC20 `forceApprove`** (OpenZeppelin v5): tries `approve(spender, amount)`, and if it reverts, falls back to `approve(spender, 0)` then `approve(spender, amount)`. This handles USDT and standard tokens but does not resolve BNB's zero-value revert (BNB returns no value rather than reverting on the initial call, so SafeERC20 accepts it as success).
2. **Token allowlisting**: only support tokens with known, tested approval behavior. Eliminates the combinatorial problem but restricts permissionlessness.
3. **Uniswap Permit2**: moves all token approvals to a single `approve(Permit2, type(uint256).max)` call per token, then uses Permit2's signature-based sub-approval system for all subsequent spending. Since the max approval to Permit2 is a one-time operation, USDT's zero-first requirement is encountered only once. Subsequent granular approvals use Permit2's own logic, bypassing each token's `approve()` entirely.
Since [[ERC-20 approve front-running allows a racing spender to withdraw both the old and new allowance amounts]], USDT's zero-first requirement was itself a mitigation for the approve race condition. The mitigation created a new incompatibility, which Permit2 resolves by moving approval management out of the token contract entirely.
---
Relevant Notes:
- [[USDT zero-first approval requirement breaks protocols that directly update allowances to non-zero values without first resetting to zero]] -- the USDT-specific behavior that creates one side of the incompatibility
- [[zero-value transfer and approval reverts create silent integration failures in protocols that use boundary-amount operations]] -- the BNB-specific behavior that creates the other side
- [[SafeERC20 resolves token return value incompatibility by wrapping calls in low-level assembly that accepts both returning and non-returning tokens]] -- SafeERC20 handles return values and USDT zero-first, but cannot resolve the USDT/BNB conflict without forceApprove
- [[ERC-20 approve front-running allows a racing spender to withdraw both the old and new allowance amounts]] -- the original vulnerability that USDT's zero-first requirement mitigates
- [[non-standard permit implementations that do not revert on signature failure enable silent gasless approval bypass when callers assume permit either succeeds or reverts]] -- extends incompatibility to the permit layer
- [[the majority of deployed ERC-20 token contracts exhibit non-standard behaviors that break DeFi protocol assumptions]] -- approval incompatibility is one of the nine classified non-standard behavior categories
Topics:
- [[vulnerability-patterns]]
- [[protocol-mechanics]]