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)
nonceis sequential per owner — read the current value fromnonces(owner).deadlineis a Unix timestamp; the signature is valid whileblock.timestamp <= deadline. For a permit that never expires, useMaxUint256.
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);
}