DeFi Security AllianceRequest an audit
Menu

EVM patterns

ERC20 Approval Race: Old Allowances and New Limits

An ERC20 approval race lets a spender use an old allowance before its replacement executes, then use the new grant. Changing a limit needs evidence about prior transfers and the permission that remains. Zero-reset compatibility helpers do not establish that earlier spending never happened.

Two blue allowance gates and a dark observation checkpoint on a transparent ledger track, illustrating token approval ordering.

Key facts

Original measurement
72 symbolic schedules, 24 per workflow, no EVM execution
Blind zero first
Both allowances spent in 4 of 24 modeled schedules
Reviewed zero first
12 replacements stopped, earlier transfers remain
Library scope
OpenZeppelin v5.4.0, with v4.9.6 comparison
Temporary approval
ERC7674 in Review as retrieved November 9, 2026

Read the allowance that the replacement will overwrite

A wallet displays a new spending limit of 30. Before that replacement executes, the approved spender takes the remaining 50. The token then records the new limit, and another transfer takes 30. Cumulative spending is 80, even though the final allowance can be zero. This constructed example follows the overwrite behavior documented by ERC-20.

The ERC20 approval race concerns the order of permission changes and spending. A spender with existing authority can consume it before a replacement becomes effective, then consume the replacement. It needs access to the approved spending route. A stranger who sees a pending approval does not automatically become its authorized spender.

Owner
The account whose token balance is exposed.
Spender
The address allowed to call transferFrom against that owner's balance. For a router integration, verify the actual spender address separately from the token contract.
Allowance
The remaining permission for a particular owner and spender on a particular token contract. Reading a different pair answers a different question.
Approval
approve(spender, value) sets an absolute allowance. It does not itself transfer the approved amount or reserve that amount in the owner's balance.

The standard's zero-first guidance addresses client interfaces. It recommends setting the allowance to zero before setting another value for the same spender, while discouraging a token-level requirement that would break older integrations. That distinction matters when a token accepts a direct replacement but a wallet still chooses a more cautious workflow. Compatibility and the owner's spending decision are separate review questions. The normative basis is the ERC-20 specification.

A confirmed zero does not explain how the allowance became zero. The owner may have revoked it, or the spender may have consumed it. Read the balance and relevant transfers alongside the permission. The existing ERC20 token audit guide covers broader listing evidence; this page follows the narrower permission change.

What changed across 72 ordered scenarios

We compared direct replacement, a blind zero-first workflow and a reviewed zero-first workflow. The last flow refuses the new grant if the owner's balance changed from its initial snapshot. That refusal is a constructed client policy. It is not an extra ERC-20 function.

Method

Evidence date
Primary sources and the model were inspected on .
State
One ordinary symbolic token, one owner and one spender. Initial balance 200, initial allowance 50, replacement values 0, 30 and 80. These are abstract token units.
Complete population
Three workflows multiplied by an optional spending attempt before, between and after the owner's actions, multiplied by three replacement values. All 72 combinations were retained, 24 per workflow, with zero exclusions.
Operations
An enabled spender takes all currently available allowance, bounded by balance. Replacement overwrites permission. Spending subtracts from both finite allowance and balance. In the direct flow, the middle slot follows replacement and the second owner action does nothing.
Review rule
After zeroing, the reviewed flow compares balance with the initial 200. A mismatch cancels the replacement. No unrelated balance changes are modeled.
Reproduction
The repository retains seo/research/erc20-approval-race-2026-11-09/measurement.py and measurement.json. Run the script with --verify for exact replay. These repository paths are not public download links.

Results

Complete symbolic grid. Both allowances means the old allowance plus a positive replacement were fully spent.
WorkflowScenariosBoth allowances spentReplacement stoppedMaximum cumulative spending
Direct overwrite2460130 units
Blind zero first2440130 units
Reviewed zero first2401280 units

The blind flow closes the middle spending slot because its allowance is zero there. It still grants the replacement after earlier spending. In the reviewed flow, all 12 scenarios with spending before the reset stop at the checkpoint. Those scenarios have already lost 50 units. The policy prevents the modeled second grant and cannot recover that first transfer.

Selected complete traces. Before and after attempts are enabled, the middle attempt is disabled.
WorkflowRequested replacementTotal spentFinal balanceFinal allowance
Direct overwrite30801200
Blind zero first30801200
Reviewed zero first30501500
Blind zero first, revoke only0501500

All four traces finish with zero allowance. Their balances establish different outcomes. We independently recalculated every row as old spending plus any permitted replacement spending and checked conservation of the initial balance. No row failed that arithmetic check.

Limits

  • This is a finite source-derived state model. No Ethereum Virtual Machine, compiler, public mempool, deployed token or gas benchmark was executed.
  • The grid deliberately uses exact transfers and finite permissions. Partial spending, fees, rebases and unrelated transfers can change what a balance checkpoint means.
  • Neither USDT behavior nor actual forceApprove execution was tested. Those compatibility details come from the separately captured library source.
  • The counts describe these enumerated inputs. They do not estimate attack probability, losses or the share of wallets using any workflow.

Why zero checks and forceApprove answer different questions

A public OpenZeppelin issue opened on questioned whether a zero-allowance guard solved the ordering problem. The author observed that a spender could consume the entire old allowance before the guarded approval arrived. The check would then see zero and permit another grant. This is a historical implementation discussion, with no incident loss established here. The retained record is OpenZeppelin issue 2219.

The captured v4.9.6 SafeERC20 source marks safeApprove deprecated. Its condition allows a zero requested value or a zero current allowance. That test examines permission at execution. It does not establish that no tokens moved earlier. Do not translate an old recommendation to use a helper into a claim that the helper proves the owner's original balance survived.

In the inspected v5.4.0 source, forceApprove first tries the desired approval directly. If that attempt does not satisfy its success check, it calls approval with zero and retries the desired value. OpenZeppelin documents this for tokens requiring a reset before a new nonzero approval, including USDT. It handles that compatibility requirement inside the calling contract's execution.

A zero-reset fallback does not inspect spending that happened before its transaction. Even when the fallback calls occur together, an older allowance may already have been used. For an ordinary token where the first call succeeds, no zero-reset fallback occurs at all. Check the installed version and actual call path against the OpenZeppelin ERC20 API.

For a wallet deciding whether to use a reset, verify the network and token implementation rather than relying on its symbol. A failed direct-approval simulation can have causes beyond a reset requirement. Preserve the revert information, inspect the current allowance and confirm that the proposed fallback matches the verified token behavior. Treat a token list or cached compatibility rule as an input that needs version and address checks. The same ticker on another network is not evidence of the same approval implementation.

The calling account also matters. SafeERC20 is a Solidity library operating on the calling contract's allowance. A helper contract cannot change an externally owned account's token allowance merely because that account called the helper. Trace who the token sees as its caller. A smart account executing a batch needs the same caller analysis.

Optional return handling solves another boundary. SafeERC20 supports tokens that return no value when a non-reverting call succeeds and rejects failed operations according to its checks. A raw approval caller must handle a returned false. These checks say something about the call outcome; the owner still needs evidence about the resulting permission and prior transfers.

Choose a delta, replacement or revocation

A request to decrease permission by a fixed amount differs from a request to replace permission with a fresh limit. Write that distinction into the transaction requirement before selecting the helper. The spender's behavior before execution can change the current allowance used by a relative update.

Permission changes selected by intent. Verify token and library support before using an optional mechanism.
IntentMechanismEvidence to inspectRemaining boundary
End the current persistent grantApprove zeroConfirmed allowance and transfers before confirmationEarlier spending remains effective
Add a fixed amountSupported increase operationCurrent allowance and requested deltaThe delta authorizes additional spending
Subtract a fixed amountSupported decrease operationExecution-time allowance and failure if too smallA prior spender may make the decrease fail
Grant a new absolute limitReplacement with a client checkpoint where applicableOld spending, zero state and renewed approval decisionThe new grant creates fresh authority

SafeERC20 v5.4.0 reads the calling contract's current allowance for safeIncreaseAllowance and passes current plus delta to forceApprove. Its decrease path rejects a requested decrease greater than the current allowance, then sets current minus delta. With an ordinary non-reentrant token, the read and update belong to the same contract execution. An offchain read followed by a later absolute approval has a different state boundary.

Suppose the existing allowance is 50 and the intended reduction is 20. With no earlier spend, a supported decrease leaves 30. If the spender already consumed the full 50, a decrease by 20 fails against the now-zero allowance. A fresh absolute approval of 30 would instead grant new authority. This source-derived arithmetic example explains the intent difference; it is separate from the enumerated measurement and was not executed against a token.

Token-level increaseAllowance and decreaseAllowance are optional extensions. They appear in the inspected OpenZeppelin v4.9.6 ERC20 implementation but are not methods of the base interface. Discover supported functions from the actual deployed implementation. A wallet should not manufacture an unsupported call because an older tutorial names it.

Signature-based approval also needs this review. ERC-2612 permit sets allowance to a signed value and increments an owner nonce. Its specification explicitly retains the standard approval race. The deadline limits when the signed permit can be submitted; it does not place an expiry on an allowance that a successful permit already created. A still-valid unsubmitted signature is a separate permission path to inventory. The Permit2 revocation guide covers that system's additional permission layers.

ERC-7674, captured in Review status on , specifies a temporary allowance within one transaction, in addition to persistent allowance. It is not a universal token feature. SafeERC20 warns that its increase and decrease helpers can behave unexpectedly when a nonzero temporary allowance is present, and that forceApprove changes only the standard allowance. Include both permission sources when the token supports them.

Verify the checkpoint before granting again

After a reset receipt arrives, a client needs a decision point. Automatically submitting the replacement discards the opportunity to notice prior spending. Choose a reference state that matches the intended account operation, and explain any discrepancy before requesting another signature.

The decision after an allowance reset A confirmed reset leads to a state check. Changed balance stops the replacement. An explained unchanged state can proceed to a renewed grant decision. The check cannot undo earlier transfers. Confirmed resetRead its block state Compare balanceExplain transfers Changed stateStop the replacement State matches the decisionRequest a fresh grant
The reviewed model treats a balance change as a stop. A real client must also explain unrelated movements and token-specific accounting.
  1. Identify the network, token, owner and actual spender. Preserve the intended old allowance and the amount the user meant to authorize.
  2. Submit the reset only through the correct owner context. Check transaction success and read allowance from the state associated with the confirmed receipt.
  3. Compare balances and transfers with the reference decision. If spending occurred or the state cannot be explained, stop the automatic replacement and ask for a new decision.
  4. Refresh the intended operation before granting again. After execution, inspect remaining permission and keep the transaction evidence with that operation.
Evidence fields for the reset checkpoint
RecordQuestion answeredFailure to detect
Receipt status and block identityWhere did the reset execute?Pending, failed or reorganized transaction
Allowance for the exact owner and spenderWhat permission remains?Wrong account or wrong router address
Balance and relevant Transfer recordsDid tokens leave before the checkpoint?Zero permission after spending
New grant and intended operationWhy should authority be created again?Automatic reapproval after changed circumstances

Keep the reset and the state reads associated with a block identity. Reading allowance at one block and balance at another can combine observations that never described one state. If the data provider cannot return a consistent observation, label that checkpoint unresolved and pause automatic reapproval. A later successful read may settle the question, but it should not silently replace the evidence used for the earlier decision.

An Approval event alone is insufficient for this record. In OpenZeppelin ERC20 v5.4.0, allowance consumption by transferFrom does not emit that event by default. Current state and transfer evidence serve different purposes. Follow the Ethereum reorg monitoring procedure if a recorded receipt leaves the canonical chain.

An insufficient-allowance error should lead back to the exact owner-spender pair and requested amount. A balance failure is separate. A generic wrapper failure can require inspecting the underlying token call. Avoid responding to every error by increasing the grant. A successful transaction simulation describes its selected state, while another transaction may consume authority before the real operation executes.

Test remaining permissions and execution paths

An unlimited grant changes the exposure after the current operation. The inspected OpenZeppelin ERC20 implementation leaves a maximum uint256 allowance unchanged when spending. Future deposits can therefore remain exposed to that spender. A small current balance does not bound future permission in the same way as a deliberately limited grant.

Batching reset, approval and an intended use can remove an inter-transaction gap inside that batch. It cannot erase spending before the batch, and caller identity still determines whose tokens are approved. If external token code reenters during an operation, that is an additional call boundary requiring implementation-specific tests. The symbolic grid did not exercise callbacks.

  • Test no prior spending, partial consumption and full consumption before a replacement. Assert cumulative transfers as well as final allowance.
  • Exercise false returns, absent return data and a reset-requiring token. Add a hostile callback fixture when reviewing external-call assumptions.
  • Check finite and maximum allowances separately. Where temporary approvals are supported, test their combination with persistent permission.
  • Include token-specific accounting and failure behavior. Fees, rebasing balances or paused transfers require expectations beyond the simple exact-transfer model.
  • Review the spender's reachable withdrawal routes and any proxy upgrade authority. An allowance grants power to an address whose future implementation may need separate governance review.

Define the acceptance property before testing each token fixture. For a revoke-only operation, require the intended persistent allowance to be zero after successful execution. For a new grant, require the recipient of authority and amount to match the renewed decision. Separately record any earlier transfers. Where a balance changes for legitimate reasons, the model's simple abort rule needs a richer reconciliation policy. Keep those reasons visible rather than weakening the check until a test passes.

A scope request should name those execution paths and the deployed revisions being reviewed. Compare reviewer evidence through the audit analysis hub and a documented provider profile such as Pharos Production. Record the final token balance, permission and transfer history together; a zero allowance by itself leaves the cumulative spending question unanswered.

Frequently asked questions

Does disconnecting a wallet revoke a token allowance?

No. Removing a dApp connection changes the wallet interface relationship. An existing onchain allowance remains until the token state changes through revocation, consumption or another supported mechanism. Reconnecting does not itself create or cancel that allowance.

Does forceApprove always perform two approvals or have one fixed gas cost?

No. The inspected v5.4.0 helper first tries the requested value. Only its fallback sends zero and retries. Actual cost depends on the token and execution path. The symbolic measurement on this page provides no gas benchmark.

How should a wallet convert the approval amount into token units?

Use the verified token contract and network, then convert the intended displayed amount into raw integer units using supported token metadata. ERC20 decimals is optional. Matching symbols across networks or contracts do not establish matching permissions or unit scales.

Comments

0

    Leave a comment

    Share a question or observation about this article.

    10 to 3,000 characters.