Solidity Replay Protection
Replay protection is the rule that a signed message may be used only once. A nonce per signer is checked and bumped on each use, so a second use fails.
The first use succeeds and bumps the nonce
A fresh signature matches the current nonce. The claim lands and the nonce moves from 0 to 1.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract ReplayGuard {
mapping(address => uint256) public nonces;
mapping(address => uint256) public claims;
function signedDigest(address signer, uint256 nonce) public view returns (bytes32) {
bytes32 dataHash = keccak256(abi.encodePacked(address(this), signer, nonce));
return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", dataHash));
}
function claim(address signer, uint256 nonce, uint8 v, bytes32 r, bytes32 s) public {
require(nonce == nonces[signer], "bad nonce");
require(ecrecover(signedDigest(signer, nonce), v, r, s) == signer, "bad signature");
nonces[signer] += 1;
claims[signer] += 1;
}
}The second use of the same signature reverts
The nonce has moved on, so the old signature no longer matches the current nonce. The replay reverts.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract ReplayGuard {
mapping(address => uint256) public nonces;
mapping(address => uint256) public claims;
function signedDigest(address signer, uint256 nonce) public view returns (bytes32) {
bytes32 dataHash = keccak256(abi.encodePacked(address(this), signer, nonce));
return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", dataHash));
}
function claim(address signer, uint256 nonce, uint8 v, bytes32 r, bytes32 s) public {
require(nonce == nonces[signer], "bad nonce");
require(ecrecover(signedDigest(signer, nonce), v, r, s) == signer, "bad signature");
nonces[signer] += 1;
claims[signer] += 1;
}
}A signature for another contract does not run here
The signed data includes this contract address. A signature made for a different contract recovers a different digest and fails here.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract ReplayGuard {
mapping(address => uint256) public nonces;
mapping(address => uint256) public claims;
function signedDigest(address signer, uint256 nonce) public view returns (bytes32) {
bytes32 dataHash = keccak256(abi.encodePacked(address(this), signer, nonce));
return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", dataHash));
}
function claim(address signer, uint256 nonce, uint8 v, bytes32 r, bytes32 s) public {
require(nonce == nonces[signer], "bad nonce");
require(ecrecover(signedDigest(signer, nonce), v, r, s) == signer, "bad signature");
nonces[signer] += 1;
claims[signer] += 1;
}
}Example
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract SignedClaim {
mapping(address => uint256) public nonces;
mapping(address => uint256) public claimedAmount;
function signedDigest(address signer, uint256 amount, uint256 nonce) public view returns (bytes32) {
bytes32 dataHash = keccak256(abi.encodePacked(address(this), signer, amount, nonce));
return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", dataHash));
}
function claim(address signer, uint256 amount, uint256 nonce, uint8 v, bytes32 r, bytes32 s) public {
require(nonce == nonces[signer], "bad nonce");
require(ecrecover(signedDigest(signer, amount, nonce), v, r, s) == signer, "bad signature");
nonces[signer] += 1;
claimedAmount[signer] += amount;
}
}Key points
- Replay: reusing a valid signed message a second time to repeat its effect.
- Nonce: a counter per signer that marks which signed message is current.
- Use a nonce to make each signature valid for exactly one use.
- Domain separation: binding signed data to one contract, so a signature for one contract cannot run on another.
- Use the contract address inside the signed data to bind a signature to that contract.
In this example, a signed claim for 50 lands once, its replay reverts, and a fresh signature with nonce 1 claims 30 more, for a total of 80.
Note
the nonce must be bumped before any outside call, so a reentrant replay cannot slip through.
Try it Yourself
Edit the code, then press Run.
Running is simulated · Remix IDE
Exercise
A signature was already used once. Its nonce has been bumped. What happens if the same signature is submitted again?