ERCs
ERC-2612 — Permit (gasless approvals)
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
| Method | Shape | Purpose |
|---|---|---|
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.