Skip to main content

EIP-2612: Gasless approvals

The EIP-2612 permit function lets a token holder sign an approval off-chain so that any account can submit it on-chain. The holder pays no gas, and you can bundle the approval with the action that consumes it — for example, approve-and-swap or approve-and-bridge — in a single transaction.

Every EVM token contract except Hedera supports permit. Polygon PoS and Tempo support only the classic v, r, s variant, and Polygon PoS uses a different signing domain. For the per-chain matrix, see Availability.

Interface

Most chains expose two variants — the classic EIP-2612 signature split into v, r, s, and a bytes variant that accepts a packed externally owned account (EOA) signature or an EIP-1271 contract wallet signature. Polygon PoS and Tempo expose only the classic v, r, s variant:

// Classic EIP-2612
function permit(
address owner, address spender, uint256 value, uint256 deadline,
uint8 v, bytes32 r, bytes32 s
) external;

// Bytes variant: packed EOA signature (r, s, v) or EIP-1271 contract signature
function permit(
address owner, address spender, uint256 value, uint256 deadline,
bytes memory signature
) external;

function nonces(address owner) external view returns (uint256);
function DOMAIN_SEPARATOR() external view returns (bytes32);

Signed message

The signed message is the standard Permit struct:

Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)
  • nonce is sequential per owner — read the current value from nonces(owner).
  • deadline is a Unix timestamp; the signature is valid while block.timestamp <= deadline. For a permit that never expires, use MaxUint256.

Sign the struct as EIP-712 typed data over the token's signing domain. Polygon PoS uses a salt-based domain with no chainId, so a permit signed with the standard domain fails there.

Example: Sign and relay a permit

The following example signs a permit with the token holder's key and submits it from a relayer account that pays the gas:

import { ethers } from 'ethers';

const TOKEN_ABI = [
'function name() view returns (string)',
'function nonces(address) view returns (uint256)',
'function permit(address,address,uint256,uint256,uint8,bytes32,bytes32)',
];

async function signAndSubmitPermit(
owner: ethers.Wallet, // token holder: signs, pays no gas
relayer: ethers.Signer, // any account: submits, pays gas
tokenAddress: string,
spender: string,
value: ethers.BigNumber
) {
const token = new ethers.Contract(tokenAddress, TOKEN_ABI, relayer);
const { chainId } = await relayer.provider!.getNetwork();

const domain = {
name: await token.name(), // on-chain name, such as 'USD₮0' — never hardcode
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 deadline = Math.floor(Date.now() / 1000) + 3600; // 1 hour
const message = {
owner: owner.address,
spender,
value,
nonce: await token.nonces(owner.address),
deadline,
};

const sig = ethers.utils.splitSignature(await owner._signTypedData(domain, types, message));

// Submitted by the relayer — the owner never sends a transaction
return token.permit(owner.address, spender, value, deadline, sig.v, sig.r, sig.s);
}