Skip to main content
Sigvex

Signature Replay Remediation

How to prevent signature replay attacks by binding signed messages to a nonce, deadline, chain ID, and contract address using EIP-712.

Signature Replay Remediation

Overview

Signature replay attacks occur when a signed message can be reused beyond its intended scope. The remediation binds each signature to: a per-use nonce (prevents same-chain replay), a deadline (limits the valid time window), the contract address (prevents cross-contract replay), and the chain ID (prevents cross-chain replay). EIP-712 provides a standard framework that automatically covers the chain ID and contract address via the domain separator.

Related Detector: Signature Replay

Before (Vulnerable)

function executeAction(bytes32 messageHash, bytes memory signature, uint256 amount) external {
    // Verifies authenticity but not uniqueness — replayable indefinitely
    address signer = ECDSA.recover(messageHash, signature);
    require(signer == owner, "Invalid signature");
    _execute(msg.sender, amount);
}

After (Fixed)

import "@audited/utils/cryptography/EIP712.sol";
import "@audited/utils/cryptography/ECDSA.sol";

contract SafeAuth is EIP712 {
    bytes32 private constant _ACTION_TYPEHASH =
        keccak256("Action(address recipient,uint256 amount,uint256 nonce,uint256 deadline)");

    mapping(address => uint256) public nonces;
    address public owner;

    constructor() EIP712("SafeAuth", "1") {}

    function executeAction(
        address recipient,
        uint256 amount,
        uint256 deadline,
        bytes memory signature
    ) external {
        require(block.timestamp <= deadline, "Signature expired");

        bytes32 structHash = keccak256(abi.encode(
            _ACTION_TYPEHASH,
            recipient,
            amount,
            nonces[owner]++,  // Increment nonce — one-time use
            deadline
        ));

        // _hashTypedDataV4 includes contract address and chain ID via domain separator
        bytes32 hash = _hashTypedDataV4(structHash);
        address signer = ECDSA.recover(hash, signature);
        require(signer == owner, "Invalid signature");

        _execute(recipient, amount);
    }
}

Alternative Mitigations

Signature bitmap — for use cases where sequential nonces are impractical (e.g., out-of-order multi-sig):

mapping(bytes32 => bool) public usedSignatures;

function executeAction(bytes32 messageHash, bytes memory signature) external {
    require(!usedSignatures[messageHash], "Signature already used");
    address signer = ECDSA.recover(messageHash, signature);
    require(signer == owner);
    usedSignatures[messageHash] = true;
    _execute();
}

Minimum manual protection when EIP-712 is not available:

bytes32 messageHash = keccak256(abi.encodePacked(
    block.chainid,   // Prevents cross-chain replay
    address(this),   // Prevents cross-contract replay
    msg.sender,      // Binds to specific caller
    amount,
    nonce,           // Prevents same-chain replay
    deadline
));

Common Mistakes

Nonce without chain ID — nonce prevents same-chain replay but not cross-chain replay. Both are required.

Deadline without nonce — a deadline prevents future replay but not replay within the valid time window.

Not invalidating a nonce on error — if the nonce is incremented before checking the action succeeds, and the action reverts, the nonce is consumed but no action was taken. Increment after successful execution.

References