All entries
Chapter VIII
Journal · 20 Oct 2026 · 13 min read

EIP-3009 vs EIP-2612 vs Permit2: Gasless USDC

EIP-3009 vs EIP-2612 vs Permit2 for gasless stablecoin payments: nonce models, token support by chain, USDC domain gotchas, x402 usage and gas costs.

A USDC authorisation signed with the EIP-712 values your code uses on Base Sepolia will fail on Base mainnet. The domain name is "USDC" on the testnet and "USD Coin" on mainnet, so the signature recovers to a different address and the contract reverts. Nothing in the user's wallet shows the difference.

Three standards let someone move stablecoins by signing a message instead of paying gas: EIP-3009, EIP-2612 and Uniswap's Permit2. The pages that rank for them explain one at a time. None puts the three side by side on nonces, token coverage and gas, and none covers the per-chain domain values that break real integrations. This guide does, using the specs, the token source code and on-chain reads I ran on 20 October 2026.

What Is EIP-3009 Transfer With Authorization?

EIP-3009 lets a token holder sign a transfer that anyone else can submit and pay gas for. The signed message is the transfer itself:

TransferWithAuthorization(
  address from, address to, uint256 value,
  uint256 validAfter, uint256 validBefore, bytes32 nonce
)

A relayer calls transferWithAuthorization(from, to, value, validAfter, validBefore, nonce, v, r, s). The token checks the signature, checks the time window and moves the funds in one call. No allowance is created and nothing is left behind for a spender to reuse.

Three details matter in practice:

  • Nonces are random 32-byte values, not a counter. The spec's rationale is that sequential nonces force ordering: if a relayer submits nonce 5 before 4, it fails and wastes gas. With random nonces a payer can have many authorisations outstanding at once, and each settles independently.
  • The time window is strict on both ends. Circle's implementation requires now > validAfter and now < validBefore.
  • authorizationState(authorizer, nonce) returns whether a nonce has been used, and each use emits AuthorizationUsed(address indexed authorizer, bytes32 indexed nonce). cancelAuthorization is listed as optional.
EIP-3009 was created in September 2020 and is still a Draft. It sat as Stagnant until October 2025, when it was moved back to Draft. That revision also added a line saying the standard "does not apply to smart contract accounts". Hold that thought, because USDC disagrees.

How Does EIP-2612 permit Work?

EIP-2612 is older (April 2020) and Final. The holder signs an approval, not a transfer:

Permit(address owner, address spender, uint256 value, uint256 nonce, uint256 deadline)

Someone calls permit(...) to set the allowance, and then the spender calls transferFrom. That is two state changes, usually in one transaction from a contract that wraps both. Nonces are sequential per owner (nonces(owner)), and the deadline is inclusive: the permit is valid while block.timestamp <= deadline.

DAI is the trap here. Its permit predates EIP-2612 and signs Permit(address holder, address spender, uint256 nonce, uint256 expiry, bool allowed), so it grants either unlimited approval or none. The selector is 0x8fcbaf0c instead of the standard 0xd505accf, and its domain version is "1". Code that treats every "permit" token alike will fail on DAI.

What Is Permit2 and Why Did Uniswap Build It?

Permit2 is a contract, not a token standard. It lives at 0x000000000022D473030F116dDEE9F6B43aC78BA3 on every chain Uniswap supports except zkSync. The user approves Permit2 once through the token's normal approve, and from then on signs messages that Permit2 verifies. That works for any ERC-20, including tokens with no permit at all.

It has two modes:

SignatureTransferAllowanceTransfer
What the signature grantsOne transfer, then it is spentAn allowance with an amount and expiry
NonceUnordered bitmap (wordPos = nonce >> 8, bitPos = uint8(nonce))Ordered uint48, per owner, token and spender
Amount typeuint256uint160; type(uint160).max is unlimited and never decremented
Expirydeadline (inclusive)uint48 expiration
ExtrapermitWitnessTransferFrom binds your own typed datalockdown() zeroes approvals in bulk

SignatureTransfer binds the spender to msg.sender, so only the contract named in the signature can execute it. The witness variant lets you sign an order or invoice struct into the same message, which is how payment contracts tie a transfer to a reference. Permit2's EIP-712 domain has name: "Permit2", a chainId and a verifyingContract, and no version field at all.

The catch is that first approve. It is an on-chain transaction, it needs gas, and it is the step gasless flows were meant to remove.

EIP-3009 vs EIP-2612 vs Permit2: Side-by-Side Comparison

EIP-3009EIP-2612Permit2 (SignatureTransfer)
Signature authorisesA transfer of value to toAn allowance for spenderA transfer pulled by the named spender
Calls to settle12 (permit, transferFrom)1, after a one-time approve
Nonce modelRandom bytes32Sequential per ownerUnordered bitmap
Concurrent signaturesYesNo, they settle in orderYes
Who can submitAnyone, or only to via receiveWithAuthorizationAnyoneOnly the signed spender
Token must implement itYesYesNo
Leftover allowanceNoneUntil spentNone
Spec statusDraftFinalNot an EIP; audited by ABDK and ChainSecurity

For payments, the nonce row matters most. A merchant issuing ten invoices to the same wallet wants ten signatures that can settle in any order. EIP-3009 and Permit2 SignatureTransfer allow that. EIP-2612 forces a queue.

How Much Gas Does transferWithAuthorization Use?

Neither Circle nor Uniswap publishes USDC-specific figures, so I took the medians from mainnet receipts of calls sent directly to the USDC contract, in blocks around 26.22 million (October 2026):

Call on Ethereum USDCTransactionsMedian gas used
transfer8045,160
transferFrom5153,931
permit51476,438
transferWithAuthorization (v, r, s)12781,041
transferWithAuthorization (bytes signature)3181,465

EIP-3009 costs about 36,000 gas more than a plain transfer. Most of that is the nonce: every authorisation writes to a fresh storage slot, which costs 20,000 gas by itself, and the signature recovery and extra calldata account for much of the rest. The permit route is worse. At these medians, permit plus transferFrom as separate transactions comes to about 130,000 gas.

Permit2's own Foundry snapshots put permitTransferFrom at 86,092 gas of execution, measured against a mock token and excluding the 21,000 base cost, so the figure is not directly comparable with the table. On L2s none of this decides anything. On Ethereum mainnet it does.

Which Stablecoins Support EIP-3009 and Permit in 2026?

Support is uneven, and it changes by chain for the same brand. I checked each contract below with name(), version(), DOMAIN_SEPARATOR() and selector scans on 20 October 2026.

Token (chain)EIP-3009EIP-2612EIP-712 name / version
USDC (Ethereum, Base, Arbitrum, OP, Polygon, Avalanche)YesYes"USD Coin" / "2"
USDC (Unichain, Linea, Monad, Celo and other newer chains)YesYes"USDC" / "2"
USDC.e (Arbitrum, bridged)NoYes"USD Coin (Arb1)" / "1"
USDC.e (Polygon, bridged)transferWithAuthorization onlyYes"USD Coin (PoS)" / "1", with salt instead of chainId
EURC (Ethereum, Avalanche / Base)YesYes"Euro Coin" / "EURC", version "2"
USDT (Ethereum)NoNoNone
USDT0 (Arbitrum, OP)YesYes"USD₮0" / "1"
USDT0 (Polygon)NoYessalt-based domain / "1"
PYUSD (Ethereum)YesYes"PayPal USD" / "1"
DAINoNon-standard"Dai Stablecoin" / "1"
USDS, RLUSD, USDeNoYesVersion "1"

Native USDC and EURC run Circle's FiatToken v2.2, which has had EIP-2612 and transferWithAuthorization since v2.0 (July 2020) and receiveWithAuthorization since v2.1 (February 2021). Legacy USDT on Ethereum supports neither, and Tether's answer is USDT0, which is not uniform across chains. If you need Ethereum USDT without gas, Permit2 is the only one of the three that works.

Why the USDC EIP-712 Domain Differs by Chain

The table shows the problem. Older USDC deployments sign as "USD Coin", newer ones and most testnets as "USDC", and EURC on Base breaks from EURC on Ethereum. USDT0's name uses the tugrik sign, U+20AE, so a hand-typed "USDT0" produces a different hash. The Polygon bridged tokens replace chainId with a salt, which most EIP-712 libraries do not build by default.

Version getters are unreliable too. x402's documentation tells integrators to read version(), but USDT0, PYUSD, RLUSD, USDe and the bridged tokens on Arbitrum and Polygon have no such function. Circle's v2.2 also recomputes DOMAIN_SEPARATOR on every call from the current chain ID, where v2.0 cached it at initialisation.

For Base mainnet USDC, the domain your signer needs is:

{
  "name": "USD Coin",
  "version": "2",
  "chainId": 8453,
  "verifyingContract": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
}

Build it from the chain, not from a config file someone copied off a testnet. Read name(), try version() and fall back to EIP-5267 eip712Domain() where it exists. Then recompute the separator and compare it with DOMAIN_SEPARATOR() before you ask anyone to sign.

How x402 Uses EIP-3009 and Permit2

x402 is the clearest example of these standards carrying real payments, and it has changed since most write-ups were done. The spec now lives at x402-foundation/x402, with coinbase/x402 as a fork. The exact scheme on EVM chains has three transfer methods:

  • EIP-3009 is the default, for tokens that implement it.
  • Permit2 was added on 8 January 2026 (PR #769) as the fallback for any ERC-20, through an x402ExactPermit2Proxy at 0x402085c248EeA27D92E8b30b2C58ed07f9E20001.
  • ERC-7710 delegation was added in March 2026 for smart accounts.
EIP-2612 appears only as a way to approve Permit2 without gas, via the eip2612GasSponsoring extension. Without that or the erc20ApprovalGasSponsoring extension, the facilitator returns 412 PERMIT2_ALLOWANCE_REQUIRED. The newer upto scheme is Permit2-only. An EIP-3009 authorisation fixes the exact value, so it cannot express "up to".

The reference facilitator is strict: validBefore must be at least six seconds in the future, validAfter must already have passed, the value must equal the requirement, to must equal payTo, and the transfer is simulated before settlement. I covered where x402 sits next to AP2 and ACP in the agentic payment protocols comparison.

Circle's own products split the same way. Circle Paymaster takes gas in USDC through an EIP-2612 permit with a maxUint256 deadline, because ERC-4337 validation rules stop a paymaster reading the block timestamp. USDC v2.2 skips the deadline check entirely for that value. Circle Gateway's nanopayments sign a struct shaped like TransferWithAuthorization, but against a "GatewayWalletBatched" domain with the Gateway wallet as verifying contract, and they require validBefore at least three days out. The base/commerce-payments contracts, successor to Coinbase Commerce's onchain protocol, ship collectors for EIP-3009, Permit2 and two approval types of their own.

receiveWithAuthorization vs transferWithAuthorization: Front-Running

transferWithAuthorization can be submitted by anyone. That is fine when a relayer pays a person. It breaks when the payee is a contract.

Picture a deposit contract that takes a user's signature, calls transferWithAuthorization to pull the USDC in, and then credits the user's balance. An attacker watching the mempool copies the signature and submits it directly to USDC first. The funds arrive at the contract, but the crediting code never runs, and the contract's own transaction reverts on the spent nonce. The EIP's security section describes the result as "unprocessed, locked up deposits".

receiveWithAuthorization takes the same arguments and adds one rule: msg.sender must equal to. Only the payee contract can execute it. If your contract is the recipient, use it.

EIP-2612 has the same weakness in a different shape. The spec admits anyone can front-run a permit. If your contract calls permit and then transferFrom unconditionally, an attacker who submits the permit first makes your call revert, even though the allowance now exists. The standard fix, documented in OpenZeppelin's IERC20Permit, is to wrap permit in try/catch and then check the allowance. x402's Permit2 proxy does exactly that. Permit2 SignatureTransfer avoids the problem by design, because only the signed spender can call it.

Permit Phishing: Why Gasless Signatures Drain Wallets

A signature that moves money without a transaction also lets a phishing site take money without one. Scam Sniffer's 2024 report counted $494 million stolen from 332,000 victims, and Permit signatures made up 56.7% of losses in thefts above $1 million. Its 2025 report found losses down 83% to $83.85 million, with Permit and Permit2 together at 38% of large-case losses, and EIP-7702 batch signatures appearing in thefts for the first time in its data ($2.54 million across two cases).

One ranking guide says a phished EIP-3009 signature can only pay the named recipient. That is true and misleading, because the phisher writes the to field. Each standard fails differently. An EIP-3009 signature is the transfer itself, capped at one amount. A Permit2 AllowanceTransfer at type(uint160).max with a distant expiry is a standing drain on that token. For a payment flow, ask for the narrowest signature: one transfer, exact amount, a short window.

Smart Wallets and EIP-7702: ERC-1271 in USDC v2.2

USDC v2.2, released in November 2023, added bytes signature overloads to permit, transferWithAuthorization, receiveWithAuthorization and cancelAuthorization, so contract wallets can sign through ERC-1271. That contradicts the EIP-3009 text quoted above, and the implementation is what counts.

The detail that bites is how the check branches. If the signer address has code, USDC uses isValidSignature only. An EOA delegated under EIP-7702 has code. Its plain ECDSA signature therefore works only if the delegate contract's isValidSignature accepts the EOA's own key. x402's facilitator removed its ECDSA fallback for addresses with code after USDC rejected such signatures on Base Sepolia. Permit2 branches the same way.

Two compliance details also catch payment teams. EIP-3009 transfers check that from and to are not blacklisted, but not the relayer. All of these functions revert while the token is paused.

Which Gasless Transfer Standard Should You Use?

My position, for payment flows rather than DeFi:

  • USDC, EURC or PYUSD to a known payee: EIP-3009. One call, no leftover allowance, concurrent invoices. Use receiveWithAuthorization whenever the payee is a contract.
  • Any token, including Ethereum USDT: Permit2 SignatureTransfer, with the witness carrying your order or invoice hash. Accept the one-time approval, or sponsor it.
  • Gas paid in the token itself: EIP-2612, which is what Circle Paymaster uses.
  • Avoid AllowanceTransfer with unlimited amounts for anything a customer signs at checkout.
The prediction: EIP-3009 will still be a Draft at the end of 2027, and it will not matter. x402, Circle Gateway and the Base commerce contracts already treat its struct as the standard payment message, and the EIP process has nothing left to add. Legacy USDT on Ethereum will never gain either function. Permit2 will remain the route for every token whose issuer does not upgrade.

What This Means for Payment Engineers

1. Never hardcode the EIP-712 domain. Read it from the contract at startup, recompute the separator, compare it with DOMAIN_SEPARATOR(), and refuse to sign if they differ. Test against mainnet values, because testnet names differ. 2. Use the nonce as your payment reference. An ERC-20 transfer has no memo field, as the USDC payment matching guide explains. An EIP-3009 nonce is 32 bytes you choose, and it is indexed in AuthorizationUsed. Derive it as keccak256(secret, invoiceId) and you get on-chain attribution for every payment that goes through your flow. 3. Make settlement idempotent on (token, authorizer, nonce). authorizationState tells you whether a retry already landed. The idempotency keys guide covers the storage side. 4. Keep validity windows short. Minutes, not days, for checkout. x402 rejects anything with less than six seconds left, so leave headroom for relayer latency. 5. Wrap every permit call in try/catch and check the allowance afterwards. 6. Test with an EIP-7702 wallet before launch. A signature that verifies off-chain with ecrecover can still fail on-chain. 7. Map revert reasons to customer messages. Paused token, blacklisted address, expired window and used nonce are different failures and need different answers.

I'm Tom Wang. Most of the gasless-payment bugs I have seen were not in the cryptography. They were in configuration: a domain name copied from a testnet, a version read from a getter that does not exist, a permit called without a fallback. All three standards work. Read the domain from the chain before anyone signs.