signature-replaylisted
Install: claude install-skill iktok90-design/ai-smart-contract-auditor
# Signature replay & EIP-712 detection
## When this applies
- `ecrecover` and any signature verification path
- `permit` (EIP-2612, ERC-4494) implementations
- Meta-tx / gasless flows (ERC-2771, EIP-3074 / EIP-7702 wrapping)
- Signed off-chain orders (0x, Seaport, custom)
- Signed governance votes
- Validator-set / bridge signature aggregation (see [[bridge-specialist]])
## Detection patterns
### Missing nonce (CRITICAL)
```solidity
bytes32 digest = keccak256(abi.encode(sender, amount)); // ← no nonce, infinitely replayable
require(ecrecover(digest, v, r, s) == sender);
```
### Missing chainId in domain (CRITICAL — cross-chain replay)
EIP-712 domain MUST include `chainId`. Without it, signing on Ethereum replays on Optimism/Arbitrum/etc.
```solidity
bytes32 domain = keccak256(abi.encode(EIP712_DOMAIN, name, version, /* no chainId */ , verifyingContract));
```
### Hardcoded chainId / `_DOMAIN_SEPARATOR` cached without fork-detect (HIGH)
```solidity
DOMAIN_SEPARATOR = _hashDomain(block.chainid); // ← cached in constructor, breaks after fork
```
Re-compute when `block.chainid` differs.
### Signature malleability (HIGH)
ECDSA accepts both `(r, s)` and `(r, n-s)` for valid signatures. If you use the digest as a uniqueness key, attacker can flip s and replay. Enforce `s ≤ secp256k1n/2` and `v ∈ {27, 28}`. Or use OpenZeppelin's `ECDSA.tryRecover` which already does this.
### `ecrecover` returns address(0) on invalid sig — not reverted (HIGH)
```solidity
address signer =