EIP-712 Signature Replay: How Cross-Implementation Exploits Work
EIP-712 signature replay is a class of vulnerability where a valid signed message—intended for a specific smart contract or blockchain—is maliciously reused on a different contract, chain, or implementation. This exploit undermines the core promise of typed structured data signing: that a signature binds to a precise context. In practice, attackers leverage incomplete domain separator definitions, shared nonce spaces, or omitted identifiers to replay approvals, orders, or authorizations across Ethereum mainnet, L2s, sidechains, or upgradeable proxy patterns. Understanding these cross-implementation exploits is critical for any developer building with EIP-712-based features like gasless permits, off-chain order books, or cross-chain relayers.
This guide dives deep into the mechanical roots of EIP-712 signature replay, the real-world protocols that fell victim, and the architectural choices that prevent it. We examine the domain separator—the keystone of EIP-712—and show how small omissions or inconsistencies in its construction trigger drastic security gaps. By the end, you will not only recognize replay vectors but also know precisely how to defend against them in your own contracts.
- EIP-712 signature replay arises from insufficient domain separation, not cryptographic weakness.
- The domain separator must include chainId and verifyingContract to prevent cross-chain and cross-contract reuse.
- OpenSea Wyvern's omission of chainId is the classic real-world example of a replay exploit.
- Static domain separator hashes become stale after upgrades or chain-id changes; compute dynamically.
- Permit2's shared domain is by design but forgives cross-implementation replay within the same ecosystem.
- Always audit upgradeable contracts for replay across implementation versions using salt or version embeddings.
What Is EIP-712 Signature Replay? The Core Mechanism
EIP-712 defines a standard for hashing and signing typed structured data. Its security model relies on a domain separator—a unique identifier that scopes a signature to a particular contract and chain. A replay attack occurs when an attacker takes a signature produced for domain A and submits it to domain B, and domain B accepts it as valid. This is possible only if the two domains produce the same domain separator hash or if the verifying contract does not check domain fields it should.
The fundamental cause is insufficient domain separation. For example, if both contracts omit chainId from their domain separator, a signature made on Ethereum mainnet can be replayed on an L2 fork. Similarly, if the verifyingContract field is absent, a signature intended for one lending protocol can authorize actions in another unrelated protocol—so long as both use the same name and version in their domain. The exploit is not about breaking cryptography; it's about binding the cryptographic signature to an insufficiently unique context.
The Root Cause: Domain Separator Breakdown
The domain separator in EIP-712 is defined as:
EIP712Domain => [ name, version, chainId, verifyingContract, salt ]
Every field is optional, but omitting any of them dramatically increases the replay surface. The most critical fields are:
chainId: Prevents cross-chain replay. Without it, a signature on Ethereum mainnet (chainId 1) works on Polygon (chainId 137) or Gnosis Chain (chainId 100).verifyingContract: Prevents cross-contract replay. Without it, any contract with the samenameandversioncan reuse the signature.salt: Adds a unique extra entropy per deployment. Useful whenverifyingContractis not available (e.g., in proxy upgrades).
Many early implementations skipped these fields either for simplicity or to save gas. The OpenSea Wyvern exchange (pre-Seaport) famously omitted chainId early on, enabling cross-chain order replay on forks. Even today, some protocols rely solely on name and version, which are trivially replicated by an attacker deploying a contract with identical string values.
Real-World Exploit: OpenSea Wyvern and Cross-Fork Replay
Omitting chainId from an EIP-712 domain means a signature can be valid on every EVM chain where a contract with the same address exists.
The original OpenSea Wyvern exchange used a domain separator that included name ('Wyvern Exchange') and verifyingContract, but no chainId. This meant any signature made for the Wyvern contract on Ethereum mainnet could be legally replayed on any fork where a contract with the same address existed—such as an Ethereum Classic replica or a testnet. Attackers could take a user's order signed on mainnet and execute it on a lower-value fork, acquiring assets at the agreed price while the user's real order remained unexecuted. OpenSea later patched by adding chainId to the domain and forcing re-signing. This incident serves as the canonical example of why chainId is non-negotiable in production.
Similar vulnerabilities have been found in early versions of 0x and Rarible, where domain separators were not anchored to a unique chain identifier.
Cross-Chain Replay: Forks, L2s, and the ChainId Omission
Even when chainId is included, cross-chain replay is possible if the chainId changes after deployment. This is the scenario a hard fork creates: if a chain splits or otherwise changes its chainId, a contract that stored a cached domain separator hash at construction does not retroactively change it—signatures computed with the old hash remain valid after the chainId changes. EIP-712 recommends computing the domain separator dynamically on each call to avoid this stale-hash issue.
Additionally, sidechains that run EVM but have the same chainId as Ethereum (e.g., early L2 testnets) can cause copies. Always verify that the chainId you use is globally unique. The EIP-155 chainId registry is maintained by the Ethereum community, but custom rollups may accidentally collide.
Cross-Contract Replay: Shared Nonce and VerifyingContract Absence
Cross-contract replay happens when two different smart contracts accept the same typed signature message because they either:
- Use identical domain separators (same
name,version,chainId, omitverifyingContract) - Share the same nonce tracker across different domains
Consider two ERC-20 permit contracts (EIP-2612) from different projects. If both use the same domain separator fields but omit verifyingContract, a permit signed for token A can be replayed on token B, granting the attacker the right to use the signature's allowance on token B. This is especially dangerous when both tokens are held by the same user. To mitigate, EIP-2612 mandates including the token's own address as the verifyingContract in the domain separator.
Nonce reuse amplifies this: if a permit contract uses a global nonce per user (not per domain), signing two permits for different versions of the same contract can cause one to invalidate the other. Worse, if the nonce is not hashed together with the domain, an attacker could craft a permit that uses the same nonce but a different domain, creating a collision.
Comparison Table: Domain Separator Fields Across Protocols
| Protocol | name | version | chainId | verifyingContract | salt | Known Replay Fix |
|---|---|---|---|---|---|---|
| OpenSea Wyvern (old) | Wyvern Exchange | 1.0 | ❌ | ✅ | ❌ | Added chainId |
| Uniswap V3 Permit2 | Permit2 | 1 | ✅ | ✅ | ❌ | Uses nonce per token + owner |
| OpenSea Seaport | Seaport | 1.5 | ✅ | ✅ | ❌ | Domain includes contract |
| 0x V4 (old) | 0x Protocol | 2.0 | ❌ | ✅ | ✅ | Later added chainId |
| EIP-2612 (standard) | (token name) | 1 | ✅ | ✅ (token addr) | ❌ | verifyingContract = token |
| Gnosis Safe (relay) | Gnosis Safe | 1 | ✅ | ✅ | ✅ (optional) | Separator computed dynamically |
The Permit2 Dilemma: Shared Typed Signatures Across Implementations
Uniswap Permit2 introduces a universal nonce and approval system that allows users to sign a single permit and use it across multiple routers. However, it also introduces a new replay risk: if two different protocols both adopt Permit2, they share the same domain separator (since Permit2's domain is fixed). An attacker could replay a permit meant for Uniswap on another DEX that also uses Permit2 if the recipient or spender addresses overlap. This is intentional by design—Permit2 is built to be reusable—but it means the signature's authorization window is wider than the user may expect.
More concerning is cross-chain Permit2 replay. Since Permit2 uses chainId, a different chain should prevent replay. But if a chain fork shares the same chainId (e.g., both call themselves chainId 1), permits are replayable. Always ensure your chainId is unique. Permit2 also uses a salt of zero, meaning two deployments with the same address on different chains (impossible by address derivation) would not be separated.
Mitigation Strategies: Building Replay-Proof EIP-712 Systems
Preventing EIP-712 signature replay requires following the standard's intent, not just its letter. Key strategies include:
- Always include chainId and verifyingContract. These fields cost nothing in gas when hashed once and cached, but they eliminate most replay vectors.
- Use a salt for upgradeable contracts. When the verifying contract address may change via proxy, derive the domain using an immutable
saltthat changes with each implementation version. EIP-712 permits asaltfield for this purpose. - Compute the domain separator dynamically. Do not hardcode a hash at construction. Dynamically recompute it on each call so that change of chainId (e.g., during a protocol upgrade) automatically invalidates old signatures.
- Incorporate a nonce that is scoped to the domain. Each domain should have its own nonce tracker per user. You can derive the nonce by hashing the user address with the domain separator.
- Test cross-implementation scenarios. Write Foundry tests that attempt to replay your signature on different contracts, chains, and upgrade implementations.
Advanced: Replay Across Upgradeable Contracts and Beacon Proxies
Upgradeable contracts introduce a subtle replay vector: if the implementation changes but the domain separator points only to the proxy address (the verifyingContract), the domain remains the same even though the logic has changed. A signature that was valid for an old implementation may still be valid for the new one, unless the new implementation explicitly checks a version or salt. Use the salt field of the domain separator to embed an implementation version number. When the implementation is upgraded, change the salt so that all old signatures become invalid.
Beacon proxies add another layer: if the beacon's address is used as verifyingContract, the signature binds to the beacon, not to any specific implementation. This can be desirable for universal permits, but it also means a signature is valid for any future implementation. Balance between flexibility and security by hashing the implementation address inside the message rather than the domain.
Tools like OpenZeppelin Defender and EIP-2535 Diamonds also face similar challenges. Always audit the lifecycle of the verifyingContract and whether old signers can influence new logic.
Conclusion and Future Directions
EIP-712 signature replay, while preventable, remains a recurring vulnerability in the DeFi ecosystem. The root cause is always insufficient domain separation—whether through omission of chainId, verifyingContract, or stale implementation references. As cross-chain and cross-application interoperability grows (ERC-4337 account abstraction, CCIP, layer-zero), the replay surface expands. Future EIPs may standardize a universal domain separator that includes a global chain namespace, but until then, developers must be meticulous.
Defending against replay is not an optional optimization; it is a fundamental security requirement. Use the comparison table above as a checklist. Always think: 'If this signature were wrong but valid on a different chain or contract, what would happen?' The answer should be 'rejected'.
Common mistakes to avoid
- Omitting chainId from the domain separator, exposing signatures to cross-chain replay on forks and L2s.
- Using a static, cached domain separator that does not update after a protocol upgrade changes chainId or verifyingContract.
- Failing to include verifyingContract, allowing cross-contract replay between different protocols with the same name/version.
- Assuming nonce is unique per user globally instead of scoping it per domain (nonce collision opens replay).
- Not testing signature replay across different proxy implementations when using upgradeable contracts.
- Copying domain separator values from another protocol without adjusting to one's own unique fields.
Frequently asked questions
Can EIP-712 signature replay be prevented without increasing gas costs?
Yes: the domain separator is hashed once and cached, so including chainId and verifyingContract has negligible gas overhead. The replay protection is free after the initial hash.
How does EIP-2612 relate to EIP-712 replay vulnerabilities?
EIP-2612 (permit) is built on EIP-712. Early implementations sometimes omitted verifyingContract, leading to cross-token replay. The standard now mandates including the token address as verifyingContract to prevent that.
What is the difference between cross-chain and cross-contract replay?
Cross-chain replay reuses a signature on a different blockchain (e.g., Ethereum → Polygon) due to missing chainId. Cross-contract replay reuses it on a different contract on the same chain due to missing verifyingContract or identical domain identifiers.
Does EIP-712 still have replay issues if I include all domain fields?
Even with all fields, a change in any field (e.g., chainId after a hard fork) invalidates signatures only if the domain separator is recomputed dynamically. Also, upgradeable contracts require careful use of salt or version to prevent old signatures from working on new implementations.
How does ERC-4337 affect EIP-712 replay?
ERC-4337 user operations use EIP-712 signatures. Since account abstraction allows signature verification from different entry points, domain separation between entry points is critical. Similarly, cross-chain user ops via ERC-4337 require unique chainId and entry point address in the domain.
Related reading
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.