Signature replay across EVM chains
A signature proves that someone agreed to something. Unless the message says where and how often, it can be presented again on another chain, another contract or another day. The binding fields, and how they fail.
- Written by
- Quantix Research, Cross-Chain Security
- Published
- Reading time
- 6 minutes
- Category
- Cross-Chain Security
Primer. Explains a known class of issue. Code is simplified and illustrative, not for production use.

Contents
Off-chain signatures are everywhere in modern EVM applications: gasless approvals, meta-transactions, order books, airdrop claims, governance votes and cross-chain messages. Each one lets a contract act on a statement that someone signed earlier, somewhere else. The contract has to answer a question the signature alone does not: is this the occasion the signer meant?
When the answer is not built into the signed message, the same signature can be presented again: on another chain, to another contract, a second time on the same contract, or long after it was meant to expire. This primer walks through each form of replay, the field that prevents it, and the implementation details that undo those fields.
What a signature proves
An ECDSA signature over a hash proves that the holder of a private key signed that hash. Nothing more. It does not say which chain the signer was looking at, which contract they meant, or whether they intended the authorisation to be used once. If two situations produce the same hash, a signature for one is valid for the other.
Replay protection is therefore a matter of making sure that every situation the signer did not intend produces a different hash. EIP-712, the standard for typed structured data, provides the tools; using them correctly is the application’s job.
Binding a signature to one use
The digest combines a domain separator, which identifies the application and where it runs, with the hash of the message, which states what is authorised. The domain fields defined by EIP-712 are name, version, chainId, verifyingContract and salt, all optional in the standard. That optionality is where many replay bugs begin.
Compare a claim function that signs only the recipient and amount with one that uses the full structure:
// Simplified, illustrative contract. Not production code.contract RewardsV1 { address public immutable signer; IERC20 public immutable token; function claim(address to, uint256 amount, bytes calldata sig) external { bytes32 h = keccak256(abi.encodePacked(to, amount)); // no nonce, chain or contract address s = ECDSA.recover(MessageHashUtils.toEthSignedMessageHash(h), sig); require(s == signer, "bad signature"); token.transfer(to, amount); }}// The same claim, bound to one chain, one contract, one use and one time window.contract RewardsV2 is EIP712, Nonces { bytes32 private constant CLAIM_TYPEHASH = keccak256("Claim(address to,uint256 amount,uint256 nonce,uint256 deadline)"); address public immutable signer; IERC20 public immutable token; constructor(address signer_, IERC20 token_) EIP712("Rewards", "2") { signer = signer_; token = token_; } function claim(address to, uint256 amount, uint256 deadline, bytes calldata sig) external { if (block.timestamp > deadline) revert Expired(); bytes32 structHash = keccak256(abi.encode(CLAIM_TYPEHASH, to, amount, _useNonce(to), deadline)); // _hashTypedDataV4 adds the domain: name, version, chainId and this contract's address. address s = ECDSA.recover(_hashTypedDataV4(structHash), sig); if (s != signer) revert BadSignature(); SafeERC20.safeTransfer(token, to, amount); }}The first version accepts the same signature any number of times, on any chain where it is deployed, and on any other contract that uses the same signer and hash construction. The second binds the signature to one chain and one contract through the domain, to one use through the nonce, and to a time window through the deadline.
Replay across chains
Ordinary transactions faced this problem first. Before EIP-155, a signed transaction did not commit to a chain, so a transaction broadcast on one network could be rebroadcast on another that shared the same account history. EIP-155 added the chain ID to the signed transaction data.
Application signatures need the same protection, and it is easy to get wrong in two ways. The first is to omit chainId from the domain, which makes a signature valid on every chain where the contract is deployed at the same address, a common situation with deterministic deployment. The second is to compute the domain separator once, at deployment, and store it. If the chain later forks and the fork adopts a new chain ID, the stored separator still matches the old one, and signatures from one side are valid on the other. OpenZeppelin’s EIP712 base contract caches the separator but rebuilds it whenever block.chainid or the contract address differs from the cached values.
Cross-chain messaging extends the problem: a message must bind its source chain, destination chain and destination contract. The bridge exploit analysis places these failures in the wider bridge attack surface.
Replay across contracts and versions
verifyingContract binds a signature to one deployment. Without it, two contracts that share a signer and a message format, such as a token on two different markets or a v1 and v2 of the same application, accept each other’s signatures. name and version separate applications and releases that might otherwise share a domain, which matters when a new version is deployed behind the same address through a proxy.
Replay on the same contract
A nonce makes each signature single-use. Sequential nonces, as in EIP-2612 permits, are simple but force the signer to use signatures in order; a signature with nonce 5 is unusable until nonce 4 has been consumed. Applications that need signatures to be used independently, such as order books, use unordered nonces. Uniswap’s Permit2, for example, tracks nonces in a bitmap so that any unused nonce can be consumed in any order.
A deadline bounds how long a signature remains usable, which limits the damage if a signature leaks or the signer changes their mind. Cancellation, by consuming a nonce on-chain or invalidating a range, should be possible for every signature type an application accepts.
Malleability and recovery pitfalls
For every valid ECDSA signature there is a second valid signature over the same hash, obtained by replacing s with the curve order minus s and flipping v. The precompile behind ecrecover accepts both. A contract that prevents replay by recording used signatures, rather than used nonces or message hashes, can be bypassed by submitting the other form. OpenZeppelin’s ECDSA.recover rejects signatures whose s is in the upper half of the range, which removes the second form.
Recovery has a second pitfall: ecrecover returns the zero address for an invalid signature instead of reverting. If the expected signer is ever the zero address, for instance because it was never initialised, every invalid signature passes the check.
Smart accounts and EIP-1271
EIP-1271 lets a contract, typically a smart account, declare whether a signature is valid on its behalf through isValidSignature(hash, signature). A simple implementation checks that the account’s owner signed hash. If one key owns several smart accounts, the same owner signature is then valid for all of them, and a permit or order intended for one account can be replayed against another. ERC-7739 addresses this with defensive rehashing that includes the account’s address, while keeping the signed content readable in wallets.
A catalogue of replay
| Replay | Missing binding | Typical cause | Fix |
|---|---|---|---|
| Another chain | chainId | Field omitted, or separator cached across a fork | Use EIP-712 with a separator rebuilt when block.chainid changes |
| Another contract | verifyingContract | Shared signer across deployments | Include the verifying contract in the domain |
| Another version | name, version | Upgrade behind the same address | Bump the version on breaking changes |
| Same contract, again | Nonce | No nonce, or signature bytes used as the key | Consume a nonce per signature |
| Same contract, later | Deadline | No expiry on long-lived authorisations | Enforce a deadline; support cancellation |
| Another smart account | Account address | EIP-1271 check of the owner’s signature only | Rehash with the account address (ERC-7739) |
The cross-chain message replay case study and the Axeline Bridge research assessment show how missing bindings are found and reproduced in a review.
Conclusions
A signature authorises a hash, and replay protection consists of making every unintended context produce a different hash. EIP-712 provides the fields; replay bugs come from leaving them out, computing them once when they can change, or tracking the wrong thing as used.
The checklist is short and mechanical: domain with chain ID and verifying contract, a nonce consumed on use, a deadline, signature normalisation, a non-zero signer, and account binding for smart accounts.
Recommendations
Developers
- Use a maintained EIP-712 implementation, and include
chainIdandverifyingContractin every domain. - Consume a nonce for every signature and enforce a deadline; offer on-chain cancellation.
- Recover signers with a library that rejects malleable signatures, and reject a zero-address signer.
Smart account builders
- Bind EIP-1271 validation to the account address, for example with ERC-7739 rehashing.
Reviewers
- For every signed message type, list the contexts in which it is valid and confirm each is intended.
- Test replay explicitly: on a second deployment, on a forked chain ID, and with the alternative s value.
Bibliography
Primary sources: standards, official documentation, incident reports and papers.
- 1EIP-712: Typed structured data hashing and signing(opens in a new tab)Ethereum Improvement Proposals
- 2EIP-155: Simple replay attack protection(opens in a new tab)Ethereum Improvement Proposals
- 3EIP-2612: Permit extension for EIP-20 signed approvals(opens in a new tab)Ethereum Improvement Proposals
- 4EIP-1271: Standard signature validation method for contracts(opens in a new tab)Ethereum Improvement Proposals
- 5ERC-7739: Readable typed signatures for smart accounts(opens in a new tab)Ethereum Improvement Proposals
- 6Cryptography utilities: ECDSA, EIP712, MessageHashUtils(opens in a new tab)OpenZeppelin Contracts 5.x API
- 7SWC-121: Missing protection against signature replay attacks(opens in a new tab)Smart Contract Weakness Classification Registry
- 8SWC-117: Signature malleability(opens in a new tab)Smart Contract Weakness Classification Registry
- 9Permit2 overview(opens in a new tab)Uniswap documentation
Applied on this site
Assessments, case studies and tools that apply this publication.





