Signature Replay
Detects signature verification without chain ID, contract address, nonce, or deadline binding, allowing valid signatures to be reused across transactions, chains, or contracts.
Signature Replay
Overview
Remediation Guide: How to Fix Signature Replay
The signature replay detector identifies signature verification logic that does not bind the signed message to a nonce, deadline, chain ID, or contract address. Without these bindings, a valid signature can be reused beyond its intended scope. Sigvex scans HIR data-flow graphs for patterns where ecrecover or EIP-712 verification occurs without a subsequent nonce-increment or timestamp check in the same control-flow path.
Signature replay attacks fall into three categories: same-chain replay (reusing a signature in a different transaction on the same contract), cross-chain replay (using a valid signature from one chain on another with the same contract address), and cross-contract replay (using a valid signature from one contract on another that accepts the same format).
Why This Is an Issue
When a contract accepts signatures as authorization without replay protection, any observer who sees a valid transaction can resubmit the same signature in a new transaction to repeat the authorized action. This allows unlimited unauthorized withdrawals, duplicate minting, or repeated execution of privileged operations — all using a single legitimately-issued signature.
Cross-chain replay is particularly insidious after blockchain forks: signatures created before the fork are mathematically valid on both chains. EIP-155 added chain ID to transaction signatures, but meta-transactions and off-chain authorization flows must include chain ID in the signed message separately.
The ERC-20 permit() front-running vulnerability allows an observer to replay a valid permit signature before the intended transaction, consuming the allowance and blocking the original flow. The Multichain exploit (July 2023, $126M) was enabled by compromised signing keys combined with insufficient replay protection.
How to Resolve
Use EIP-712 structured data signing, which automatically binds the signature to the contract address and chain ID via the domain separator. Include a monotonically incrementing nonce and a deadline in every signed message.
// Before: Vulnerable — no replay protection
function executeAction(bytes32 messageHash, bytes memory signature, uint256 amount) external {
address signer = ECDSA.recover(messageHash, signature);
require(signer == owner, "Invalid signature");
// Signature can be replayed indefinitely on any chain
_executeTransfer(msg.sender, amount);
}
// After: Fixed with EIP-712, nonce, and deadline
import "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
contract SafeAuth is EIP712 {
bytes32 private constant _EXECUTE_TYPEHASH =
keccak256("Execute(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(_EXECUTE_TYPEHASH, recipient, amount, nonces[owner]++, deadline));
bytes32 hash = _hashTypedDataV4(structHash); // Includes contract address + chain ID
address signer = ECDSA.recover(hash, signature);
require(signer == owner, "Invalid signature");
_executeTransfer(recipient, amount);
}
}
Examples
Vulnerable Code
contract VulnerableAuth {
address public owner;
// VULNERABLE: verifies signature authenticity but not uniqueness
// The same valid signature can be submitted in multiple transactions
function executeAction(
bytes32 messageHash,
bytes memory signature,
uint256 amount
) external {
address signer = ECDSA.recover(messageHash, signature);
require(signer == owner, "Invalid signature");
// No nonce, no deadline, no chain ID — unlimited replay
_executeTransfer(msg.sender, amount);
}
}
Fixed Code
// Minimum protection: chain ID + nonce + deadline (without EIP-712)
function executeAction(
uint256 amount,
uint256 nonce,
uint256 deadline,
bytes memory signature
) external {
require(block.timestamp <= deadline, "Expired");
require(nonce == userNonces[msg.sender], "Invalid nonce");
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 // Time-bounds the signature
));
address signer = ECDSA.recover(messageHash.toEthSignedMessageHash(), signature);
require(signer == owner, "Invalid signature");
userNonces[msg.sender]++;
_process(msg.sender, amount);
}
Sample Sigvex Output
{
"detector_id": "signature-replay",
"severity": "medium",
"confidence": 0.70,
"description": "Function executeAction() calls ecrecover at offset 0x4c without a subsequent nonce increment or deadline check. The recovered signature can be replayed in future transactions.",
"location": {
"function": "executeAction(bytes32,bytes,uint256)",
"offset": 76
}
}
Detection Methodology
Sigvex detects signature replay vulnerabilities through HIR data-flow analysis:
- Signature verification identification: Locates calls to
ecrecover(via theSTATICCALLto the precompile at address0x01) or EIP-712 verification patterns (calls to_hashTypedDataV4or equivalent). - Nonce check detection: Searches the function’s CFG for a storage read of a monotonically incrementing counter that is compared against a caller-supplied value, followed by a storage write incrementing that counter.
- Deadline check detection: Searches for a
TIMESTAMPopcode followed by a comparison with a function parameter or storage value. - Chain binding detection: Checks whether the signed message hash includes
CHAINIDor whether theEIP712domain separator is used.
If signature verification is present but none of nonce increment, deadline check, or chain binding is found in the same function scope, a finding is emitted.
Limitations
False positives:
- One-time-use permit flows (e.g.,
permit()) where the nonce is managed by the token contract itself rather than the calling contract may be flagged because the increment occurs in a cross-contract call not visible in bytecode analysis. - Functions that delegate nonce management to a parent contract or library may produce false positives.
False negatives:
- Replay protection implemented via a bitmap (marking used signatures as spent) rather than a counter is not detected as a nonce pattern.
- Off-chain nonce validation (where the server verifies nonce uniqueness before broadcasting) is not detectable from bytecode.
Related Detectors
- Access Control — detects missing authorization checks that signature replay can bypass
- Front-Running — detects permit front-running as a related attack pattern