EIP-712, ERC-2612, and Permit2: Token Approvals Explained
Every ERC-20 interaction starts with the same annoying two-step: approve, then transact. You want to swap USDC on Uniswap? First, sign an approval transaction. Wait for confirmation. Then sign the actual swap. Two transactions, two gas fees, two wallet popups.
Three different standards have tried to fix this problem. They're often confused with each other, and the naming doesn't help. Here's what each one actually does.
Table of Contents
- EIP-712: The Signature Format
- ERC-2612: Permit Built Into the Token
- Permit2: Universal Approvals
- Security: The Phishing Problem
- Comparison Table
- When to Use What
EIP-712: The Signature Format
EIP-712 is not about approvals at all. It's a standard for how you sign structured data off-chain.
Before EIP-712, if a dApp asked you to sign something, your wallet showed you a hex blob like 0x4e2f1a.... You had no idea what you were agreeing to. You could be signing a token swap, a governance vote, or an approval to drain your wallet.
EIP-712 fixes this by defining a format that wallets can parse and display in human-readable form:
"You are signing:"
Action: Swap
Token: USDC
Amount: 1,000
Recipient: 0xUniswap...
Chain: Ethereum Mainnet
How It Works
Every EIP-712 signature has two parts: a domain separator and the typed data.
The domain separator identifies which contract and chain the signature is for:
bytes32 DOMAIN_SEPARATOR = keccak256(abi.encode(
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
keccak256("MyProtocol"), // name
keccak256("1"), // version
block.chainid, // chainId
address(this) // verifyingContract
));
The five possible domain fields (you can skip any that don't apply):
| Field | Type | Purpose |
|---|---|---|
name | string | Name of the dApp or protocol |
version | string | Current major version |
chainId | uint256 | EIP-155 chain ID (prevents cross-chain replay) |
verifyingContract | address | Contract that will verify the signature |
salt | bytes32 | Disambiguating salt (last resort) |
The typed data defines the structure of what's being signed:
// Define the type
bytes32 PERMIT_TYPEHASH = keccak256(
"Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"
);
// Hash an instance
bytes32 structHash = keccak256(abi.encode(
PERMIT_TYPEHASH,
owner,
spender,
value,
nonce,
deadline
));
// Combine with domain separator
bytes32 digest = keccak256(abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR,
structHash
));
// Recover signer
address signer = ecrecover(digest, v, r, s);
The \x19\x01 prefix is part of the spec. It prevents the signed data from being valid as a regular Ethereum transaction.
EIP-712 Is a Building Block
EIP-712 doesn't do anything with tokens. It's the foundation that ERC-2612 and Permit2 both build on. Whenever you see a dApp ask you to "sign a message" with structured data in your wallet, that's EIP-712.
Other protocols use EIP-712 for completely different purposes. Lending protocols use it for signed intents. Governance systems use it for gasless votes. NFT marketplaces use it for off-chain listings. The signing format is the same; the typed data changes.
Status: Final. Created September 2017. Source: EIP-712 specification.
ERC-2612: Permit Built Into the Token
ERC-2612 adds a permit() function directly to the ERC-20 token contract. Instead of calling approve() in a separate transaction, you sign an EIP-712 message off-chain and pass the signature to the contract that needs the approval.
The Standard Two-Step (Without Permit)
Transaction 1: USDC.approve(uniswapRouter, 1000e6) // costs gas
Transaction 2: uniswapRouter.swap(USDC, WETH, 1000e6) // costs gas
Two transactions. Two gas fees. The user waits for the first to confirm before sending the second.
With ERC-2612 Permit
Off-chain: User signs a permit message // free
Transaction: router.permitAndSwap(permit, swap) // one gas fee
One transaction. The contract calls USDC.permit(owner, spender, value, deadline, v, r, s) internally, which sets the allowance, then proceeds with the swap.
The Interface
function permit(
address owner, // who is granting the approval
address spender, // who can spend the tokens
uint256 value, // how many tokens
uint256 deadline, // signature expiry timestamp
uint8 v, bytes32 r, bytes32 s // ECDSA signature components
) external;
function nonces(address owner) external view returns (uint256);
function DOMAIN_SEPARATOR() external view returns (bytes32);
The token contract verifies the EIP-712 signature, checks the nonce (prevents replay), and sets the approval. The nonce increments by 1 after each successful permit() call.
The Catch: Token Must Support It
ERC-2612 is baked into the token contract at deployment. If the token was deployed without permit(), you can't add it later (unless it's a proxy-based upgradeable token, which most aren't).
Tokens that support ERC-2612 permit: USDC, most modern ERC-20 tokens, Uniswap V2 LP tokens.
Tokens that do NOT support it: USDT on mainnet (deployed before the standard existed), WETH, and many older tokens.
This is the fundamental limitation. You can only use permit with tokens that were deployed with it.
The DAI Gotcha
DAI has a permit() function, but it's not ERC-2612 compliant. DAI was deployed before the standard was finalized, so its implementation differs:
| ERC-2612 | DAI | |
|---|---|---|
| Allowance parameter | uint256 value (exact amount) | bool allowed (0 or max uint256) |
| Deadline parameter | uint256 deadline | uint256 expiry |
| Behavior | Sets allowance to value | Sets allowance to 0 or type(uint256).max |
These aren't just naming differences. The signed message structure is different, which means the signatures are incompatible. Any protocol integrating permit() needs separate code paths for DAI.
Source: wagmi-permit library (handles both DAI and ERC-2612).
Nonce Limitation
ERC-2612 uses sequential nonces per address. Nonce 0 must be used before nonce 1, which must be used before nonce 2. If you sign two permits and the second one lands on-chain first, it fails because the nonce is wrong.
This matters in high-gas environments where transactions sit in the mempool for a while, or when users interact with multiple dApps simultaneously.
Status: Final. Source: ERC-2612 specification.
Permit2: Universal Approvals
Permit2 is a standalone contract deployed by Uniswap Labs in November 2022. It solves the problem ERC-2612 can't: gasless approvals for tokens that don't have native permit support.
The contract lives at 0x000000000022D473030F116dDEE9F6B43aC78BA3 on every chain (Ethereum, Arbitrum, Optimism, Polygon, Base, and others). Same address everywhere.
Source: Permit2 on Etherscan.
How It Works
The idea: you do a one-time approve() to the Permit2 contract. After that, every subsequent approval to specific spenders happens via off-chain signatures through Permit2.
Setup (once per token):
USDT.approve(permit2Contract, type(uint256).max) // one-time gas cost
Every future interaction:
Off-chain: sign a Permit2 message for spender X // free
On-chain: spender calls permit2.permitTransferFrom() // one transaction
You approve Permit2 once. Then Permit2 manages all your future approvals via signatures. No more separate approve transactions for each new dApp.
Two Modes
Permit2 has two distinct systems:
SignatureTransfer: One-time permits. You sign a message authorizing a specific transfer. The signature is consumed in that transaction and can't be reused. There's no persistent allowance stored anywhere. This is the most secure mode because permissions exist only for the duration of one transaction.
// The spender submits this
permit2.permitTransferFrom(
permit, // signed permit details (token, amount, nonce, deadline)
transferDetails, // where to send the tokens
owner, // who signed the permit
signature // the EIP-712 signature
);
AllowanceTransfer: Time-limited and amount-limited allowances, stored in the Permit2 contract (not in the token contract). Closer to traditional approve, but with built-in expiration and more granular control.
// The owner signs a permit, then spender submits it
permit2.permit(owner, permitSingle, signature);
// Later, within the allowance window
permit2.transferFrom(owner, receiver, amount, token);
The key difference: AllowanceTransfer stores an allowance that persists across transactions (until it expires or is used up). SignatureTransfer stores nothing; each transfer needs a fresh signature.
Nonce Model
Permit2's SignatureTransfer uses a nonce bitmap instead of sequential nonces. Each nonce is a bit in a 256-bit word, indexed by a nonce parameter. This means you can use nonces in any order. Nonce 5 can land before nonce 3. This fixes the sequential nonce problem from ERC-2612.
AllowanceTransfer uses sequential nonces per (owner, token, spender) tuple, similar to ERC-2612.
Who Uses Permit2
- Uniswap's Universal Router uses Permit2 for all token transfers
- CoW Protocol/CoWSwap uses it for signed swap orders
- 0x Protocol uses it to eliminate persistent allowance risk
- 1inch supports Permit2 signatures
Source: Uniswap Permit2 announcement.
Audits
Permit2 has been audited by multiple firms including OpenZeppelin, Trail of Bits, and ChainSecurity.
Source: Uniswap Permit2 docs.
Security: The Phishing Problem
All three signature-based approval methods share the same risk: phishing.
When a user signs an EIP-712 message, they're authorizing an action. If a malicious site tricks them into signing a Permit2 message that approves a drainer contract, the attacker can take their tokens. Unlike a regular approve() transaction (which costs gas and shows a clear "you are spending X" in the wallet), signatures feel "free" and users are less cautious.
In the first half of 2024, 260,000 victims lost a combined $314 million to phishing attacks involving permit and Permit2 signatures on EVM chains.
Source: Gate.io phishing analysis.
Notable incident: a holder lost $1.4 million worth of PEPE tokens through a Uniswap Permit2 phishing attack.
Source: Decrypt coverage.
The mitigation is wallet UX. MetaMask and other wallets have been improving how they display permit signatures, making it clearer what the user is actually approving. But the risk remains as long as users sign things without reading them.
Comparison Table
| EIP-712 | ERC-2612 (Permit) | Permit2 | |
|---|---|---|---|
| What it is | Signature format standard | Token-level permit function | Universal approval contract |
| Layer | Foundation (signing) | Token contract | Standalone contract |
| Scope | Any structured data | Only tokens deployed with it | Any ERC-20 token |
| Built by | Ethereum community | Ethereum community | Uniswap Labs |
| On-chain state | None (just a format) | Nonce per owner in token | Allowances + nonces in Permit2 |
| Nonce model | N/A | Sequential per owner | Bitmap (SignatureTransfer) or sequential (AllowanceTransfer) |
| Gas for setup | None | None (built into token) | One-time approve to Permit2 |
| Gas per use | None (off-chain only) | Bundled into the consuming tx | Bundled into the consuming tx |
| Works with USDT | Yes (as a format) | No (USDT has no permit) | Yes (after one-time approve) |
| License | EIP standard | EIP standard | MIT (Uniswap) |
| Deployed address | N/A | Inside each token | 0x000...22D473...78BA3 (all chains) |
When to Use What
Building a protocol that accepts off-chain signed messages (intents, orders, votes): You need EIP-712 for the signature format. This is the lowest level. Every off-chain signing scheme uses it.
Building a protocol that needs token approvals from users: Support ERC-2612 permit() for tokens that have it. This covers USDC and most modern tokens. For tokens without native permit (USDT, WETH, older tokens), integrate Permit2 as a fallback. Most production protocols support both paths.
Building a wallet or frontend: Display EIP-712 signatures clearly. Show the user exactly what they're approving, which token, how much, which contract, and what deadline. This is the primary defense against phishing.
The three standards aren't competing. They're layers. EIP-712 defines how to sign. ERC-2612 uses that signing for built-in token approvals. Permit2 uses that same signing for universal token approvals. Each solves a different piece of the approval problem.