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 > validAfterandnow < validBefore. authorizationState(authorizer, nonce)returns whether a nonce has been used, and each use emitsAuthorizationUsed(address indexed authorizer, bytes32 indexed nonce).cancelAuthorizationis listed as optional.
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:
| SignatureTransfer | AllowanceTransfer | |
|---|---|---|
| What the signature grants | One transfer, then it is spent | An allowance with an amount and expiry |
| Nonce | Unordered bitmap (wordPos = nonce >> 8, bitPos = uint8(nonce)) | Ordered uint48, per owner, token and spender |
| Amount type | uint256 | uint160; type(uint160).max is unlimited and never decremented |
| Expiry | deadline (inclusive) | uint48 expiration |
| Extra | permitWitnessTransferFrom binds your own typed data | lockdown() 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-3009 | EIP-2612 | Permit2 (SignatureTransfer) | |
|---|---|---|---|
| Signature authorises | A transfer of value to to | An allowance for spender | A transfer pulled by the named spender |
| Calls to settle | 1 | 2 (permit, transferFrom) | 1, after a one-time approve |
| Nonce model | Random bytes32 | Sequential per owner | Unordered bitmap |
| Concurrent signatures | Yes | No, they settle in order | Yes |
| Who can submit | Anyone, or only to via receiveWithAuthorization | Anyone | Only the signed spender |
| Token must implement it | Yes | Yes | No |
| Leftover allowance | None | Until spent | None |
| Spec status | Draft | Final | Not 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 USDC | Transactions | Median gas used |
|---|---|---|
transfer | 80 | 45,160 |
transferFrom | 51 | 53,931 |
permit | 514 | 76,438 |
transferWithAuthorization (v, r, s) | 127 | 81,041 |
transferWithAuthorization (bytes signature) | 31 | 81,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-3009 | EIP-2612 | EIP-712 name / version |
|---|---|---|---|
| USDC (Ethereum, Base, Arbitrum, OP, Polygon, Avalanche) | Yes | Yes | "USD Coin" / "2" |
| USDC (Unichain, Linea, Monad, Celo and other newer chains) | Yes | Yes | "USDC" / "2" |
| USDC.e (Arbitrum, bridged) | No | Yes | "USD Coin (Arb1)" / "1" |
| USDC.e (Polygon, bridged) | transferWithAuthorization only | Yes | "USD Coin (PoS)" / "1", with salt instead of chainId |
| EURC (Ethereum, Avalanche / Base) | Yes | Yes | "Euro Coin" / "EURC", version "2" |
| USDT (Ethereum) | No | No | None |
| USDT0 (Arbitrum, OP) | Yes | Yes | "USD₮0" / "1" |
| USDT0 (Polygon) | No | Yes | salt-based domain / "1" |
| PYUSD (Ethereum) | Yes | Yes | "PayPal USD" / "1" |
| DAI | No | Non-standard | "Dai Stablecoin" / "1" |
| USDS, RLUSD, USDe | No | Yes | Version "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
x402ExactPermit2Proxyat0x402085c248EeA27D92E8b30b2C58ed07f9E20001. - ERC-7710 delegation was added in March 2026 for smart accounts.
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
receiveWithAuthorizationwhenever 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.
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.