EIP-2612 Permit
EIP-2612 permit: gasless approvals for ERC-20 tokens via signed messages. The pattern that powers gasless dApps and one-click swaps.
Web3 — ERC-20 permit
EXAMPLE
// ===== The problem =====
// Standard ERC-20 transferFrom requires the spender to be approved.
// approve() costs gas; users hate paying gas just to grant a future operation.
// ===== EIP-2612 =====
// Adds permit(owner, spender, value, deadline, v, r, s) to tokens.
// Users sign an EIP-712 typed message OFF-CHAIN.
// The dApp or relayer submits the permit + the actual transfer in one transaction.
// Net effect: user signs once, performs one tx (instead of approve + use).
// ===== Token side (Solidity) =====
// Inheriting from OpenZeppelin's ERC20Permit gives you permit():
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
contract MyToken is ERC20Permit {
constructor() ERC20("MyToken", "MTK") ERC20Permit("MyToken") {}
}
// ===== Client side (viem) =====
import { createWalletClient, custom, parseUnits } from 'viem';
const wallet = createWalletClient({ chain, transport: custom(window.ethereum) });
const [owner] = await wallet.requestAddresses();
const value = parseUnits('100', 18);
const deadline = BigInt(Math.floor(Date.now() / 1000) + 3600); // 1h
// Read nonce + name + version + chainId for the typed data domain:
const nonce = await client.readContract({ address: tokenAddress, abi: TokenABI, functionName: 'nonces', args: [owner] });
const name = await client.readContract({ address: tokenAddress, abi: TokenABI, functionName: 'name' });
const domain = {
name,
version: '1',
chainId,
verifyingContract: tokenAddress,
};
const types = {
Permit: [
{ name: 'owner', type: 'address' },
{ name: 'spender', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' },
],
};
const message = { owner, spender: routerAddress, value, nonce, deadline };
const signature = await wallet.signTypedData({ account: owner, domain, types, primaryType: 'Permit', message });
const { v, r, s } = parseSignature(signature);
// Then call a contract that uses permit + does the action:
await wallet.writeContract({
address: routerAddress,
abi: RouterABI,
functionName: 'swapWithPermit',
args: [tokenAddress, value, deadline, v, r, s, ...swapArgs],
account: owner,
});
// ===== Router-side contract (Solidity) =====
function swapWithPermit(
IERC20Permit token,
uint256 amount,
uint256 deadline,
uint8 v, bytes32 r, bytes32 s,
// ... other swap params
) external {
token.permit(msg.sender, address(this), amount, deadline, v, r, s);
token.transferFrom(msg.sender, address(this), amount);
// ... perform swap
}
// ===== EIP-2612 vs DAI permit (legacy) =====
// DAI uses a slightly different permit signature (allowed flag instead of value).
// Check the token contract's permit ABI before signing.
// ===== Permit2 (Uniswap) =====
// A separate contract that exposes a generalised permit interface.
// Lets you grant ALLOWANCES via signatures to ANY token, even those without EIP-2612.
// Becoming the standard for new dApps.
// ===== Common bugs =====
// - Signing the wrong domain (chainId / version) -> permit fails
// - Re-using a nonce -> permit fails
// - Deadline in the past -> permit fails
// - Front-running the permit (someone submits it first) -> not a bug, but mind the UX
// ===== Patterns to internalise =====
// - OpenZeppelin ERC20Permit for any new ERC-20
// - Cache nonce + name + version when reading typed data
// - Long deadlines (15-60 min) for UX; short for security-critical flows
// - Permit2 for tokens that don't ship EIP-2612
// ===== Pitfalls =====
// - Permitting MAX_UINT256 (unlimited approval) -> users approve for life
// - Frontends that sign permits but don't verify the spender address
// - Permit + transferFrom in separate txs (defeats the purpose)
// - Tokens with non-standard permits (DAI) and assuming EIP-2612
Why it matters
EIP-2612 permit removes the approve transaction by signing a typed message instead. OpenZeppelin ERC20Permit on the token side, signTypedData + a router that calls permit + transferFrom on the client. Permit2 extends this to ANY token. The wins are gasless approvals and smoother UX; the discipline is signing the right domain and limiting deadlines + amounts.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
// EIP-2612: gasless ERC-20 approvals via signatures. // Cuts a step out of UX: approve → swap becomes permit + swap in one tx.Try it Yourself »
Discussion
Loading…