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.

Move the authorization

With approve, the owner sends the allowance transaction. With permit, the owner signs the fields and any account can submit them.

Approve × permit
approve
1
Owner
Sends transaction
2
Token
owner = msg.sender
3
Storage
allowance = value
permit
1
Owner
Signs typed Permit
2
Any submitter
Sends fields + signature
3
Token
Recovers owner; checks nonce + deadline
4
Storage
allowance = value; nonce += 1
spender: transferFromallowance ↓ · balance ↓

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.

The signed fields

A Permit contains five values:

FieldTypeSource
owneraddressAuthorizing account
spenderaddressAccount receiving allowance
valueuint256New allowance, in base units
nonceuint256Token storage: nonces(owner)
deadlineuint256Last 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.

The state transition

Let n be the token's current nonce for the owner and A its current allowance for the spender. A successful permit performs:

(n,A)⟶(n+1,value)(n, A) \longrightarrow (n + 1, \text{value})

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:

SOLIDITY
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.

Permit → allowance → transfer
Signed authorization
owner
0x1111111111111111111111111111111111111111
spender
0x2222222222222222222222222222222222222222
value
30 units
nonce
0
deadline
60 s
Unsigned · model
Token storage · initial → current
nonce00
allowance00
owner balance100100
Clock · 0 s
time ≤ deadlinenonce = 0
Unsigned
Authorized digest · nonce 00xc2c492ac5da21f1a0c794ef820c78c5a2e9ce7be676ee62d350477535c81621f
Verifier digest · storage nonce 00xc2c492ac5da21f1a0c794ef820c78c5a2e9ce7be676ee62d350477535c81621f
Authorized preimage · 66 bytes0x1901b84fbe916fe855d9beff8203d496f90d4e763e3f31157049324d86b356703d8d5b05e34f996528d1fed5cad5d7cf30184f07aa050a5677fe605bcf9456bda84f

The 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.

Build a permit

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.

JS
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.

Expiry and replay

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.

Similar names

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.

References