iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

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…