Signature Replay Exploit Generator
Sigvex exploit generator that validates missing replay protection in meta-transaction and permit functions by testing same-chain, cross-chain, and cross-contract replay scenarios.
Signature Replay Exploit Generator
Overview
The signature replay exploit generator validates findings from the signature-replay detector by executing the target contract’s signature-based function under four scenarios: first use (baseline), same-chain replay, cross-chain replay (different chain ID), and cross-contract replay (different address). A properly protected contract should allow the first call and reject all three replay attempts. Any replay that succeeds confirms a specific dimension of the vulnerability.
Signature replay vulnerabilities have caused losses across multiple bridge protocols, meta-transaction relayers, and ERC-20 permit() implementations. An attacker who observes a valid signature on-chain can reuse it to transfer funds they were never authorized to touch.
Note: Exploit generation in Sigvex is for vulnerability validation purposes only.
Attack Scenario
Same-chain replay (no nonce):
- A user signs a meta-transaction: “transfer 100 tokens to
recipient”. - The victim contract processes the signature, verifying it against the message hash but not tracking used signatures or nonces.
- The attacker calls
executeMetaTx(recipient, 100, v, r, s)again with the same signature. - The contract processes it as a fresh, valid authorization.
- The attacker repeats until the signer’s balance is exhausted.
Cross-chain replay (no chain ID):
- A user signs a meta-transaction on Ethereum mainnet (chain ID 1).
- The same contract is deployed on BSC (chain ID 56). The message hash does not include
block.chainid. - The attacker submits the Ethereum signature to the BSC contract.
- The BSC contract accepts it — the signature is valid for the identical encoded message without chain binding.
Cross-contract replay (no contract address):
- A user authorizes an action on
ContractA. ContractBhas the same function signature and does not bind the message hash toaddress(this).- The attacker replays the signature against
ContractB.
Exploit Mechanics
The generator executes four independent runs with different world state configurations:
| Scenario | chain_id | contract_address | Storage slot 2 (nonce) |
|---|---|---|---|
| 1 — First use | 1 | contract_a | 0 |
| 2 — Same-chain replay | 1 | contract_a | 1 (incremented) |
| 3 — Cross-chain | 56 (BSC) | contract_a | 0 (fresh) |
| 4 — Cross-contract | 1 | contract_b | 0 (fresh) |
Storage layout:
- Slot 0: Signer address (authorized)
- Slot 1: Signer token balance (
1000) - Slot 2: Nonce for signer
- Slot 3: Chain ID
Calldata encodes: executeMetaTx(recipient, 100, v=27, r=0xAA..., s=0xBB...) using selector 0x0c53c51c.
Verdict mapping (highest-priority match wins):
- Scenario 1 succeeds AND Scenario 2 succeeds → same-chain replay (confidence 0.95)
- Scenario 1 succeeds AND Scenario 3 succeeds → cross-chain replay (confidence 0.95)
- Scenario 1 succeeds AND Scenario 4 succeeds → cross-contract replay (confidence 0.90)
- All replay scenarios revert → fully protected
The generated PoC demonstrates all three attack variants with commentary explaining EIP-712 compliance:
// VULNERABLE: No nonce, no chain ID
function executeMetaTx(address recipient, uint256 amount,
uint8 v, bytes32 r, bytes32 s) external {
bytes32 hash = keccak256(abi.encodePacked(recipient, amount));
address signer = ecrecover(hash, v, r, s);
// Signature can be replayed indefinitely!
balances[signer] -= amount;
balances[recipient] += amount;
}
// SECURE: EIP-712 with nonce + chain ID + contract address
bytes32 structHash = keccak256(abi.encode(
TYPE_HASH,
recipient,
amount,
nonces[signer]++ // prevents same-chain replay
));
bytes32 digest = keccak256(abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR // encodes chainId + address(this)
));
address recovered = ecrecover(digest, v, r, s);
Remediation
- Detector: Signature Replay Detector
- Remediation Guide: Signature Replay Remediation
All three replay protection elements are required simultaneously:
- Nonce: Increment
nonces[signer]++after each valid signature use. - Chain ID: Include
block.chainidin the signed message (EIP-712 domain separator). - Contract address: Include
address(this)in the domain separator.
Use OpenZeppelin’s EIP712 base contract, which handles the domain separator automatically:
import "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
contract SafeMetaTx is EIP712 {
mapping(address => uint256) public nonces;
constructor() EIP712("MyContract", "1") {}
function executeMetaTx(
address recipient, uint256 amount,
uint8 v, bytes32 r, bytes32 s
) external {
bytes32 structHash = keccak256(
abi.encode(TYPE_HASH, recipient, amount, nonces[msg.sender]++)
);
address signer = ECDSA.recover(_hashTypedDataV4(structHash), v, r, s);
require(signer == msg.sender, "Invalid signature");
// proceed with transfer
}
}
References
- EIP-712: Typed Structured Data Hashing and Signing
- Multiple bridge hacks via cross-chain signature replay (Ronin Bridge 2022: $625M)
- SWC-121: Missing Protection Against Signature Replay Attacks
- OpenZeppelin EIP712 Documentation