Signature Replay Attack

Many smart contracts use off-chain signatures to authorize on-chain actions without requiring gas from the signer. If a signature is accepted without a nonce, expiry, or domain separator, an attacker can reuse the same signature indefinitely — replaying the authorized action over and over.

How it works

Replay attacks come in two forms:

  • Same-chain replay: Reusing a signature on the same contract to repeat an action (e.g., withdrawing funds multiple times).
  • Cross-chain replay: Using a signature valid on Ethereum to authorize the same action on a forked chain (e.g., BSC).

Vulnerable pattern

// VULNERABLE — signature can be replayed indefinitely
function withdraw(uint amount, bytes memory sig) public {
    bytes32 hash = keccak256(abi.encode(msg.sender, amount));
    require(recoverSigner(hash, sig) == owner);
    payable(msg.sender).transfer(amount);
    // No nonce, no expiry — same sig works forever!
}

Safe pattern (EIP-712 with nonce)

// SAFE — EIP-712 structured data with nonce and domain separator
mapping(address => uint) public nonces;

bytes32 public DOMAIN_SEPARATOR;
bytes32 constant WITHDRAW_TYPEHASH = keccak256(
    "Withdraw(address user,uint256 amount,uint256 nonce,uint256 deadline)"
);

function withdraw(uint amount, uint deadline, bytes memory sig) public {
    require(block.timestamp <= deadline, "Expired");
    bytes32 structHash = keccak256(abi.encode(
        WITHDRAW_TYPEHASH, msg.sender, amount, nonces[msg.sender]++, deadline
    ));
    bytes32 digest = keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, structHash));
    require(recoverSigner(digest, sig) == owner);
    payable(msg.sender).transfer(amount);
}

Real-world exploits

  • Poly Network (2021) — signature replay across chains contributed to the $611 million exploit.
  • Multiple NFT marketplaces have suffered replay attacks where cancelled listings were reused.

How to prevent it

  • Always include a nonce in signed messages and increment it on use.
  • Always include a deadline (expiry timestamp) in signed messages.
  • Use EIP-712 domain separators that encode the chain ID and contract address to prevent cross-chain and cross-contract replays.
  • Use OpenZeppelin's EIP712 and SignatureChecker utilities.
← Back to Glossary