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.
Replay attacks come in two forms:
// 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 — 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);
}
EIP712 and SignatureChecker utilities.