DeFi Intel

EIP-712 Struct Hash Collisions: Replay Attacks Across Chains

Quick answerEIP-712 struct hash collisions occur when identical message structures across different chains produce the same hash, allowing signature reuse. This enables replay attacks unless domain separators (chainId, verifyingContract, salt) are unique and properly enforced. Mitigations include strict domain separation and chain-specific validation.

EIP-712 struct hash collisions arise when the same typed data structure and parameters produce identical hashes on different blockchain networks, creating a vector for cross-chain replay attacks. This deep dive explains the cryptographic underpinnings, the role of domain separators, and how protocols like Uniswap and OpenSea have addressed or failed to address these vulnerabilities.

We will dissect real exploit scenarios, compare domain separator fields across chains, and provide concrete mitigations for developers building cross-chain applications that rely on off-chain signatures.

Key takeaways
  • EIP-712 struct hash collisions occur when domain separator fields are identical across different blockchains.
  • chainId is the most critical field for preventing cross-chain replay attacks; it must be dynamic and not hardcoded.
  • Domain-separation weaknesses have prompted chainId/verifyingContract hardening in protocols such as OpenSea (Wyvern), Uniswap Permit2, and Gnosis Safe.
  • Proper domain separation requires always including chainId, verifyingContract, and a unique salt per deployment.
  • Testing for collisions should be part of every cross-chain dApp’s CI pipeline, including fuzzing across multiple chain IDs.
  • EIP-5267 and improved wallet enforcement are helping, but developer caution remains the first line of defense.

The Hidden Danger of EIP-712 Struct Hash Collisions

EIP-712 introduced a standard for hashing typed structured data to improve security and user experience for off-chain signature verification. However, when the same struct types and field values are hashed on different chains, the resulting hashes can be identical if the domain separator does not diverge. This EIP-712 struct hash collision allows an attacker to take a signature intended for one blockchain and replay it on another, executing unintended actions.

For example, a user signs a limit order on Ethereum mainnet. If the contract on Polygon uses the same domain separator (e.g., missing chainId), the same signature can authorize the same trade on Polygon. This is not just theoretical — several high-profile exploits have leveraged this gap.

How EIP-712 Domain Separators Were Supposed to Prevent Cross-Chain Replay

The EIP-712 standard includes a domainSeparator (a keccak256 hash of domain fields: name, version, chainId, verifyingContract, salt) to bind a signature to a specific domain. The intent is that different chains have different chainId, and different contracts have distinct verifyingContract addresses. However, many early implementations omitted chainId or verifyingContract, making domains identical across deployments.

The Mechanics of Struct Hash Collisions

The EIP-712 hash computation involves two keccak256 hashes: one over domainSeparator and one over typedData. The final digest is keccak256(0x1901 || domainSeparator || hashStruct(message)). If the domain separator is identical across chains, and the message struct is identical, the final digest collides.

Consider a struct Order(address maker, address taker, uint256 amount, bytes32 salt). A user signs one order. On Ethereum, the domain includes chainId=1. On Polygon, chainId=137. If the contract does not include chainId in its domain separator, both hashes are equal — collision. The signature can be replayed.

Real-World Examples: Where Collisions Strike

Several real incidents highlight the risk:

These cases demonstrate that even major protocols have had to strengthen domain separation to close off replay vectors.

Cross-Chain Replay Attacks: The Attack Vector

A cross-chain replay attack using EIP-712 struct hash collisions works as follows:

  1. Victim signs a message on chain A (e.g., Ethereum) with a struct of type Transfer(address to, uint256 amount). The signature is valid for domain on chain A.
  2. Attacker intercepts the off-chain signature (e.g., via a public mempool, dApp frontend, or data breach).
  3. Attacker submits the same signature to chain B (e.g., Polygon) where the same contract address exists and domain separator matches.
  4. Chain B verifies the signature as valid, executing the transfer on the victim's behalf on chain B.

The key enabler is the absence of chain-specific domain fields. The attack does not require any private key compromise — only signature reuse.

Comparison Table: Domain Separator Fields and Collision Risks

Domain FieldPurposeRisk if MissingExample Impact
nameHuman-readable identifierLow collision risk if other fields uniqueMinor confusion
versionDistinguishes contract versionsCollision across different versionsReplay across upgrades
chainIdBinds signature to specific chainCross-chain replay (primary vector)OpenSea on Ethereum → Polygon
verifyingContractBinds to specific deployed contractCross-contract reuse on same chainSignature valid for multiple copies
saltAdditional entropyStatic salt → predictableReplay if name/version/chainId same

Mitigations: Proper Domain Separator Usage

Developers must ensure the domain separator is unique per chain and per deployment. Best practices:

Example correct domain construction in Solidity: EIP712Domain({ name: "MyApp", version: "1", chainId: block.chainid, verifyingContract: address(this) }).

Protocol-Level Safeguards (EIP-5267, EIP-712 with chainId Enforcement)

EIP-5267 proposes a standard for retrieving domain separator information from contracts, making it easier for off-chain signers to verify they are signing for the correct chain. Additionally, wallets and SDKs (like ethers.js and web3.js) now enforce chainId in EIP-712 methods. For example, MetaMask automatically prepends the chainId from the wallet's active network into the signing request, reducing human error.

Protocols should also implement nonce-based replay protection or include a unique identifier (like a deadline or order ID) in every struct. While these don't prevent cross-chain collisions directly, they limit the window for replay.

Testing Your Contracts for Collisions

To detect potential EIP-712 struct hash collisions, developers should:

OpenZeppelin's EIP-712 utilities provide a solid foundation, but always customize the domain constructor.

Future of EIP-712 and Cross-Chain Signatures

The Ethereum ecosystem is moving toward native cross-chain interoperability (e.g., LayerZero, Chainlink CCIP). EIP-712 struct hash collisions will become more critical as dApps expand across many chains. Future standards may introduce mandatory domain fields or a global registry of domain separators.

Until then, the responsibility lies with protocol developers. Vigilance in domain separation is cheap; a single missed field can cost millions. The community should adopt tooling that warns when chainId is omitted, and wallets should enforce domain uniqueness before signing.

Common mistakes to avoid

Frequently asked questions

What is the difference between a struct hash collision and a signature replay attack?

A struct hash collision is the precondition that enables signature replay. When two different domains produce the same hash output, signatures are interchangeable, allowing replay across chains.

Can a salt alone prevent EIP-712 struct hash collisions across chains?

A unique salt per chain can help, but if chainId is omitted and salt is static (or reused), collisions are still possible. The safest approach is to combine chainId, verifyingContract, and a random salt.

How can I test if my contract is vulnerable to cross-chain replay?

Deploy the contract to two testnets with different chain IDs, then try to use a signature from one chain's transaction on the other. If it succeeds, you have a collision.

Track the entities behind the concepts

DeFi Intel maps 11,000+ protocols, tokens and companies to a typed knowledge graph — with live data, incidents and regulation.

Entities mentioned