EIP-712 Domain Separator
The domain separator is the 32-byte value that binds an EIP-712 signature to a specific protocol, version, chain, and contract. It is the mechanism that prevents a signature produced for one DApp from being replayed against another — even when the two DApps use structurally identical message types.
The definition
domainSeparator = hashStruct(eip712Domain)
Where eip712Domain is an instance of a reserved struct type named EIP712Domain. Domain separators are computed using the same hashStruct rules as any other struct — EIP712Domain is not special at the hashing layer, only semantically.
The EIP712Domain fields
EIP712Domain may contain one or more of the following fields. Protocol designers include only the fields that make sense for their signing domain:
| Field | Purpose |
|---|---|
string name | User-readable name of the signing domain (e.g., “Ether Mail”, “Uniswap V2”, “Farcaster SignedKeyRequestValidator”) |
string version | Current major version; signatures from different versions are incompatible |
uint256 chainId | EIP-155 chain ID; wallets should refuse signing if it does not match the active chain |
address verifyingContract | Address of the contract that will verify the signature; enables contract-specific phishing prevention |
bytes32 salt | Disambiguating salt of last resort |
Field ordering rules:
- Present fields must appear in the order above (absences skipped).
- Future field additions must be alphabetical and come after these.
- User-agents should accept fields in any order as specified by the
EIP712Domaintype. - DApp implementers should not add private fields; new fields should be proposed through the EIP process.
What the domain separator protects against
Cross-DApp collision
Two DApps may independently define the same structure:
Transfer(address from,address to,uint256 amount)
Without domain separation, a signature for DApp A’s Transfer would also authorize DApp B’s Transfer — a catastrophic replay. With domain separation, the two signing domains produce distinct domainSeparator values and the signatures are incompatible.
Cross-chain replay
Because chainId is part of EIP712Domain, a signature for Ethereum mainnet cannot be replayed on Optimism, Arbitrum, or any other EVM chain. This is why wallets should verify chainId against the active chain before signing.
Cross-contract replay within the same DApp
Because verifyingContract is part of EIP712Domain, a signature for contract A cannot be replayed against contract B even in the same DApp — useful when a protocol has multiple verifier contracts.
Cross-version replay
Because version is part of EIP712Domain, upgrading a protocol’s major version invalidates all outstanding signatures from the previous version. This is how a protocol can force renewal of existing approvals after a breaking change.
Multiple signing purposes within a single DApp
The same DApp may need signatures from multiple parties over the same struct — for example, both from and to in a Transfer(address from,address to,uint256 amount). By using two distinct domain separators (e.g., one with name: "DApp From" and one with name: "DApp To"), the signatures can be distinguished from each other without changing the underlying struct type.
Rejected alternative: verifyingContract alone
An early alternative was to use only the verifying contract address as the domain separator. This solves the cross-DApp collision problem but:
- Does not support multiple signing purposes on the same struct within a DApp.
- Does not separate across versions or chains.
- Does not give wallets a human-readable name to display.
The full EIP712Domain struct was adopted instead. The standard still suggests implementors include verifyingContract where appropriate — it just isn’t sufficient on its own.
The final signable encoding
Once the domain separator is computed, every EIP-712 signature follows the same pattern:
sign(keccak256("\x19\x01" ‖ domainSeparator ‖ hashStruct(message)))
The \x19\x01 prefix is ERC-191 version byte 0x01 with the domain separator as version-specific data. The hashStruct(message) at the end is the primary-type struct being authorized. See Ethereum Signed Message Encoding for how this prefix keeps the encoding injective with respect to transactions and raw-bytes signing.
Applied example: Farcaster SignedKeyRequest
The Farcaster account-creation walkthrough shows a real-world domain separator computation:
EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)
With:
name = "Farcaster SignedKeyRequestValidator"version = "1"chainId = 10(Optimism)verifyingContract = 0x00000000FC700472606ED4fA22623Acf62c60553
These four fields ensure the signature authorizing a new Farcaster signing key is bound to Farcaster’s validator on Optimism and cannot be replayed against any other contract, chain, or protocol version.
Caching in Solidity
Protocol implementations typically cache the domain separator as a storage variable or a bytes32 immutable, recomputing only if chainId changes (rare, only on chain forks). The typeHash for EIP712Domain itself is always a compile-time constant.
bytes32 private constant EIP712_DOMAIN_TYPEHASH = keccak256(
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
);
bytes32 public immutable DOMAIN_SEPARATOR = keccak256(abi.encode(
EIP712_DOMAIN_TYPEHASH,
keccak256(bytes("MyProtocol")),
keccak256(bytes("1")),
block.chainid,
address(this)
));
Connections
- EIP-712: Typed structured data hashing and signing: the parent standard
- EIP-712 hashStruct: the hashing function used to compute the domain separator
- EIP-712 Typed Data: the type system that
EIP712Domainis an instance of - Off-Chain Signatures: the broader pattern the domain separator makes safe
- Creating a Farcaster Account by Hand: applied example computing a real domain separator