EIP-712 Typed Data

concept
ethereumeip-712type-systemencodingabi

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:

  • bytes1 through bytes32
  • uint8 through uint256
  • int8 through int256
  • bool
  • address

Deliberate omissions:

  • No uint/int aliases — 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:

  • bytes
  • string

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) or Type[n] (fixed size). Encoded as the keccak256 hash of the concatenated encodeData of their contents — so a SomeType[5] has the same encoding as a struct with five members of SomeType.
  • 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:

  1. Collected
  2. Sorted alphabetically by name
  3. 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 kindEncoding
booluint256 0 or 1
addressuint160 left-padded to 32 bytes
uint/intSign-extended to 256 bits, big-endian
bytes1bytes31Zero-padded at the end to bytes32
bytes, stringkeccak256 of contents
Arrayskeccak256 of concatenated encodeData of elements
Nested structshashStruct(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