Standard / EIP·EVM
EIP-712 — Typed Structured Data Hashing and Signing
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.
- 01off-chain orders (DEX, NFT marketplaces)
- 02permit-style approvals (ERC-2612, Permit2)
- 03meta-transactions / gasless UX
- 04Safe / multisig confirmations
- 05ERC-4337 UserOp signing
- # 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
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.
- ⚑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`.