← Protocols
EIP-712 — Typed Structured Data Hashing and Signing
Standard / EIP·EVM

EIP-712 — Typed Structured Data Hashing and Signing

01Description

Standard for hashing and signing typed structured data so wallets can show users a human-readable message (domain + types + values) instead of opaque hex. Status: Final (Standards Track / Interface). Underpins Permit, OpenSea orders, Safe transactions, ERC-4337 UserOp signatures, and almost every meta-tx scheme.

02Best for
  • 01off-chain orders (DEX, NFT marketplaces)
  • 02permit-style approvals (ERC-2612, Permit2)
  • 03meta-transactions / gasless UX
  • 04Safe / multisig confirmations
  • 05ERC-4337 UserOp signing
03Install
  • # No install — EIP-712 is implemented by every signing library:
  • pnpm add viem # signTypedData / verifyTypedData / recoverTypedDataAddress / hashTypedData
  • pnpm add ethers # signer.signTypedData(domain, types, value) / TypedDataEncoder
  • pnpm add @metamask/eth-sig-util # signTypedData_v4 reference impl
05Prompt snippet
Use EIP-712 to sign structured data with a wallet. Build a `domain` `{ name, version, chainId, verifyingContract, salt? }`, a `types` object whose keys are struct names mapping to `{ name, type }[]`, and a `message` matching the primary type. With viem: `await walletClient.signTypedData({ account, domain, types, primaryType, message })`. Verify off-chain via `verifyTypedData({ address, domain, types, primaryType, message, signature })` or recover with `recoverTypedDataAddress(...)`. On-chain, hash with `keccak256("\x19\x01" || domainSeparator || hashStruct(message))` where `domainSeparator = keccak256(abi.encode(EIP712_DOMAIN_TYPEHASH, ...))`. For contract signers (multisigs, smart accounts), pair with EIP-1271 `isValidSignature(hash, sig)` to validate the result. The exact same digest format is used by ERC-2612 permits, Permit2, ERC-4337 UserOps, and Safe message hashes.
06Gotchas
  • ChainId in the domain MUST match the chain where the signature will be used — signatures are otherwise replayable across forks/L2s. Always include `chainId` and `verifyingContract` in the domain unless you have a specific replay-by-design use case.
  • Type strings are order-sensitive: `EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)` — reordering changes the typeHash and the signature will not verify.
  • Nested structs are encoded recursively; the typeString concatenates the primary type's encoding followed by referenced types ALPHABETICALLY (`Mail(Person from,Person to)Person(string name,address wallet)`).
  • `bytes` and `string` fields are hashed (`keccak256`) before being included in `hashStruct`; fixed-size types are encoded as 32-byte words. Off-by-one here silently produces a signature that recovers to a wrong address.
  • `signTypedData_v3` (legacy MetaMask) ignores arrays and nested structs — always use v4 / viem / ethers.signTypedData.
  • When a smart-account signer (ERC-1271) is used, the recovered address WILL NOT match the signer EOA — call `isValidSignature(hash, sig) == 0x1626ba7e` instead of `ecrecover`.
07Alternatives