Replay Attacks

Smart Contract Vulnerability Deep Dive

JohnnyTime
JohnnyTime · Updated June 24, 2026
24 min read
Total Stolen $68,000
Last Attack Jul 03, 2023
Latest Victim AzukiDAO

Summarize with AI

Replay Attacks in Solidity: How Hackers Reuse Signatures to Drain Smart Contracts

Imagine handing someone a signed blank check, expecting them to cash it exactly once. Now imagine they photocopy that exact same check and cash it a hundred times over. That is the devastating reality of a smart contract replay attack.

Replay attacks have quietly siphoned over $20 million from a single protocol, facilitated the theft of 40,000 ETC from major exchanges, and left vulnerabilities exposed across more than 1,700 live contracts. If your smart contract verifies signatures without rock-solid replay protection, every valid signature becomes a hacker’s unlimited license to steal.

In this deep dive, we'll guide you through the mechanics of replay attacks - from the most basic vulnerable patterns to sophisticated, real-world exploits. Plus, we'll arm you with production-ready prevention code you can deploy to secure your dApps today.

What Exactly Is a Replay Attack?

A replay attack happens when a perfectly valid digital signature (or transaction) is intercepted and resubmitted. This tricks the contract into executing the same action multiple times - or executing it on a completely different chain - without the original signer ever approving the repeated action.

In Solidity, this nightmare scenario usually kicks off when a contract uses ecrecover to verify off-chain signatures, but completely forgets to keep a receipt of which signatures have already been used. The attacker doesn't have to forge an expert-level fake; they just recycle a key that already unlocks the door.

The VIP Concert Ticket Analogy 🎟️

Think of a replay attack like going to an exclusive concert. Normally, the security guard scans your VIP ticket, it gets marked "used" in the system, and you walk in. But what if the scanner is broken and never marks it as used?

You could walk in, slip the ticket through the fence to your friend, they walk in, hand it to another friend, and suddenly the entire venue is packed - all off one legitimate ticket. Replay attacks operate on this exact flaw: one valid signature equals endless unauthorized transactions.


Why Replay Attacks Are So Dangerous

These aren't just theoretical bugs; replay attacks are responsible for some of the most eye-watering hacks in blockchain history:

$0M+
Largest Single Incident
0
Vulnerable Contracts Found
2016–2025
Active Threat Period
Attack Year Impact
Optimism OP Token Theft 2022 20M OP tokens (~$17.6M) stolen via cross-chain replay
ETH/ETC Chain Split 2016 40,000+ ETC drained from exchanges
ETHW Omni Bridge 2022 200 ETHW stolen + 37% token price crash
Nomad Bridge 2022 $190M drained (copy-paste replay exploit)

A 2025 academic study analyzed 15,383 signature-verification contracts and found 1,739 contained replay vulnerabilities -- holding approximately $4.76 million in active assets. The problem is far more widespread than most developers realize.

What Makes Replay Attacks Unique

Unlike reentrancy or flash loan attacks that exploit execution flow, replay attacks exploit data validity. The signature is legitimate. The message is authentic. The signer did authorize it -- just not a second, third, or hundredth time. This makes replay attacks harder to detect in audits because the code "works correctly" for the first use.


Want to exploit replay attacks yourself in a safe lab? The Smart Contract Hacking course includes hands-on signature replay exercises where you'll drain vulnerable contracts -- exactly like real security researchers do.


The Optimism OP Token Theft: A $20M Replay Masterclass 💥

The Optimism OP token theft wasn't just another bug - it was an absolute masterclass in cross-chain replay exploitation that shook L2 networks to their cores.

The Setup: Missing on Arrival

Flashback to May 2022: The Optimism Foundation set aside a whopping 20 million OP tokens for market maker Wintermute. To receive them, Wintermute handed over a Gnosis Safe multisig address. The catch? That specific address existed on Ethereum L1, but nobody had deployed it to Optimism L2 yet.

Believing everything was fine, the Optimism team successfully sent the 20 million OP in three transactions. Suddenly, those tokens were chilling at an address on Optimism that literally no one controlled.

The Attack: June 5, 2022

An observant attacker spotted a massive vulnerability: the old transactions that originally deployed the Gnosis Safe proxy factory on Ethereum L1 predated EIP-155. That meant the transaction signatures were missing a crucial piece of data: the chainId.

Here's the exact playbook they used:

Step The Attacker's Move The "Why It Worked" Magic
1 Snatched the Gnosis Safe factory deployment tx from Ethereum mainnet Transaction was in a legacy format, naked without a chainId.
2 Copied and ran that raw deployment tx directly on Optimism Identical bytecode + identical transaction data = identical deployment.
3 Spammed calls to the factory, incrementing the nonce Deterministic CREATE operations always produce the exact same address at the exact same nonce.
4 Spawned the exact target multisig address on Optimism The attacker was now the undisputed captain of that L2 address.
5 Scooped up all 20M OP tokens With full wallet control, it was payday.

Mind-blowing fact: The attacker didn't crack any complex cryptography or glitch out Optimism's network. They simply replayed a totally valid Ethereum transaction on a different chain, entirely because the signature had no idea which chain it belonged to.

The Aftermath

The attacker decided to play semi-nice, returning 17 million OP tokens but pocketing 2 million (around $1.4M at the time) as a self-awarded bug bounty. The fallout instantly forced developers to scramble, making EIP-155 replay protection standard practice across all cross-chain deployments.


How Replay Attacks Actually Work: A Step-by-Step Breakdown

If you want to stop a replay attack, you have to think like an attacker. Here is how a standard signature replay plays out.

The Exploit Flow

Step What Happens The State of the Contract
1 The true owner signs a message saying: "Send 1 ETH to Bob" The signature is totally valid.
2 A relayer hands that signature to the smart contract Contract checks it: Match! It sends 1 ETH.
3 The greedy attacker scrapes that exact signature right out of the on-chain data Signature is still valid (because no nonce was tracked).
4 The attacker fires the exact same signature back at the contract Contract checks it: Match! It sends another 1 ETH.
5 The attacker puts it on a loop until the vault is running on fumes There is zero logic to say "Hey, we already used this!"

The Attack Phases

1
Tap to reveal
Attacker Monitors On-Chain Activity

The attacker watches for transactions that include off-chain signatures -- such as meta-transactions, permit approvals, or gasless transfers. These signatures are publicly visible in calldata.

2
Tap to reveal
Signature Extraction

The attacker extracts the signature and the signed message parameters from the transaction calldata. Since everything is on-chain, this requires zero special access.

3
Tap to reveal
Replay Submission

The attacker submits the identical signature and parameters to the same contract (same-chain replay), a different contract (cross-contract replay), or the same contract on another chain (cross-chain replay).

4
Tap to reveal
Repeated Exploitation

Without nonce tracking, chain ID binding, or contract address binding, the signature passes verification every time. The attacker loops until funds are drained or the desired action is exhausted.


The Replay Attack Multiverse

Replay attacks aren't a one-trick pony. Hackers have evolved multiple variants, each completely destroying a different oversight in signature verification.

🔁
Same-Chain Replay
"The exact same signature is relentlessly blasted back at the exact same contract because the dev forgot to track what's been used."
Root cause: Missing nonce | Most common variant
🔗
Cross-Chain Replay
"A signature verified on one blockchain is teleported and executed on another chain where an identical contract lives."
Root cause: Missing chainId | $20M+ in losses
📄
Cross-Contract Replay
"A user signs a message for Contract A, but the attacker drops it into Contract B, totally tricking it into authorizing the action."
Root cause: Missing address(this) | 493+ instances found

Beyond the big three, there are two deeper variants that keep top-tier security auditors up at night:

Signature Malleability Replay - Cryptography can be weird. ECDSA signatures have a quirky mathematical trait: for every valid (v, r, s) signature, an attacker can magically flip a bit of math to generate a second valid tuple (v', r, n-s). If the smart contract checks signatures strictly by their raw byte data instead of their message hash, an attacker prints a brand new key for free - zero private key hacking required.

State Management Spaghetti - Even when devs try to track used signatures natively, they botch the execution. They build a custom mapping(bytes32 => bool) usedSignatures, but ultimately fail to reliably flip the boolean to true after execution. The door is slammed shut, but the lock doesn't click.

Replay attack diagram - One signature, many doors: where a single valid signature gets replayed
One signature, many doors: where a single valid signature gets replayed

Want to rip apart vulnerable vaults and execute these precise replay exploits yourself? The Smart Contract Hacking course lets you go hands-on with replay and signature vulnerabilities in a highly realistic sandbox environment, mentored by JohnnyTime (12+ years in cybersecurity) and Trust (#1 Code4rena warden).


Code Breakdown: The "Please Hack Me" Vault

Let’s dissect the most common, fundamental flavor of a replay vulnerability: a signature-based withdrawal with completely missing nonce protection.

Caution: This contract is completely unsafe by design. Do not copy-paste this into your project.

The Vulnerable Signature Verification

// VULNERABLE CONTRACT - DO NOT USE IN PRODUCTION
contract VulnerableVault {
    address public owner;

    constructor() payable {
        owner = msg.sender;
    }

    // Anyone can call this with a valid owner signature
    function withdraw(address _to, uint256 _amount, bytes memory _sig) external {
        // Build message hash -- MISSING: nonce, chainId, contract address
        bytes32 msgHash = keccak256(abi.encodePacked(_to, _amount));
        bytes32 ethHash = keccak256(
            abi.encodePacked("\x19Ethereum Signed Message:\n32", msgHash)
        );

        // Recover signer from signature
        (uint8 v, bytes32 r, bytes32 s) = splitSignature(_sig);
        address signer = ecrecover(ethHash, v, r, s);
        require(signer == owner, "Invalid signature");

        // Transfer funds -- no record that this signature was used!
        (bool sent, ) = _to.call{value: _amount}("");
        require(sent, "Transfer failed");
    }

    function splitSignature(bytes memory sig) internal pure
        returns (uint8, bytes32, bytes32)
    {
        require(sig.length == 65, "Invalid sig length");
        bytes32 r;
        bytes32 s;
        uint8 v;
        assembly {
            r := mload(add(sig, 32))
            s := mload(add(sig, 64))
            v := byte(0, mload(add(sig, 96)))
        }
        return (v, r, s);
    }

    receive() external payable {}
}

Why Is This Vulnerable?

The problem is what's missing from the signed message:

  1. No nonce -- same signature works unlimited times on this contract

  2. No block.chainid -- signature works on every EVM chain

  3. No address(this) -- signature works on every copy of this contract

  4. No deadline -- signature is valid forever

  5. Raw ecrecover -- doesn't reject malleable signatures

The owner signs "Send 1 ETH to Alice" once. That signature can drain the entire vault.


Replay Attack Exploit Walkthrough

Here's how an attacker exploits the vulnerable vault above.

// ATTACKER CONTRACT - Educational purposes only
interface IVulnerableVault {
    function withdraw(address _to, uint256 _amount, bytes memory _sig) external;
}

contract ReplayAttacker {
    IVulnerableVault public target;

    constructor(address _target) {
        target = IVulnerableVault(_target);
    }

    // Attacker calls this with a signature they observed on-chain
    function drainVault(
        address _to,
        uint256 _amount,
        bytes memory _capturedSig
    ) external {
        // Replay the same valid signature until vault is empty
        while (address(target).balance >= _amount) {
            target.withdraw(_to, _amount, _capturedSig);
        }
    }
}

The Anatomy of the Exploit

  1. Stalk the Chain - The attacker lurks in the mempool or on-chain history, waiting for a legitimate user to submit a valid withdraw() transaction.

  2. Scrape the Data - They casually scoop up the _to, _amount, and _sig parameters right out of the transaction’s public calldata. No dark magic required.

  3. Mash the Replay Button - The attacker forcefully hits withdraw() over and over, feeding the contract its exact same valid signature.

  4. Drain to Zero - Since the contract’s brain has amnesia about past actions, it happily verifies the signature every single time, bleeding funds until it hits absolute zero.

Notice that the attacker didn’t need to hack any private keys, crack ECDSA math, or forge a single bit of data. They completely ruined the vault solely by recycling something that was authorized just a few blocks ago.

{
  "title": "🎬 Signature replay: one valid signature drains the whole vault",
  "stage": { "width": 860, "height": 400 },
  "nodes": [
    { "id": "vault", "label": "The Vault", "role": "verifies via ecrecover", "emoji": "🏦", "x": 345, "y": 40, "color": "cyan" },
    { "id": "owner", "label": "Owner EOA", "role": "signs once, off-chain", "emoji": "🧑‍💼", "x": 60, "y": 250, "color": "green" },
    { "id": "attacker", "label": "Attacker", "role": "scrapes the signature", "emoji": "🧑‍💻", "x": 630, "y": 250, "color": "red" }
  ],
  "links": [
    { "from": "owner", "to": "vault" },
    { "from": "attacker", "to": "vault" },
    { "from": "vault", "to": "attacker" }
  ],
  "nets": [
    { "id": "vault", "label": "Vault Balance" },
    { "id": "atk", "label": "Attacker Take" }
  ],
  "legend": [
    { "cls": "call", "label": "contract call" },
    { "cls": "token", "label": "ETH transfer" },
    { "cls": "sig", "label": "signature / state write" },
    { "cls": "fail", "label": "reverted / blocked" }
  ],
  "scenarios": {
    "Vulnerable (no nonce)": [
      { "note": "The owner signs <b>\"withdraw 1 ETH\"</b> and the relayer posts it once. The vault verifies the signature and pays out — totally legitimate.", "hi": ["owner", "vault"], "bal": { "vault": "10 ETH", "atk": "0" }, "net": { "vault": "9 ETH", "atk": "0" }, "chip": { "from": "owner", "to": "vault", "label": "withdraw 1 ETH (sig)", "cls": "sig" } },
      { "note": "The signature sits in public calldata. The attacker <b>scrapes it</b> — it's still valid, because nothing recorded that it was used.", "hi": ["attacker"], "chip": { "from": "attacker", "to": "vault", "label": "replay same sig", "cls": "call" } },
      { "note": "The vault re-runs <b>ecrecover</b>, sees the owner's valid signature, and sends <b>another 1 ETH</b> — this time to the attacker.", "tone": "bad", "hi": ["vault", "attacker"], "bal": { "vault": "8 ETH", "atk": "2 ETH" }, "net": { "vault": "8 ETH", "atk": "2 ETH" }, "chip": { "from": "vault", "to": "attacker", "label": "1 ETH", "cls": "token" } },
      { "note": "Nothing marks the signature as spent, so the attacker just loops the replay until the vault hits <b>zero</b>.", "tone": "bad", "hi": ["vault", "attacker"], "bal": { "vault": "0 ETH", "atk": "10 ETH" }, "net": { "vault": "0 ETH", "atk": "10 ETH" }, "chip": { "from": "vault", "to": "attacker", "label": "…drained", "cls": "token" } }
    ],
    "Fixed (EIP-712 + nonce)": [
      { "note": "The owner signs an EIP-712 message that bundles a <b>nonce</b>, the <b>chainId</b> and the vault <b>address</b>. The first withdrawal consumes nonce #0.", "hi": ["owner", "vault"], "bal": { "vault": "10 ETH", "atk": "0" }, "net": { "vault": "9 ETH", "atk": "0" }, "chip": { "from": "owner", "to": "vault", "label": "withdraw (nonce 0)", "cls": "sig" } },
      { "note": "The attacker replays the identical signature. But the vault now expects <b>nonce #1</b>, so the recovered message no longer matches — it <b>reverts</b>.", "tone": "ok", "hi": ["attacker", "vault"], "chip": { "from": "attacker", "to": "vault", "label": "replay → revert ✋", "cls": "fail" } },
      { "note": "One signature equals exactly <b>one</b> withdrawal. The drain is impossible.", "tone": "ok", "hi": ["vault"], "net": { "vault": "9 ETH", "atk": "0" } }
    ]
  }
}

How to Crush Replay Attacks: Battle-Tested Defenses

Multiple layers of defense work together to make signatures single-use and context-bound.

Effectiveness95/100

What it does: Binds every signature to a specific contract name, version, chainId, and verifying contract address. This single mechanism prevents both cross-chain and cross-contract replay.

When to use: Every contract that verifies off-chain signatures. This is the industry standard.

Limitation: Must recalculate the domain separator on chain forks (don't cache block.chainid at deployment). OpenZeppelin's EIP712 contract handles this automatically.

Effectiveness90/100

What it does: Assigns an incrementing counter to each signer. Every signature includes the current nonce value. After use, the nonce increments, invalidating the old signature permanently.

When to use: All signature-based operations. Use OpenZeppelin's Nonces contract for a battle-tested implementation.

Limitation: Sequential nonces require in-order execution. For unordered execution, consider Uniswap's Permit2 bitmap nonce pattern.

Effectiveness75/100

What it does: Adds a deadline timestamp to the signed message. The contract rejects signatures where block.timestamp > deadline.

When to use: As an additional layer alongside nonces. Especially important for permit/approval signatures where stale signatures can be exploited at the worst possible moment.

Limitation: Not sufficient alone -- an attacker can still replay within the deadline window. Always combine with nonces.

Effectiveness60/100

What it does: Maintains a mapping(bytes32 => bool) of used message hashes. After verification, the hash is marked as consumed.

When to use: Simple one-time-use signature systems (e.g., NFT whitelist minting).

Limitation: Tracking by signature bytes instead of message hash is vulnerable to signature malleability. Always track by the message hash, never by the raw signature bytes. Nonces are generally preferred.

🔒
"Use EIP-712 + Nonces + Deadline. Always."
This trio forms the industry-standard defense against all replay attack variants. OpenZeppelin provides battle-tested implementations of all three. Don't roll your own.
Replay attack diagram - One signature, ten submissions: what a nonce changes
One signature, ten submissions: what a nonce changes

Replay Attack Secure Code Example

Here's the production-ready secure implementation using OpenZeppelin's EIP712 and Nonces contracts.

// SECURE CONTRACT - Production-ready
// Uses OpenZeppelin's EIP712, ECDSA, and Nonces for complete replay protection

import "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/utils/Nonces.sol";

contract SecureVault is EIP712, Nonces {
    using ECDSA for bytes32;

    // STEP 1: Define the typed struct for EIP-712
    bytes32 public constant WITHDRAW_TYPEHASH = keccak256(
        "Withdraw(address to,uint256 amount,uint256 nonce,uint256 deadline)"
    );

    address public owner;

    constructor() payable EIP712("SecureVault", "1") {
        owner = msg.sender;
    }

    function withdraw(
        address _to,
        uint256 _amount,
        uint256 _deadline,
        bytes calldata _signature
    ) external {
        // STEP 2: CHECKS - Verify deadline hasn't passed
        require(block.timestamp <= _deadline, "Signature expired");

        // STEP 3: EFFECTS - Consume nonce atomically (prevents replay)
        // _useNonce returns current nonce and increments it
        bytes32 structHash = keccak256(
            abi.encode(
                WITHDRAW_TYPEHASH,
                _to,
                _amount,
                _useNonce(owner),   // Nonce consumed here
                _deadline
            )
        );

        // STEP 4: Build EIP-712 digest with domain separator
        // Includes: contract name, version, chainId, address(this)
        bytes32 digest = _hashTypedDataV4(structHash);

        // STEP 5: Recover and verify signer
        address signer = digest.recover(_signature);
        require(signer == owner, "Invalid signer");

        // STEP 6: INTERACTIONS - Transfer funds
        (bool sent, ) = _to.call{value: _amount}("");
        require(sent, "Transfer failed");
    }

    receive() external payable {}
}

Security Features at a Glance

Feature Protection How
EIP-712 Domain Separator Cross-chain + cross-contract replay chainId + address(this) in domain
Per-user nonce Same-chain replay _useNonce(owner) increments atomically
Deadline parameter Stale signature exploitation block.timestamp <= _deadline check
OpenZeppelin ECDSA Signature malleability Enforces low-s value requirement
Typed data hashing Blind signing / phishing Wallets display human-readable signing prompts

This implementation would have prevented the Optimism OP token theft, the ETH/ETC exchange drains, and every signature replay finding in Code4rena and Sherlock audit contests.


Advanced Replay Attack Patterns

Beyond the basic patterns, sophisticated replay attacks target complex DeFi architectures.

ERC-2612 Permit Replay

The permit() function in ERC-2612 lets users approve token spending via signatures instead of on-chain transactions. While the standard includes nonces, implementations sometimes fail:

  • Stale permits without deadlines -- A signed permit with deadline = type(uint256).max never expires. If the user's nonce hasn't been consumed, an attacker holding the signature can use it months later at the worst possible price.

  • Cross-contract permit replay -- If the EIP-712 domain separator is missing or incorrectly implemented, a permit for Token A can be replayed on Token B.

Meta-Transaction Forwarder Replay

Gasless transaction systems (EIP-2771) forward signed messages on behalf of users. The critical vulnerability: if the forwarder hashes the request without a nonce, any relayer -- or observer -- can replay the same request unlimited times.

In December 2023, an even more dangerous pattern was discovered: contracts implementing both ERC-2771 and Multicall were vulnerable to address spoofing. An attacker could craft malicious calldata within a forwarded request, using Multicall's delegatecall to manipulate _msgSender() resolution. This affected OpenZeppelin, ThirdWeb, and multiple token contracts.

Hard Fork Chain-Split Replay

When a blockchain forks without replay protection, every transaction on one chain is valid on the other. This is how 40,000+ ETC was drained from exchanges after the 2016 DAO fork, and how 200 ETHW was stolen via the Omni Bridge just days after the 2022 Ethereum Merge.

EIP-155 was created specifically to solve this by embedding chainId into transaction signatures, but legacy contracts and deployment transactions may still lack this protection.

Vulnerable

Raw ecrecover

No nonce, no chainId, no contract address in the hash. Accepts malleable signatures. Every signature is replayable everywhere, forever.

Partial Defense

Manual Nonce + chainId

Prevents same-chain replay and cross-chain replay, but missing EIP-712 structured typing means blind signing risk and potential hash collisions from abi.encodePacked.

Recommended

EIP-712 + Nonces + ECDSA

OpenZeppelin's full stack: domain separator, atomic nonce consumption, deadline, malleability protection, and human-readable wallet prompts. Defense-in-depth.


These advanced patterns separate junior auditors from senior researchers. The Smart Contract Hacking course covers replay attacks alongside reentrancy, flash loans, oracle manipulation, and more. Join 2,000+ security researchers in our Discord community.


Common Misconceptions

?

"Adding a nonce prevents all replay attacks."

Tap to reveal
MYTH

A nonce alone prevents same-chain replay but NOT cross-chain or cross-contract replay. You also need block.chainid and address(this) in the signed data. EIP-712 domain separators bundle all three protections.

?

"EIP-155 protects smart contract signatures from replay."

Tap to reveal
MYTH

EIP-155 only protects transaction-level signatures (the raw ETH transaction). It does NOT protect application-level signatures verified via ecrecover inside smart contracts. You need EIP-712 domain separators for that.

?

"Tracking used signature bytes in a mapping prevents replay."

Tap to reveal
MYTH

Due to ECDSA signature malleability, an attacker can produce a second valid signature from any existing one by flipping the s value. Track by message hash instead, or better yet, use nonces which inherently prevent replay regardless of malleability.

?

"OpenZeppelin's ECDSA library alone prevents replay attacks."

Tap to reveal
FACT (partially)

OpenZeppelin's ECDSA library prevents signature malleability (by enforcing low-s values) and rejects invalid signatures. But it does NOT add nonces, chain binding, or contract binding. You need the full EIP712 + Nonces stack for complete replay protection.


Replay attacks frequently intersect with other smart contract vulnerability classes, and understanding these connections is critical for comprehensive security.

Replay attacks share a common DNA with Call/Delegatecall Attacks -- both exploit the dangers of how contracts interact with external addresses and handle message data. A delegatecall combined with a replayed signature is a particularly devastating combination.

Access Control Attacks can amplify replay attack impact dramatically. If a function lacks proper access controls AND its signatures are replayable, the blast radius multiplies. The 2023 Bybit incident ($1.5B) demonstrated how signature manipulation combined with access control failures can cause catastrophic losses.

Flash Loan Attacks can also fund replay attack exploitation at scale, allowing an attacker to use flash-loaned capital alongside replayed signatures within a single atomic transaction.


Test Your Replay Attack Knowledge

Test Your Replay Attack IQ

5 questions. How well do you really know this attack?

Question 1 of 5

Frequently Asked Questions About Replay Attacks

A replay attack in blockchain occurs when a valid signature or transaction is captured and resubmitted to execute the same action multiple times without authorization. In smart contracts, this typically happens when off-chain signatures lack a nonce (unique counter) to prevent reuse. The attacker doesn't forge anything -- they simply replay something that was legitimately signed.

The industry-standard prevention uses three mechanisms together: (1) EIP-712 domain separators that bind signatures to a specific chain and contract address, (2) per-user nonces that make each signature single-use, and (3) signature deadlines that expire stale signatures. OpenZeppelin provides production-ready implementations of all three via their EIP712, Nonces, and ECDSA contracts.

A replay attack reuses valid data (signatures or transactions) to repeat an authorized action without permission. A reentrancy attack re-enters a function during execution before state updates complete. Replay exploits data validity; reentrancy exploits execution flow. Both can drain funds, but they require completely different prevention techniques.

EIP-712 is the Ethereum standard for typed structured data hashing and signing. It creates a "domain separator" -- a unique hash containing the contract's name, version, chainId, and verifyingContract address. This means a signature created for one contract on one chain cannot be reused on any other contract or chain. It also enables wallets to display human-readable signing prompts instead of opaque hex hashes.

Yes. Cross-chain replay attacks are one of the most dangerous variants. If a signature doesn't include the chainId, it's valid on every EVM-compatible chain where the same contract (or contract address) exists. The 2022 Optimism OP token theft ($20M) was a cross-chain replay attack, and the 2016 ETH/ETC split caused widespread exchange losses from the same vector.

EIP-155 was created after the 2016 Ethereum/Ethereum Classic fork to prevent transaction-level replay attacks across forked chains. It embeds the chainId into raw transaction signatures, making them chain-specific. However, EIP-155 only protects transactions -- it does NOT protect application-level signatures verified via ecrecover inside smart contracts. For that, you need EIP-712.


Quick Reference: Replay Attack Prevention Checklist

Before deploying any contract that verifies signatures, ensure:

  • Nonce tracking -- Each signer has an incrementing nonce included in the signed data

  • Chain ID binding -- block.chainid is part of the signature hash (via EIP-712 domain separator)

  • Contract address binding -- address(this) is part of the signature hash (via EIP-712 domain separator)

  • Signature deadline -- block.timestamp <= deadline check before processing

  • OpenZeppelin ECDSA -- Using ECDSA.recover() instead of raw ecrecover (prevents malleability)

  • EIP-712 typed data -- Structured hashing with domain separator (not raw keccak256 + abi.encodePacked)

  • Zero-address check -- Recovered signer is validated against address(0)

  • Domain separator recalculation -- Not caching chainId at deployment (handles chain forks)

  • Nonce consumption before external calls -- Following CEI pattern for nonce updates


Take Your Security Skills to the Next Level

Understanding replay attacks is just the beginning. To become a professional smart contract auditor, you need hands-on experience exploiting these vulnerabilities in real scenarios.

The Smart Contract Hacking course delivers:

  • 320+ videos covering replay attacks, reentrancy, flash loans, oracle manipulation, and more

  • 40+ hands-on exercises exploiting and securing real contracts

  • Expert instruction from JohnnyTime, Trust (#1 Code4rena warden), and Pashov

  • 2,000+ member Discord for support and job opportunities

  • SSCH Certification to validate your expertise

Our students win audit contests, land security jobs, and earn significant bug bounties. See their success stories.

Start Your Security Researcher Journey

Sources and editorial notes

Reviewed by JohnnyTime. Last updated .

Incident database

Real Replay Attacks hacks to study

A stable selection of high-signal incidents linked to this attack class, ordered by reported loss and recency.

Master Replay Attacks in a safe lab

Practice the exploit path, debug the vulnerable code, and learn the prevention workflow auditors use in real reviews.

Exploit setup Root-cause tracing Patch review
Practice Replay Attacks Free Trial