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
Recommended Fix
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 "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import "@openzeppelin/contracts/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.