An ERC-20 allowance normally starts with a transaction from the token owner. approve(spender, value) identifies that owner through msg.sender.
EIP-2612 adds another path. The owner signs an approval off chain. Someone submits it to the token contract, which recovers the owner and writes the allowance. The signature separates permission from transaction submission.
With approve, the owner sends the allowance transaction. With permit, the owner signs the fields and any account can submit them.
The submitter pays gas for permit. The spender may later pay gas for a transfer, or a helper contract may combine permit and consumption in one transaction. Signing itself neither writes allowance nor moves tokens.
The owner may also be the submitter. EIP-2612 does not require a relayer service.
A Permit contains five values:
| Field | Type | Source |
|---|---|---|
owner | address | Authorizing account |
spender | address | Account receiving allowance |
value | uint256 | New allowance, in base units |
nonce | uint256 | Token storage: nonces(owner) |
deadline | uint256 | Last valid submission timestamp |
The canonical declaration is Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline).
owner supplies the signature. spender receives permission. value is the new allowance in the token's smallest units. nonce must equal the owner's current token nonce. deadline is the last Unix timestamp at which the permit may be accepted.
The token and chain belong in the EIP-712 domain. The message hash and domain separator then enter the EIP-712
envelope.
PERMIT_TYPEHASH hashes that canonical declaration. It is not the selector of the Solidity permit function. It binds the field schema used to construct the message digest.
Let n be the token's current nonce for the owner and A its current allowance for the spender. A successful permit performs:
The token checks that the deadline has not passed, the owner is nonzero, and the signature matches the digest built with its current nonce and expected domain. If a check fails, the transaction reverts.
The ABI does not take a nonce argument:
function permit(
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) external;The contract reads the nonce from its own storage. Submitting the same signature again constructs a different digest after the first success, so recovery no longer matches the owner. The signature's nonce is implicit in that check.
A successful permit replaces allowance with value; it does not add value to the previous allowance. Balance is unchanged. Token movement happens through a subsequent operation such as transferFrom.
00001001000x1901b84fbe916fe855d9beff8203d496f90d4e763e3f31157049324d86b356703d8dThe lab models a token starting with nonce 0, allowance 0, and owner balance 100. Its simulated clock starts at Unix time 0 and its deadline is 60. The fixed authorization grants 30 units. Purple tracks token state; cyan tracks the authorization. Signing changes no storage; accepting the permit advances the nonce, so the verifier's next digest differs from the original one.
The digest calculations use the illustrated EIP-712 domain and Permit fields. The buttons simulate signing and contract state; they do not create an ECDSA signature, connect a wallet, or submit a transaction. Signature validity is assumed while nonce, deadline, allowance, and balance transitions are modeled.
This ethers 6.17.0 example signs and verifies locally. It assumes a token whose domain is Example Token, version 1, on chain 1, at the illustrative address below. There is no deployed token behind the example.
import assert from 'node:assert/strict';
import { Wallet, Signature, verifyTypedData } from 'ethers';
const owner = Wallet.createRandom();
const domain = {
name: 'Example Token',
version: '1',
chainId: 1,
verifyingContract: '0x3333333333333333333333333333333333333333',
};
const types = {
Permit: [
{ name: 'owner', type: 'address' },
{ name: 'spender', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' },
],
};
const permit = {
owner: owner.address,
spender: '0x2222222222222222222222222222222222222222',
value: 30n,
nonce: 0n,
deadline: BigInt(Math.floor(Date.now() / 1000) + 300),
};
const signature = await owner.signTypedData(domain, types, permit);
assert.equal(verifyTypedData(domain, types, permit, signature), owner.address);
assert.notEqual(
verifyTypedData(domain, types, { ...permit, nonce: 1n }, signature),
owner.address,
);
const { v, r, s } = Signature.from(signature);
console.log({ v, r, s });For a real token, read nonces(owner) from the token and obtain its actual domain fields; do not assume every token uses version 1 or supports this interface. The submitter calls permit(owner, spender, value, deadline, v, r, s) on that same token.
For contract implementation, use a reviewed library such as OpenZeppelin's ERC20Permit. Correct ECDSA handling, nonce use, and domain construction should not depend on an abbreviated tutorial verifier.
The deadline limits when the permit can be submitted. It does not expire the allowance created by a successful submission. In the lab, submit first, pass the deadline, then spend: the existing allowance remains usable.
Likewise, approving zero does not consume an outstanding permit nonce. A previously signed, unexpired permit may still restore an allowance if its nonce remains current. Revoking allowance and invalidating unused signatures are different operations.
Any account can submit a valid permit. An observer may submit it before the intended relayer. The resulting allowance is the same, but a later transaction that unconditionally calls permit can fail because the nonce has already advanced. Integrations must handle this case without weakening their checks on owner, spender, amount, and the intended subsequent action.
Permit authorizes a spender. It contains no swap price or recipient constraint. Those terms belong to the consuming protocol, as in an RFQ
order.
DAI's older permit uses holder, allowed, and expiry, with a different signed type and ABI. allowed selects zero or unlimited allowance. An ERC-2612 signature cannot be substituted for a DAI permit signature.
Permit2 is another protocol with its own contracts and authorization paths. Its name does not make it interchangeable with a token's ERC-2612 implementation.
Basic ERC-2612 verification is specified around a secp256k1 owner signature. Support for contract-wallet signatures requires an explicit implementation or another authorization path; it does not follow automatically from using EIP-712.