← Back to the library
Web3Applied · 8 min

Token approvals fail or leave users exposed

“Approving USDT reverts for users who approved before, a permit-based flow breaks for some wallets, and a drained user turns out to have granted an unlimited allowance to a contract years ago.”

ERROR CODES YOU MAY SEE

A code is a clue. Use its meaning and surrounding evidence to narrow the cause.

CALL_EXCEPTIONethers v6 · library

Execution reverted during a contract call, gas estimation, or transaction processing.

Inspect the action, revert data, and receipt when available. A failed simulation does not mean a transaction was submitted or mined.

Official reference for CALL_EXCEPTION (opens in new tab)Code definition reviewed
4001EIP-1193 provider errors · standard

The user rejected the request.

Treat it as a user decision, not a failure to retry. If it follows a chain switch or signature prompt, check that the prompt showed the expected chain and payload.

Official reference for 4001 (opens in new tab)Code definition reviewed

THE PRINCIPLE

Grant the smallest allowance for the shortest time that completes the action, and treat every standing approval as attack surface.

FIRST MOVES

  1. Read allowance(owner, spender) before approving. For USDT-style tokens that revert on non-zero to non-zero changes, set the allowance to 0 first; in contracts use OpenZeppelin SafeERC20.forceApprove.
  2. Default to exact-amount approvals. If you offer unlimited approval, make it an explicit opt-in and explain that it lasts until revoked.
  3. Prefer EIP-2612 permit where the token supports it, with a short deadline. Wrap permit in try/catch on-chain because anyone can front-run a published permit, and keep an approve path for wallets that cannot sign one.
  4. For tokens without permit, Permit2 adds per-spender amounts and expirations after a one-time approval of the Permit2 contract.
  5. Give users a revoke path: approve(spender, 0) on the token, plus Permit2 approve or lockdown for allowances held inside Permit2.

TOOLS: THEN → NOW

approve(spender, type(uint256).max) by defaultExact approvals, EIP-2612 permits, or Permit2 with expirations; unlimited only as opt-in
SafeERC20.safeApprove (deprecated, absent from OpenZeppelin 5.x)SafeERC20.forceApprove, safeIncreaseAllowance
Users auditing approvals by handRevoke tools such as Revoke.cash or Etherscan's Token Approval Checker

PATTERN SNAPSHOT

// Exact approval that also works for tokens that reject non-zero -> non-zero changes.
async function ensureAllowance(token: Address, spender: Address, needed: bigint) {
  const current = await client.readContract({
    address: token, abi: erc20Abi, functionName: "allowance", args: [account, spender],
  });
  if (current >= needed) return;
  if (current > 0n) await approveAndWait(token, spender, 0n); // USDT-style reset
  await approveAndWait(token, spender, needed);               // not maxUint256
}

// Solidity equivalent: IERC20(token).forceApprove(spender, needed);

CLOSE THE AI. EXPLAIN THIS.

Why can a permit transaction revert even though the user signed a valid permit, and how should the receiving contract handle it?

HOW IT WORKS UNDERNEATH

SOURCES

Guide reviewed