Skip to main content
Sign a Permit2 or ERC-2612 permit using EIP-712 typed data
import * as Secp256k1 from '@tevm/voltaire/Secp256k1';
import * as Keccak256 from '@tevm/voltaire/Keccak256';
import * as Hex from '@tevm/voltaire/Hex';
import * as Hash from '@tevm/voltaire/Hash';

// Private key (32 bytes)
const privateKey = Hex.toBytes(
	"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
);

// EIP-712 Domain Separator
const domain = {
	name: "Uniswap V2",
	version: "1",
	chainId: 1n,
	verifyingContract: "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D",
};

// Permit type hash: keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)")
const PERMIT_TYPEHASH = Hash.keccak256String(
	"Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"
);

// EIP-712 domain type hash
const EIP712_DOMAIN_TYPEHASH = Hash.keccak256String(
	"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
);

// Permit message data
const permit = {
	owner: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
	spender: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
	value: 1000000000000000000n, // 1 token (18 decimals)
	nonce: 0n,
	deadline: 1893456000n, // Far future timestamp
};

// Helper: ABI encode an address (20 bytes -> 32 bytes, left-padded)
function encodeAddress(addr: string): Uint8Array {
	const bytes = Hex.toBytes(addr);
	const padded = new Uint8Array(32);
	padded.set(bytes, 12); // Left-pad to 32 bytes
	return padded;
}

// Helper: ABI encode a uint256
function encodeUint256(value: bigint): Uint8Array {
	const bytes = new Uint8Array(32);
	let v = value;
	for (let i = 31; i >= 0; i--) {
		bytes[i] = Number(v & 0xffn);
		v >>= 8n;
	}
	return bytes;
}

// Helper: Concatenate Uint8Arrays
function concat(...arrays: Uint8Array[]): Uint8Array {
	const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);
	const result = new Uint8Array(totalLength);
	let offset = 0;
	for (const arr of arrays) {
		result.set(arr, offset);
		offset += arr.length;
	}
	return result;
}

// Step 1: Hash the domain separator
// domainSeparator = keccak256(abi.encode(
//   EIP712_DOMAIN_TYPEHASH,
//   keccak256(name),
//   keccak256(version),
//   chainId,
//   verifyingContract
// ))
const domainSeparator = Keccak256.hash(concat(
	EIP712_DOMAIN_TYPEHASH,
	Hash.keccak256String(domain.name),
	Hash.keccak256String(domain.version),
	encodeUint256(domain.chainId),
	encodeAddress(domain.verifyingContract),
));

// Step 2: Hash the struct data
// structHash = keccak256(abi.encode(
//   PERMIT_TYPEHASH, owner, spender, value, nonce, deadline
// ))
const structHash = Keccak256.hash(concat(
	PERMIT_TYPEHASH,
	encodeAddress(permit.owner),
	encodeAddress(permit.spender),
	encodeUint256(permit.value),
	encodeUint256(permit.nonce),
	encodeUint256(permit.deadline),
));

// Step 3: Create EIP-712 signing hash
// digest = keccak256("\x19\x01" + domainSeparator + structHash)
const prefix = new Uint8Array([0x19, 0x01]);
const digest = Keccak256.hash(concat(prefix, domainSeparator, structHash));

// Step 4: Sign the digest
const signature = Secp256k1.signHash(digest, privateKey);

// Step 5: Extract r, s, v components
const r = Hex.fromBytes(signature.r);
const s = Hex.fromBytes(signature.s);
const v = signature.v;

// Ready for permit() call: token.permit(owner, spender, value, deadline, v, r, s)
This is a fully executable example. View the complete source with test assertions at examples/signing/eip712-permit.ts.

EIP-712 Structure

EIP-712 defines a standard for typed structured data hashing and signing:
  1. Domain Separator - Uniquely identifies the application (contract name, version, chain, address)
  2. Type Hash - keccak256 of the type string (e.g., "Permit(address owner,...)")
  3. Struct Hash - keccak256 of the encoded struct data with type hash
  4. Digest - keccak256("\x19\x01" || domainSeparator || structHash)
The \x19\x01 prefix prevents collision with eth_sign messages.