What is Signature Replay?
Signature replay is a smart-contract vulnerability in which a valid cryptographic signature is accepted more than once (or on a chain it was never intended for) because the verifying contract fails to track that the signature has already been used. Because a digital signature is stateless — it just proves “this private key approved this message” — the contract itself must record that the signature was consumed. If it doesn’t, an attacker can resubmit the same signature to trigger the same action repeatedly: claiming an airdrop ten times, withdrawing funds multiple times, or re-approving a transfer.
This class of bug is one of the most common and expensive in smart-contract security. It is the on-chain cousin of a classic network replay attack, but the “replay” happens inside a single contract’s logic rather than across two chains. Combined with EIP-712 typed signatures and gasless “permit” flows, signature-replay bugs have drained millions of dollars from real protocols.
How Signature Replay Works / Technical Details
Many modern contracts let users authorize an action with a signature instead of an on-chain transaction (cheaper, gasless UX). A typical flow:
- User signs an off-chain message: “I allow the contract to transfer 100 USDC from me”
- Anyone (often a relayer) submits that signature to the contract
- The contract calls
ecrecoverto verify the signature came from the user - The contract performs the action
The problem: ecrecover only checks authenticity, not freshness. The same signature will verify as “authentic” forever. Without additional guards, step 4 can be repeated.
The Four Guards a Signature Must Carry
A robust signature-based action must bind the signature to four things, or it is replayable:
| Guard | Prevents | How |
|---|---|---|
| Nonce | Same action twice on this chain | Contract increments a per-user counter; signature must include it |
| Chain ID | Replay on other EVM chains | Include block.chainid (or EIP-712 domain chainId) in the signed data |
| Contract address | Replay on a cloned deployment | Include the verifying contract address in the signed message |
| Expiry | Replay far in the future | Include a deadline timestamp |
Drop any one of these and you open a replay window.
Example Vulnerable Pattern
// VULNERABLE: no nonce, no chainId, no expiry
function claim(bytes calldata sig) external {
address signer = recoverClaimSignature(sig);
require(signer != address(0), "bad sig");
// ❌ nothing records that this sig was used
token.transfer(signer, REWARD);
}
An attacker who obtains one valid sig can call claim in a loop until the contract is empty. The fix is a mapping(bytes32 => bool) used keyed on a hash of the signature (plus nonce/chainId/deadline) that flips to true on first use and rejects duplicates.
Notable Examples and Attack Vectors
The “Missing Nonce” Airdrop Drains
Multiple airdrop-claim contracts have been drained because the claim signature carried no nonce. A single valid claim signature could be replayed thousands of times, minting or transferring far more tokens than intended. The pattern is so common that auditors treat “signature without used-flag” as a default critical finding.
Cross-Chain Replay of Permit Signatures
EIP-2612 permit signatures and EIP-712 typed-data signatures must include the chain ID in their domain separator. Contracts that deployed the same bytecode on multiple chains without setting a chain-specific domain separator allowed a permit signed on mainnet to be replayed on an L2 (or vice versa), granting approvals the user never intended on the second chain.
The 2022 “Authorized Mint” Replay
Several NFT and token contracts let an admin “authorize” minting with an off-chain signature. When the contract failed to bind the signature to a specific token ID or a single-use nonce, a single admin signature authorized unlimited mints, which attackers exploited to mint large supplies.
How to Prevent Signature Replay
1. Always Mark Signatures as Used
Keep a mapping(bytes32 => bool) of consumed signature hashes. On every verification, require it is false, then set it to true. This is the single most important defense.
2. Use EIP-712 Typed Data with a Full Domain
EIP-712’s EIP712Domain should include name, version, chainId, and verifyingContract. This binds each signature to one contract on one chain, killing cross-chain replay.
3. Include a Per-User Nonce and Deadline
Sign each message with a strictly-increasing nonce and an expiry timestamp. The contract checks both, so a signature cannot be reused or submitted after it has expired.
4. Audit the Full Verification Path
Watch for partial-overlap replay: if signatures cover (amount, token) but not (recipient), an attacker might replay with a different recipient. Hash every variable that should be fixed.
5. Reuse Battle-Tested Libraries
OpenZeppelin’s EIP712, Nonces, and SafeERC20 implement these patterns correctly. Don’t hand-roll signature verification.
Frequently Asked Questions
Q: Isn’t ecrecover enough to verify a signature?
A: ecrecover only tells you who signed, not when or whether it was already used. You must add your own nonce, expiry, and used-flag logic on top of it.
Q: Does EIP-155 protect smart-contract signatures? A: No. EIP-155 protects raw transactions by binding the chain ID into the transaction signature. Off-chain signatures passed into contracts must include the chain ID separately (via EIP-712 domain) — otherwise they remain cross-chain replayable.
Q: How is signature replay different from reentrancy? A: Reentrancy exploits a contract calling out before updating state; signature replay exploits reusing a valid signature because state was never recorded. Both are state-management bugs, but the triggers differ.