ERCs

ERC-2612 — Permit (gasless approvals)

eips.ethereum.org/EIPS/eip-2612

ERC-20 approvals normally require a transaction. ERC-2612 adds permit(owner, spender, value, deadline, v, r, s) — an EIP-712 signature the owner produces off-chain, then any party submits as a transaction. Gas is paid by whoever submits, not by the owner.

import { permit, nonces, DOMAIN_SEPARATOR } from "@ethernauta/erc/2612";
import type { Provider } from "@ethernauta/eip/1193";
import {
  contract,
  create_provider,
  create_reader,
  encode_chain_id,
  http,
} from "@ethernauta/transport";
import { eth_call } from "@ethernauta/eth";
import { eip155_1 } from "@ethernauta/chain/eip155-1";
import {
  AddressSchema,
  Bytes32Schema,
  Uint8Schema,
  Uint256Schema,
  type Bytes,
} from "@ethernauta/core";
import { parse } from "valibot";

const CHAIN_ID = encode_chain_id({ namespace: "eip155", reference: eip155_1.chainId });
const reader = create_reader([
  { chainId: CHAIN_ID, transports: [http("https://ethereum-rpc.publicnode.com")] },
]);
declare const provider: Provider; // see /eips/6963 for discovery
const { signer } = create_provider(provider);

const token = parse(AddressSchema, "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
const owner = parse(AddressSchema, "0x70997970C51812dc3A010C7d01b50e0d17dc79C8");
const spender = parse(AddressSchema, "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC");
const value = parse(Uint256Schema, "0x3e8");
const deadline = parse(Uint256Schema, "0xffffffff");
const v = parse(Uint8Schema, "0x1b");
const r = parse(Bytes32Schema, "0x" + "00".repeat(32));
const s = parse(Bytes32Schema, "0x" + "00".repeat(32));

const token_ctx = contract({ chain_id: CHAIN_ID, to: token });

// step 1: read nonce + domain
const nonce_callable = nonces([owner])(token_ctx);
const nonce_bytes: Bytes = await eth_call([{ to: nonce_callable.to, input: nonce_callable.data }])(
  reader({ chain_id: CHAIN_ID }),
);
const nonce = nonce_callable.decode(nonce_bytes);

const ds_callable = DOMAIN_SEPARATOR()(token_ctx);
const ds_bytes: Bytes = await eth_call([{ to: ds_callable.to, input: ds_callable.data }])(
  reader({ chain_id: CHAIN_ID }),
);
const domain_separator = ds_callable.decode(ds_bytes);

void nonce;
void domain_separator;

// step 2: build the EIP-712 typed data, owner signs

// step 3: submit
const signed_bytes = await permit([owner, spender, value, deadline, v, r, s])(
  signer({ chain_id: CHAIN_ID, to: token }),
);

void signed_bytes;

Surface

MethodShapePurpose
permit({ owner, spender, value, deadline, v, r, s })Signable<Hash32>Submit a permit.
nonces({ owner })Callable<Uint256>Current permit nonce.
DOMAIN_SEPARATOR()Callable<Bytes32>The EIP-712 domain hash.

Typed-data payload

The signer signs an EIP-712 message with this shape:

domain: { name, version, chainId, verifyingContract }
types: {
  Permit: [
    { name: "owner", type: "address" },
    { name: "spender", type: "address" },
    { name: "value", type: "uint256" },
    { name: "nonce", type: "uint256" },
    { name: "deadline", type: "uint256" }
  ]
}

Hash with hash_typed_data from @ethernauta/eip/712, sign with the signer.

See also