Smart-contract security & auditing

signature replay attack

A signature replay attack reuses a validly signed message in a context where it was never meant to be valid again. An off-chain signature is like a signed check: it authorizes an action, and a contract that accepts signatures (for gasless approvals, meta-transactions, or off-chain orders) must make sure each check can be cashed exactly once, in exactly one place. If the signed data does not pin down which contract, which chain, and which single use it is for, an attacker can submit the same signature twice, or on a different chain, and the contract will honor it again.

There are three distinct replay surfaces, each closed by binding the right value into the signed message. Reusing the same signature against the same contract is stopped by a per-user nonce that the contract marks as consumed. Replaying across chains, for example after a chain splits or onto a fork that shares the same accounts, is stopped by including the chain id. Replaying a signature meant for one contract against another is stopped by including the verifying contract's own address. The EIP-712 typed-data standard packages chain id, contract address, and a domain string into a single domain separator precisely so all three are bound at once.

A subtler trap is ECDSA signature malleability. For any valid signature (r, s) there is a second valid signature (r, n minus s) on the same message, where n is the curve order. If a contract uses the raw signature bytes as the uniqueness key, for instance recording the signature itself as used, an attacker simply flips s to produce a different-looking but equally valid signature and replays the action. The fix is to key uniqueness on a nonce or the message hash rather than the signature, and to require the canonical low-s form (as OpenZeppelin's ECDSA library does).

bytes32 digest = keccak256(abi.encode(
    msg.sender,
    amount,
    nonces[msg.sender]++,   // single-use
    block.chainid,          // chain-bound
    address(this)           // contract-bound
));
require(ECDSA.recover(digest, sig) == signer, "bad sig");

Bind nonce, chain id, and contract into the signed digest.

ECDSA is malleable: for every valid (r, s) there is a second valid (r, n minus s). If you mark a signature as used by its own bytes, an attacker flips s and replays. Bind uniqueness to a nonce, not to the raw signature.

Also called
replay attack重放攻擊