Skip to main content

Permit

EIP-2612 Permit signatures enable gasless token approvals through meta-transactions. Users sign an off-chain message granting approval, which can be submitted by a relayer paying the gas.

Overview

EIP-2612 extends ERC-20 tokens with a permit function that accepts EIP-712 signed messages. This enables:
  • Gasless approvals: Users don’t need ETH to approve tokens
  • Single-transaction flows: Approve + transfer in one transaction
  • Meta-transactions: Relayers can pay gas on behalf of users
  • Better UX: No separate approval transaction needed

Types

PermitType

interface PermitType {
  readonly owner: AddressType;
  readonly spender: AddressType;
  readonly value: Uint256Type;
  readonly nonce: Uint256Type;
  readonly deadline: Uint256Type;
}

PermitDomainType

interface PermitDomainType {
  readonly name: string;
  readonly version: string;
  readonly chainId: ChainIdType;
  readonly verifyingContract: AddressType;
}

Methods

createPermitSignature

Creates an EIP-712 signature for a permit message.
import * as Permit from '@tevm/primitives/Permit';
import * as Address from '@tevm/primitives/Address';
import * as Uint256 from '@tevm/primitives/Uint256';

const permit: Permit.PermitType = {
  owner: Address.fromHex('0x...'),
  spender: Address.fromHex('0x...'),
  value: Uint256.fromBigInt(1000000n),
  nonce: Uint256.fromBigInt(0n),
  deadline: Uint256.fromBigInt(BigInt(Math.floor(Date.now() / 1000) + 3600)),
};

const domain: Permit.PermitDomainType = {
  name: 'USD Coin',
  version: '2',
  chainId: 1,
  verifyingContract: Address.fromHex('0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'),
};

const signature = Permit.createPermitSignature(permit, domain, privateKey);

verifyPermit

Verifies a permit signature matches the permit data and domain.
const isValid = Permit.verifyPermit(permit, signature, domain);

Known Tokens

Pre-configured domain data for popular ERC-2612 tokens:
import * as Permit from '@tevm/primitives/Permit';

// USDC Mainnet
const domain = Permit.KnownTokens.USDC_MAINNET;

// DAI Mainnet
const domain = Permit.KnownTokens.DAI_MAINNET;

// Other tokens
Permit.KnownTokens.USDC_POLYGON
Permit.KnownTokens.USDC_ARBITRUM
Permit.KnownTokens.USDT_MAINNET
Permit.KnownTokens.UNI_MAINNET
Permit.KnownTokens.WETH_MAINNET

Complete Example

import * as Permit from '@tevm/primitives/Permit';
import * as Address from '@tevm/primitives/Address';
import * as Uint256 from '@tevm/primitives/Uint256';

// Setup
const owner = Address.fromHex('0x...');
const spender = Address.fromHex('0x...');
const amount = Uint256.fromBigInt(1000000n); // 1 USDC

// Create permit
const permit: Permit.PermitType = {
  owner,
  spender,
  value: amount,
  nonce: Uint256.fromBigInt(0n),
  deadline: Uint256.fromBigInt(BigInt(Math.floor(Date.now() / 1000) + 3600)),
};

// Sign with known token domain
const domain = Permit.KnownTokens.USDC_MAINNET;
const signature = Permit.createPermitSignature(permit, domain, privateKey);

// Verify
const isValid = Permit.verifyPermit(permit, signature, domain);

// Submit to contract
const { r, s, v } = parseSignature(signature);
await contract.permit(
  permit.owner,
  permit.spender,
  permit.value,
  permit.deadline,
  v,
  r,
  s
);

Security Considerations

Deadline

Always set a reasonable deadline. Expired permits can’t be used:
// 1 hour from now
const deadline = Uint256.fromBigInt(BigInt(Math.floor(Date.now() / 1000) + 3600));

Nonce Management

Each permit must use the owner’s current nonce. Replay protection:
// Query on-chain nonce
const nonce = await contract.nonces(owner);

Domain Verification

Ensure the domain matches the token contract:
// Verify domain separator
const onChainSeparator = await contract.DOMAIN_SEPARATOR();

Amount Limits

Be careful with unlimited approvals:
// Limited approval
const amount = Uint256.fromBigInt(1000000n);

// Unlimited (use with caution)
const unlimited = Uint256.MAX;

References