Typed Data and EIP-712¶
Plain message signing, covered in Signing Messages, has a real usability problem: a wallet can only show the user a raw, undifferentiated string, for anything beyond a short login message, the user has no reliable way to actually understand what they're approving. EIP-712 solves this by making signed data structured and human-readable, verified here with a real, working signature.
The problem plain signing creates¶
Recall from Signing Messages that a malicious dapp can present a disguised message. This risk is much worse for plain signing of anything more complex than a short login phrase, a wallet showing a user a wall of raw hex or a long, unstructured string gives them essentially no practical way to verify what a complex authorization (a token permit, an order for a decentralized exchange) actually says before approving it.
Structured, typed data¶
import { verifyTypedData } from "viem";
const domain = {
name: "Example App",
version: "1",
chainId: 1,
verifyingContract: "0x000000000000000000000000000000000000dEaD" as const,
} as const;
const types = {
Order: [
{ name: "buyer", type: "address" },
{ name: "amount", type: "uint256" },
{ name: "price", type: "uint256" },
],
} as const;
const message = {
buyer: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" as const,
amount: 100n,
price: 5000n,
};
const signature = await account.signTypedData({
domain,
types,
primaryType: "Order",
message,
});
const isValid = await verifyTypedData({
address: account.address,
domain,
types,
primaryType: "Order",
message,
signature,
});
console.log("valid:", isValid);
Verified: this exact structure, run with a real local account, correctly signs and verifies. A wallet supporting EIP-712 shows the user the decoded structure ("Order: buyer 0xd8dA..., amount 100, price 5000") rather than an opaque blob, letting the user actually evaluate what they're approving in the same terms the application itself uses.
The domain separator: preventing cross-application replay¶
The domain object (name, version, chain ID, and the specific verifying contract's address) is hashed into every signature as a domain separator, ensuring a signature valid for one specific application, on one specific chain, targeting one specific contract, cannot be replayed against a different application, a different chain, or a different contract, even if the rest of the signed data happens to be identical. This closes a real, documented risk category: without domain separation, a signature a user approved for one legitimate purpose could, in principle, be captured and reused somewhere the user never intended it to apply.
Where this shows up in practice¶
EIP-712 signatures are the basis for gasless approvals (the permit pattern, letting a user authorize a token spend via signature alone, with a relayer paying the gas to submit it on-chain, see Allowances and Approvals), off-chain order signing for decentralized exchanges (a user signs an intended trade once; the exchange's matching engine only submits an on-chain transaction once a counter-order is found, saving the gas cost of posting every unmatched order), and many other patterns where an application wants a cryptographically binding commitment without the immediate cost of an on-chain transaction.
Common misconceptions¶
EIP-712 does not make a signature inherently safer just because it's more readable, a well-designed wallet displaying a clearly structured but still malicious request (a permit granting unlimited spending to an attacker's address, clearly labeled as such) is still a request a careless user can approve; readability helps users make an informed decision, it doesn't make that decision for them.
A permit-style EIP-712 signature is not the same as an on-chain approve transaction in terms of when it takes effect, the signature alone grants nothing until someone (the user themselves, or a relayer on their behalf) submits it in an actual transaction calling the contract's permit function, which is what actually updates the on-chain allowance.
Further reading¶
← Previous: Signing Messages · Back to Building Web3 Applications · Next: Transaction Receipts →