EIP-712: Typed structured data hashing and signing
EIP-712 is the Ethereum standard for hashing and signing typed structured data. It replaced opaque eth_sign hex strings with structured, human-readable, domain-scoped signatures. Authored by Remco Bloemen, Leonid Logvinov, and Jacob Evans; created 2017-09-12. Standards Track, Interface category. Requires EIP-155 and EIP-191.
Motivation
Signing raw bytestrings is a solved problem. Signing structured messages is not — and hashing them incorrectly erases the security properties of the system. Before EIP-712, users were shown messages like 0x19457468657265756d205369676e6564... and asked to sign blindly. This defeats the purpose of asking for consent. EIP-712 encodes both the data and its structure so that wallets can render field names and values in a verification UI before the user signs.
The extended signable message set
The set of signable messages is extended from transactions and bytestrings 𝕋 ∪ 𝔹⁸ⁿ to also include structured data 𝕊:
encode(transaction : 𝕋) = RLP_encode(transaction)encode(message : 𝔹⁸ⁿ) = "\x19Ethereum Signed Message:\n" ‖ len(message) ‖ messageencode(domainSeparator : 𝔹²⁵⁶, message : 𝕊) = "\x19\x01" ‖ domainSeparator ‖ hashStruct(message)
The encoding is deterministic (each component is) and injective (the three cases always differ in the first byte, and RLP_encode(transaction) never starts with \x19). The encoding is EIP-191 compliant with version byte 0x01, domain separator as version-specific data, and hashStruct(message) as the data to sign.
See Ethereum Signed Message Encoding for the full leading-byte disambiguation scheme.
Typed structured data (𝕊)
The type system is modeled on Solidity structs. Example:
struct Mail {
address from;
address to;
string contents;
}
Three categories of member types:
- Atomic types:
bytes1–bytes32,uint8–uint256,int8–int256,bool,address. Nouint/intaliases. No fixed-point numbers. - Dynamic types:
bytes,string. Declared like atomic types but encoded differently. - Reference types: Arrays (
Type[]orType[n]) and other structs. Recursive struct types are supported; cyclical data is undefined.
See EIP-712 Typed Data for the full type grammar.
hashStruct
The core hashing function:
hashStruct(s : 𝕊) = keccak256(typeHash ‖ encodeData(s))
typeHash = keccak256(encodeType(typeOf(s)))
typeHash is a compile-time constant per type. Because it prefixes encodeData, encodeData only needs to be injective within a single type — different types are already separated by their different typeHash.
Full derivation and alternative encodings considered are covered in EIP-712 hashStruct.
encodeType
A struct type is encoded as Name(type1 name1,type2 name2,...,typeN nameN). Example: Mail(address from,address to,string contents).
When a struct references other struct types, the set of referenced types is collected, sorted alphabetically by name, and appended to the encoding:
Transaction(Person from,Person to,Asset tx)Asset(address token,uint256 amount)Person(address wallet,string name)
encodeData
Concatenation of encoded member values, each exactly 32 bytes:
- Booleans encoded as
uint2560 or 1 - Addresses encoded as
uint160 - Integers sign-extended to 256 bits, big-endian
bytes1–bytes31zero-padded at the end tobytes32- Dynamic
bytesandstringencoded askeccak256of their contents - Arrays encoded as
keccak256of concatenatedencodeDataof their elements - Nested structs encoded recursively as
hashStruct(value)
This matches abi.encode closely enough that a Solidity implementation is a one-liner:
function hashStruct(Mail memory mail) pure returns (bytes32) {
return keccak256(abi.encode(
MAIL_TYPEHASH,
mail.from,
mail.to,
keccak256(mail.contents)
));
}
It also admits an in-place EVM implementation that writes typeHash and sub-hashes into the struct’s own memory layout and hashes in place, backing up and restoring the surrounding words.
Domain separator
domainSeparator = hashStruct(eip712Domain)
Where EIP712Domain is a struct with one or more of:
string name— the user-readable name of the signing domain (DApp or protocol name)string version— the major version; signatures from different versions are incompatibleuint256 chainId— the EIP-155 chain ID; wallets should refuse signing if it does not match the active chainaddress verifyingContract— the contract that will verify the signature; wallets may apply contract-specific phishing preventionbytes32 salt— a disambiguating salt of last resort
Protocol designers include only the fields that make sense for their domain. Fields must appear in the order above (with absences skipped); future field additions must be alphabetical and come after these. See EIP-712 Domain Separator for why this matters and the canonical patterns.
eth_signTypedData JSON-RPC
The wallet RPC added by EIP-712:
sign(keccak256("\x19\x01" ‖ domainSeparator ‖ hashStruct(message)))
Parameters:
Address— 20 bytes, the signing account (must be unlocked)TypedData— JSON object withtypes,primaryType,domain,message
JSON schema (simplified):
{
"types": {
"EIP712Domain": [...],
"CustomStruct": [...]
},
"primaryType": "CustomStruct",
"domain": { "name": "...", "version": "...", "chainId": 1, "verifyingContract": "0x..." },
"message": { ... }
}
Returns a 65-byte hex signature in (r, s, v) big-endian order. The v parameter includes the EIP-155 chain ID.
personal_signTypedData takes an additional password argument. web3.eth.signTypedData(typedData, address) and web3.eth.personal.signTypedData(typedData, address, password) expose the same operation in Web3.js.
Security considerations
EIP-712 is a hashing and signing standard only. Two application-level concerns it explicitly does not address:
Replay attacks: Signed messages can be submitted twice unless the application rejects duplicates or makes the authorized action idempotent. Nonces are the standard mitigation. See Off-Chain Signatures for replay protection patterns.
Frontrunning: An attacker who intercepts a signature can submit it to the contract before the intended use. The application must either reject the out-of-order submission or produce the same effect regardless.
Backwards compatibility
keccak256(someInstance) in Solidity, when someInstance is a struct, currently evaluates to the hash of the memory address of the instance. EIP-712 considers this behavior dangerously broken — it appears correct in some scenarios and silently fails determinism in others. DApps depending on it are broken.
Connections
- EIP-712 Typed Data: the type system and encoding rules
- EIP-712 Domain Separator: binding signatures to chain, contract, name, version
- EIP-712 hashStruct: the core hashing algorithm and its in-place EVM implementation
- Off-Chain Signatures: the broader pattern of authorizing on-chain actions with off-chain signatures
- Ethereum Signed Message Encoding: the
\x19leading-byte scheme from ERC-191 - ERC-1271: Standard Signature Validation Method for Contracts: the standard that lets smart accounts verify EIP-712 signatures on their own behalf
- Smart Account Signatures: the EOA vs contract signer verification pattern
- Creating a Farcaster Account by Hand: applied example — computing an EIP-712 digest manually with
castand routing it through a Safe via EIP-1271