EIP-712 Typed Data
The type system that EIP-712 uses to describe signable structured messages. Modeled on Solidity structs, it gives wallets enough information to render human-readable signing prompts and gives contracts a canonical form to hash and verify.
The type grammar
A struct type has a valid identifier as a name and zero or more member variables. Each member variable has a member type and a name. Member types fall into three categories.
Atomic types
Types with a fixed byte width, encoded directly:
bytes1throughbytes32uint8throughuint256int8throughint256booladdress
Deliberate omissions:
- No
uint/intaliases — the width must be explicit. - No fixed-point numbers.
- Contract addresses are always plain
address— not distinguished from EOAs.
Dynamic types
Types with variable length, declared like atomic types but encoded differently:
bytesstring
Dynamic values are encoded as the keccak256 hash of their contents. This is what keeps every member slot in encodeData at a fixed 32-byte width regardless of the underlying value length.
Reference types
Types that reference other types:
- Arrays:
Type[](dynamic) orType[n](fixed size). Encoded as thekeccak256hash of the concatenatedencodeDataof their contents — so aSomeType[5]has the same encoding as a struct with five members ofSomeType. - Structs: references to other struct types by name. Encoded recursively as
hashStruct(value).
Recursive struct types are supported (e.g., struct List { uint256 value; List next; }). Cyclical data is undefined — the standard is optimized for tree-shaped data.
encodeType: the canonical string form
A struct type is serialized to a canonical string:
Name(type1 name1,type2 name2,...,typeN nameN)
Example:
Mail(address from,address to,string contents)
When a struct references other struct types (including transitively), the set of referenced types is:
- Collected
- Sorted alphabetically by name
- Appended to the primary type’s encoding
Example:
Transaction(Person from,Person to,Asset tx)Asset(address token,uint256 amount)Person(address wallet,string name)
This canonical form is what gets hashed into typeHash:
bytes32 constant MAIL_TYPEHASH = keccak256("Mail(address from,address to,string contents)");
The typeHash becomes a compile-time constant. No runtime string manipulation is needed.
encodeData: the per-instance encoding
The instance of a struct is encoded as the concatenation of its encoded member values, each exactly 32 bytes long:
| Member kind | Encoding |
|---|---|
bool | uint256 0 or 1 |
address | uint160 left-padded to 32 bytes |
uint/int | Sign-extended to 256 bits, big-endian |
bytes1–bytes31 | Zero-padded at the end to bytes32 |
bytes, string | keccak256 of contents |
| Arrays | keccak256 of concatenated encodeData of elements |
| Nested structs | hashStruct(value) (recursive) |
Because every member is exactly 32 bytes, encodeData resembles Solidity’s abi.encode closely enough that a one-line implementation works:
function hashStruct(Mail memory mail) pure returns (bytes32) {
return keccak256(abi.encode(
MAIL_TYPEHASH,
mail.from,
mail.to,
keccak256(mail.contents) // dynamic type: hash the contents
));
}
The keccak256(mail.contents) call on the dynamic string is the only step where the developer has to remember the special case. Everything else falls through to abi.encode.
Why not ABIv2 directly?
ABIv2 encoding (abi.encode) is non-deterministic — there are multiple valid encodings of the same data. It also does not allow in-place EVM computation. EIP-712’s encodeData rules are a deliberate subset that preserves determinism and admits an optimized implementation.
Why not tight packing?
Tight packing (Solidity’s default when passing multiple arguments to keccak256) minimizes bytes hashed but requires complex packing instructions in EVM and prevents in-place computation. EIP-712 trades bytes for simplicity and speed.
JSON-RPC representation
In the eth_signTypedData RPC, types are passed as a JSON object:
{
"types": {
"EIP712Domain": [
{"name": "name", "type": "string"},
{"name": "version", "type": "string"},
{"name": "chainId", "type": "uint256"},
{"name": "verifyingContract", "type": "address"}
],
"Person": [
{"name": "name", "type": "string"},
{"name": "wallet", "type": "address"}
],
"Mail": [
{"name": "from", "type": "Person"},
{"name": "to", "type": "Person"},
{"name": "contents", "type": "string"}
]
},
"primaryType": "Mail",
"domain": { ... },
"message": { ... }
}
The types map includes every referenced struct, primaryType names the top-level type being signed, domain is the EIP712Domain instance, and message is the primaryType instance.
Connections
- EIP-712: Typed structured data hashing and signing: the parent standard
- EIP-712 hashStruct: how the type encoding becomes a hash
- EIP-712 Domain Separator: the mandatory top-level
EIP712Domaintype