# signature-replay-analysis

Detects signature replay vulnerabilities in smart contracts — affecting 19.63% of signature-using contracts. Covers five replay types (same-chain, cross-chain, cross-contract, nonce-skip, expired-signature), EIP-712 domain separator verification, nonce management analysis, ecrecover edge cases (address(0), malleability, s-value), permit/permit2 safety, ERC-1271 contract wallet support, and meta-transaction security. Use when auditing contracts with ecrecover, ECDSA, EIP-712, permit, meta-transactions, multi-sig, or any off-chain signature verification.

- **Kind:** skill
- **Source:** https://github.com/quillai-network/qs_skills
- **Page:** https://forefy.com/skills/289da40a-53ee-4414-b94f-5660265b817c
- **API (JSON + files):** https://forefy.com/api/asr/289da40a-53ee-4414-b94f-5660265b817c

---

## SKILL.md

---
name: signature-replay-analysis
description: Detects signature replay vulnerabilities in smart contracts — affecting 19.63% of signature-using contracts. Covers five replay types (same-chain, cross-chain, cross-contract, nonce-skip, expired-signature), EIP-712 domain separator verification, nonce management analysis, ecrecover edge cases (address(0), malleability, s-value), permit/permit2 safety, ERC-1271 contract wallet support, and meta-transaction security. Use when auditing contracts with ecrecover, ECDSA, EIP-712, permit, meta-transactions, multi-sig, or any off-chain signature verification.
---

# Signature & Replay Analysis

Detect vulnerabilities where **cryptographic signatures can be reused**, replayed across chains/contracts, or exploited through implementation flaws. Research shows 19.63% of Ethereum contracts using signatures contain replay vulnerabilities.

## When to Use

- Auditing contracts that verify signatures (`ecrecover`, ECDSA, EIP-712)
- Reviewing ERC-20 `permit()` / Uniswap Permit2 implementations
- Analyzing meta-transaction / gasless relay systems
- Verifying multi-sig signature aggregation
- Checking off-chain order books or signed message execution

## When NOT to Use

- Contracts without any signature verification
- Pure on-chain access control (use semantic-guard-analysis)
- Token standard compliance (use external-call-safety)

## Core Concept: The Signature Trust Model

A signature proves that a specific private key holder authorized a specific action. For this to be secure, the signature must be:

1. **Bound to context** — specific chain, contract, and version (domain separation)
2. **Used exactly once** — nonce prevents replay
3. **Time-limited** — deadline/expiry prevents late execution
4. **Correctly verified** — ecrecover edge cases handled

Any gap in this model creates a replay vulnerability.

## The Five Replay Types

### Type 1: Same-Chain Replay

The exact same signature is submitted multiple times to the same contract on the same chain.

```solidity
// VULNERABLE: No nonce — same signature works forever
function executeWithSig(address to, uint256 amount, bytes memory signature) external {
    bytes32 hash = keccak256(abi.encodePacked(to, amount));
    address signer = ECDSA.recover(hash, signature);
    require(signer == admin, "Invalid signer");
    token.transfer(to, amount);
    // Attacker can submit this same signature again and again!
}

// SAFE: Use nonce
mapping(address => uint256) public nonces;

function executeWithSig(address to, uint256 amount, uint256 nonce, bytes memory signature) external {
    require(nonce == nonces[admin], "Invalid nonce");
    bytes32 hash = keccak256(abi.encodePacked(to, amount, nonce));
    address signer = ECDSA.recover(hash, signature);
    require(signer == admin, "Invalid signer");
    nonces[admin]++;
    token.transfer(to, amount);
}
```

### Type 2: Cross-Chain Replay

A signature valid on one chain (e.g., Ethereum) is replayed on another chain (e.g., Polygon, Arbitrum) where the same contract is deployed.

```solidity
// VULNERABLE: No chainId in signed message
bytes32 hash = keccak256(abi.encodePacked(to, amount, nonce));
// This hash is identical on Ethereum, Polygon, Arbitrum, etc.

// SAFE: Include chainId (via EIP-712 domain separator)
bytes32 DOMAIN_SEPARATOR = keccak256(abi.encode(
    keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
    keccak256(bytes("MyContract")),
    keccak256(bytes("1")),
    block.chainid,
    address(this)
));
```

### Type 3: Cross-Contract Replay

A signature for Contract A is replayed on Contract B (same chain) if both accept the same message format without contract-specific binding.

```solidity
// VULNERABLE: No contract address in signed message
bytes32 hash = keccak256(abi.encodePacked(to, amount, nonce, block.chainid));
// Same hash for any contract on this chain

// SAFE: Include verifyingContract (via EIP-712)
// The domain separator includes address(this), binding to this specific contract
```

### Type 4: Nonce-Skip Replay

Nonce implementation allows gaps or out-of-order execution, enabling skipped nonces to be replayed later.

```solidity
// VULNERABLE: Bitmap nonce without invalidation
mapping(uint256 => bool) public usedNonces;

function execute(uint256 nonce, ...) external {
    require(!usedNonces[nonce], "Used");
    usedNonces[nonce] = true;
    // If nonces 1, 2, 3 are used but 4 is skipped,
    // nonce 4 can be used anytime in the future
    // This may be intentional OR a vulnerability depending on context
}

// SAFER for strict ordering: Sequential nonce
mapping(address => uint256) public nonces;

function execute(uint256 nonce, ...) external {
    require(nonce == nonces[signer], "Invalid nonce");
    nonces[signer]++;
}
```

### Type 5: Expired-Signature Replay

A signature without a deadline can be held and executed at an arbitrary future time when conditions have changed.

```solidity
// VULNERABLE: No deadline — signature valid forever
function permit(address owner, address spender, uint256 value, uint8 v, bytes32 r, bytes32 s) external {
    bytes32 hash = keccak256(abi.encodePacked(owner, spender, value, nonces[owner]++));
    require(ecrecover(hash, v, r, s) == owner, "Invalid");
    allowance[owner][spender] = value;
    // This permit can be executed weeks later when user doesn't expect it
}

// SAFE: Include deadline
function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external {
    require(block.timestamp <= deadline, "Expired");
    // ... rest of verification
}
```

## ecrecover Safety

### Edge Case 1: Returns address(0)

`ecrecover` returns `address(0)` for invalid signatures instead of reverting.

```solidity
// VULNERABLE: address(0) accepted as valid signer
address signer = ecrecover(hash, v, r, s);
require(signer == owner, "Invalid");
// If owner == address(0) AND signature is invalid → passes!

// SAFE: Explicit zero check
address signer = ecrecover(hash, v, r, s);
require(signer != address(0), "Invalid signature");
require(signer == owner, "Wrong signer");

// SAFEST: Use OpenZeppelin's ECDSA.recover() — reverts on address(0)
address signer = ECDSA.recover(hash, signature);
```

### Edge Case 2: Signature Malleability

For every valid ECDSA signature (r, s, v), there exists a second valid signature (r, s', v') for the same message. This allows anyone to create an alternate valid signature without the private key.

```solidity
// The Ethereum standard: s must be in the lower half of the curve
// s' = secp256k1n - s (the "flipped" signature)

// VULNERABLE: Accepts both s values
address signer = ecrecover(hash, v, r, s); // Works for both s and s'
// If used as a unique identifier, the same message has TWO valid signatures

// SAFE: Enforce lower-s (OpenZeppelin's ECDSA library does this)
require(uint256(s) <= 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0, "Invalid s");
```

### Edge Case 3: v Value

```solidity
// v should be 27 or 28 (Ethereum standard)
// Some implementations use 0 or 1 (subtract 27)
// Not normalizing v can cause signature verification to fail

require(v == 27 || v == 28, "Invalid v");
```

## EIP-712 Domain Separator Verification

### Complete Domain

```solidity
bytes32 constant DOMAIN_TYPEHASH = keccak256(
    "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
);

bytes32 DOMAIN_SEPARATOR = keccak256(abi.encode(
    DOMAIN_TYPEHASH,
    keccak256(bytes(name)),        // Contract name
    keccak256(bytes(version)),     // Version string
    block.chainid,                 // Chain ID — prevents cross-chain replay
    address(this)                  // Contract address — prevents cross-contract replay
));
```

### Required Fields

| Field | Purpose | Missing = |
|-------|---------|-----------|
| `name` | Identifies the signing domain | MEDIUM risk |
| `version` | Prevents replay across upgrades | MEDIUM risk |
| `chainId` | **Prevents cross-chain replay** | HIGH risk |
| `verifyingContract` | **Prevents cross-contract replay** | HIGH risk |
| `salt` (optional) | Additional disambiguation | LOW risk |

### Common Mistakes

```solidity
// MISTAKE 1: Hardcoded chainId (doesn't update on chain forks)
uint256 immutable CHAIN_ID = 1;
// After a fork, signatures valid on both chains!

// SAFE: Use block.chainid at verification time, or recalculate domain separator
function DOMAIN_SEPARATOR() public view returns (bytes32) {
    if (block.chainid == INITIAL_CHAIN_ID) return _DOMAIN_SEPARATOR;
    return _calculateDomainSeparator(); // Recalculate for new chain
}

// MISTAKE 2: Empty name/version
keccak256(bytes("")) // Valid but weak — same across all contracts with empty name

// MISTAKE 3: Missing struct type hash in message
// EIP-712 requires: hashStruct(message) = keccak256(typeHash + encodeData(message))
// Omitting typeHash weakens the domain binding
```

## Permit and Permit2 Verification

### ERC-2612 Permit Checklist

```
- [ ] Uses EIP-712 domain separator with chainId and verifyingContract
- [ ] Includes per-user sequential nonce
- [ ] Includes deadline with block.timestamp check
- [ ] Uses ECDSA.recover (not raw ecrecover)
- [ ] Checks recovered address != address(0)
- [ ] Checks recovered address == owner parameter
- [ ] Nonce incremented BEFORE any state change
- [ ] Domain separator recalculated on chain fork
```

### Permit2 Considerations

```
- Permit2 uses nonce-bitmap approach (unordered nonces)
- Supports batch permits and transfer-with-permit
- Still requires deadline, domain separator, nonce management
- Contracts integrating Permit2 must verify the permit2 contract address
```

## Workflow

```
Task Progress:
- [ ] Step 1: Find all signature verification code (ecrecover, ECDSA.recover, EIP-712)
- [ ] Step 2: Check for same-chain replay protection (nonce management)
- [ ] Step 3: Check for cross-chain replay protection (chainId in domain/message)
- [ ] Step 4: Check for cross-contract replay protection (address(this) in domain/message)
- [ ] Step 5: Check deadline/expiry enforcement
- [ ] Step 6: Verify ecrecover safety (address(0) check, s-value, v-value)
- [ ] Step 7: Verify EIP-712 domain separator completeness
- [ ] Step 8: Check ERC-1271 support for contract wallets (if applicable)
- [ ] Step 9: Score findings and generate report
```

## Output Format

```markdown
## Signature & Replay Analysis Report

### Finding: [Title]

**Function:** `functionName()` at `Contract.sol:L42`
**Replay Type:** [Same-Chain | Cross-Chain | Cross-Contract | Nonce-Skip | Expired]
**Severity:** [CRITICAL | HIGH | MEDIUM]

**Issue:**
[Description of the replay vulnerability or signature verification flaw]

**Signed Message Fields:**
- [x] to/from addresses
- [x] amount/value
- [ ] chainId ← MISSING
- [ ] verifyingContract ← MISSING
- [x] nonce
- [ ] deadline ← MISSING

**Attack Scenario:**
1. User signs message for [intended purpose]
2. Attacker captures signature from [source]
3. Attacker replays on [target chain/contract/time]
4. [Unauthorized action occurs]

**Recommendation:**
[Add EIP-712 domain separator, add nonce, add deadline, use ECDSA.recover]
```

## Quick Detection Checklist

- [ ] Does every signature include a nonce? (Prevents same-chain replay)
- [ ] Does the signed message include `chainId`? (Prevents cross-chain replay)
- [ ] Does the signed message include `address(this)`? (Prevents cross-contract replay)
- [ ] Is there a deadline/expiry with `block.timestamp` check? (Prevents late execution)
- [ ] Is `ecrecover` result checked against `address(0)`?
- [ ] Is the s-value enforced to be in the lower half? (Prevents malleability)
- [ ] Is the domain separator recalculated on chain fork? (Prevents fork replay)
- [ ] Is OpenZeppelin's ECDSA library used instead of raw `ecrecover`?
- [ ] For permit: Is the nonce incremented before state changes?
- [ ] For contract wallets: Is ERC-1271 `isValidSignature` supported?

For replay type details, see [{baseDir}/references/replay-taxonomy.md]({baseDir}/references/replay-taxonomy.md).
For EIP-712 checklist, see [{baseDir}/references/eip712-checklist.md]({baseDir}/references/eip712-checklist.md).

## Rationalizations to Reject

- "We use nonces so replay is impossible" → Check for cross-chain and cross-contract replay (nonce doesn't prevent those)
- "No one would replay on another chain" → Attackers monitor all chains; automated bots scan for replayable signatures
- "ecrecover is a built-in, so it's safe" → It returns address(0) on failure, not revert; it doesn't enforce s-value
- "The signature includes all the parameters" → Without chainId and contract address, it's still replayable
- "We hardcoded chainId = 1" → Chain forks create two live chains with the same chainId; use block.chainid
- "Permit is a standard, so it's safe" → The standard defines the interface, not the implementation; bugs are in how it's coded

## references

```

```

## references/eip712-checklist.md

# EIP-712 Implementation Verification Checklist

## Domain Separator

### Required Fields

```solidity
bytes32 constant EIP712DOMAIN_TYPEHASH = keccak256(
    "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
);
```

| # | Check | Severity if Missing |
|---|-------|-------------------|
| 1 | `name` field present and meaningful | MEDIUM |
| 2 | `version` field present | MEDIUM |
| 3 | `chainId` field uses `block.chainid` | **HIGH** — cross-chain replay |
| 4 | `verifyingContract` uses `address(this)` | **HIGH** — cross-contract replay |
| 5 | Optional `salt` for additional disambiguation | LOW |

### Dynamic Chain ID Handling

```solidity
// CHECK: Is domain separator recalculated when chain ID changes (fork)?

// VULNERABLE: Computed once in constructor, never updated
constructor() {
    DOMAIN_SEPARATOR = keccak256(abi.encode(
        EIP712DOMAIN_TYPEHASH,
        keccak256(bytes(name)),
        keccak256(bytes(version)),
        block.chainid,           // Captured at deployment
        address(this)
    ));
}
// After a chain fork, DOMAIN_SEPARATOR is stale → cross-chain replay!

// SAFE: Recalculate when chainId changes
bytes32 private immutable _CACHED_DOMAIN_SEPARATOR;
uint256 private immutable _CACHED_CHAIN_ID;

function DOMAIN_SEPARATOR() public view returns (bytes32) {
    return block.chainid == _CACHED_CHAIN_ID
        ? _CACHED_DOMAIN_SEPARATOR
        : _buildDomainSeparator();
}
```

| # | Check | Severity if Failing |
|---|-------|-------------------|
| 6 | Domain separator updates when `block.chainid` changes | **HIGH** |
| 7 | Cached for gas efficiency on normal path | LOW (gas) |

---

## Struct Type Hash

### Correct Pattern

```solidity
// Each signed struct needs its own type hash
bytes32 constant PERMIT_TYPEHASH = keccak256(
    "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"
);
```

| # | Check | Severity if Failing |
|---|-------|-------------------|
| 8 | Type hash string matches actual struct fields exactly | HIGH |
| 9 | Type hash includes ALL security-relevant fields (nonce, deadline) | **HIGH** |
| 10 | Type hash is a constant (not computed dynamically) | LOW |
| 11 | Nested structs use the correct encoding rules | MEDIUM |

---

## Hash Construction

### Correct Pattern

```solidity
bytes32 structHash = keccak256(abi.encode(
    PERMIT_TYPEHASH,
    owner,
    spender,
    value,
    nonces[owner],
    deadline
));

bytes32 digest = keccak256(abi.encodePacked(
    "\x19\x01",
    DOMAIN_SEPARATOR(),
    structHash
));
```

| # | Check | Severity if Failing |
|---|-------|-------------------|
| 12 | Uses `"\x19\x01"` prefix (EIP-191 version 1) | HIGH |
| 13 | Uses `abi.encode` (NOT `abi.encodePacked`) for struct hash | **HIGH** — collision risk |
| 14 | Domain separator comes BEFORE struct hash | HIGH |
| 15 | All fields in correct order matching type hash | HIGH |

### Common Mistake: abi.encodePacked vs abi.encode

```solidity
// VULNERABLE: abi.encodePacked can produce collisions for dynamic types
bytes32 hash = keccak256(abi.encodePacked(addr1, amount1, addr2, amount2));
// "addr1 = 0xAB, amount1 = 0xCD" produces same hash as
// "addr1 = 0xABCD, amount1 = 0x..." for certain values

// SAFE: abi.encode pads each value to 32 bytes
bytes32 hash = keccak256(abi.encode(addr1, amount1, addr2, amount2));
```

---

## Signature Recovery

| # | Check | Severity if Failing |
|---|-------|-------------------|
| 16 | Uses OpenZeppelin's ECDSA.recover (not raw ecrecover) | MEDIUM |
| 17 | Checks recovered address != address(0) | **CRITICAL** |
| 18 | Enforces lower-half s value (malleability protection) | MEDIUM |
| 19 | Validates v is 27 or 28 | LOW |
| 20 | Supports ERC-1271 for contract wallets (if needed) | MEDIUM |

### ERC-1271 Contract Wallet Support

```solidity
// For protocols that should support smart contract wallets (e.g., Gnosis Safe)
function isValidSignature(address signer, bytes32 hash, bytes memory signature) internal view returns (bool) {
    if (signer.code.length > 0) {
        // Contract wallet — use ERC-1271
        try IERC1271(signer).isValidSignature(hash, signature) returns (bytes4 magicValue) {
            return magicValue == IERC1271.isValidSignature.selector;
        } catch {
            return false;
        }
    } else {
        // EOA — use ECDSA
        return ECDSA.recover(hash, signature) == signer;
    }
}
```

---

## Nonce Management

| # | Check | Severity if Failing |
|---|-------|-------------------|
| 21 | Nonce included in signed message | **CRITICAL** — same-chain replay |
| 22 | Nonce incremented/consumed BEFORE any state change | HIGH |
| 23 | Nonce increment is atomic with verification | HIGH |
| 24 | Sequential nonces: checked against current value | MEDIUM |
| 25 | Bitmap nonces: deadline required to limit validity | MEDIUM |

---

## Deadline Enforcement

| # | Check | Severity if Failing |
|---|-------|-------------------|
| 26 | Deadline field exists in signed struct | HIGH |
| 27 | Checked: `require(block.timestamp <= deadline)` | **HIGH** |
| 28 | Deadline included in the signed hash (not just checked) | CRITICAL |
| 29 | Reasonable maximum deadline enforced | LOW |

---

## Permit-Specific Checks (ERC-2612)

| # | Check | Severity if Failing |
|---|-------|-------------------|
| 30 | `owner` parameter matches recovered signer | CRITICAL |
| 31 | `spender` is the intended approval target | N/A (design) |
| 32 | `value` is the approval amount | N/A (design) |
| 33 | Cannot permit to self (`owner != spender`) | LOW |
| 34 | Permit emits `Approval` event | LOW |
| 35 | `PERMIT_TYPEHASH` matches ERC-2612 specification | HIGH |

---

## Meta-Transaction Specific Checks

| # | Check | Severity if Failing |
|---|-------|-------------------|
| 36 | Relayer cannot profit by delaying execution | MEDIUM |
| 37 | Gas parameters included in signature (if relevant) | MEDIUM |
| 38 | Relayer cannot manipulate msg.value | HIGH |
| 39 | Target function cannot be called directly (bypass relay) | MEDIUM |
| 40 | Trusted forwarder address is validated | HIGH |

---

## Summary Scoring

| Score | Assessment |
|-------|-----------|
| 35-40 checks passing | Robust implementation |
| 25-34 checks passing | Acceptable with noted risks |
| 15-24 checks passing | Significant vulnerabilities likely |
| < 15 checks passing | **Critical — fundamental replay risks** |

## references/replay-taxonomy.md

# Signature Replay Taxonomy — Detailed Reference

## Type 1: Same-Chain Replay

### Definition
The identical signature is submitted to the same contract on the same chain multiple times.

### Prerequisites for Exploitation
- No nonce in signed message, OR
- Nonce not incremented after use, OR
- Nonce checked but not atomically consumed

### Detection Heuristic

```
For each signature verification function:
  1. Extract all fields included in the signed hash
  2. Check if a nonce is included
  3. If no nonce → SAME-CHAIN REPLAY (CRITICAL)
  4. If nonce exists:
     a. Is nonce incremented BEFORE or AFTER state change?
     b. Is nonce increment atomic with verification?
     c. Can the function be called again before nonce increment?
     If after or non-atomic → SAME-CHAIN REPLAY (HIGH)
```

### Example Vulnerability

```solidity
// Nonce checked but not consumed before external call
function executeMetaTx(uint256 nonce, bytes memory sig) external {
    require(nonce == nonces[signer], "Bad nonce");
    address signer = ECDSA.recover(hash, sig);

    // External call BEFORE nonce increment — reentrancy can replay!
    (bool success, ) = target.call(data);

    nonces[signer]++; // Too late if target re-enters
}
```

---

## Type 2: Cross-Chain Replay

### Definition
A signature valid on Chain A is replayed on Chain B where the same (or similar) contract exists.

### Prerequisites for Exploitation
- No `chainId` in signed message or domain separator
- Hardcoded `chainId` that doesn't update on fork
- Same contract deployed at same address on multiple chains

### Real-World Case: Post-Fork Replay (Ethereum / Ethereum Classic)

```
Before EIP-155:
  Transactions on Ethereum were valid on Ethereum Classic (and vice versa)
  Any signed transaction could be replayed on the other chain

EIP-155 added chainId to transaction signatures
But APPLICATION-LEVEL signatures (EIP-712) must ALSO include chainId
```

### Detection Heuristic

```
For each signature verification:
  1. Does the signed hash include block.chainid?
  2. Does the EIP-712 domain separator include chainId field?
  3. Is chainId hardcoded or dynamic?

  If no chainId → CROSS-CHAIN REPLAY (HIGH)
  If hardcoded chainId:
    Is domain separator recalculated when block.chainid changes?
    If not → FORK REPLAY (MEDIUM)
```

### Correct Implementation

```solidity
// Cache domain separator for gas efficiency, but recalculate on fork
bytes32 private immutable _CACHED_DOMAIN_SEPARATOR;
uint256 private immutable _CACHED_CHAIN_ID;

function DOMAIN_SEPARATOR() public view returns (bytes32) {
    if (block.chainid == _CACHED_CHAIN_ID) {
        return _CACHED_DOMAIN_SEPARATOR;
    }
    return _buildDomainSeparator(); // Recalculate with new chainId
}
```

---

## Type 3: Cross-Contract Replay

### Definition
A signature intended for Contract A is replayed on Contract B (same chain) when both accept the same message format.

### Prerequisites for Exploitation
- No `verifyingContract` (address(this)) in signed message
- Multiple contracts with identical signature verification logic
- Same signer address used across contracts

### Detection Heuristic

```
For each signature verification:
  1. Does the signed hash include address(this)?
  2. Does the EIP-712 domain separator include verifyingContract?

  If neither → CROSS-CONTRACT REPLAY (HIGH)
```

### Example

```solidity
// Contract A: Token Bridge
function processWithdrawal(address to, uint256 amount, bytes memory sig) external {
    bytes32 hash = keccak256(abi.encodePacked(to, amount, nonces[to]++));
    require(ECDSA.recover(hash, sig) == bridge_admin);
    token.transfer(to, amount);
}

// Contract B: Different Token Bridge (same admin, same chain)
function processWithdrawal(address to, uint256 amount, bytes memory sig) external {
    bytes32 hash = keccak256(abi.encodePacked(to, amount, nonces[to]++));
    require(ECDSA.recover(hash, sig) == bridge_admin); // SAME admin!
    otherToken.transfer(to, amount);
}

// If nonces are synchronized, a signature for A can be replayed on B
```

---

## Type 4: Nonce-Skip Replay

### Definition
The nonce system allows gaps, enabling a "saved" nonce to be used at an arbitrary future time.

### Sequential vs Bitmap Nonces

```
Sequential: nonce must be exactly currentNonce + 1
  - Strict ordering: 0, 1, 2, 3, ...
  - A skipped nonce is LOST (can never be used)
  - Pro: Simple, prevents out-of-order execution
  - Con: One stuck transaction blocks all subsequent ones

Bitmap: Any unused nonce in the bitmap can be used
  - Flexible ordering: 5, 2, 8, 1, ...
  - Each nonce can be used independently
  - Pro: Out-of-order execution, no blocking
  - Con: Signed messages with unused nonces remain valid indefinitely
```

### Detection Heuristic

```
For bitmap nonce systems:
  1. Can a signed message with nonce N be held and executed later?
  2. Is there a deadline that limits the validity window?
  3. Can the signer cancel/invalidate a specific nonce?

  If no deadline AND no cancellation → NONCE-SKIP REPLAY (MEDIUM)
  Impact depends on whether delayed execution is harmful
```

---

## Type 5: Expired-Signature Replay

### Definition
A signature without a time limit is executed long after it was created, when the signer's intent has changed.

### Prerequisites for Exploitation
- No `deadline` or `expiry` field in signed message
- No time-based validation in verification function

### Example Scenario

```
1. User signs a permit() for 1000 USDC to DEX contract
2. User cancels the trade (off-chain)
3. Weeks later, token price has changed dramatically
4. Attacker submits the stored permit at unfavorable time
5. DEX uses the permit to execute a swap at bad price
```

### Detection Heuristic

```
For each signature verification:
  1. Is there a deadline/expiry parameter?
  2. Is deadline checked: require(block.timestamp <= deadline)?
  3. Is the deadline a reasonable duration (not type(uint256).max)?

  If no deadline → EXPIRED-SIGNATURE REPLAY (MEDIUM-HIGH)
  If deadline = type(uint256).max commonly used → EFFECTIVELY NO DEADLINE (MEDIUM)
```

---

## Complete Replay Protection Checklist

| Protection | Same-Chain | Cross-Chain | Cross-Contract | Nonce-Skip | Expired |
|------------|-----------|-------------|----------------|------------|---------|
| Sequential nonce | YES | NO | NO | YES | NO |
| Bitmap nonce | YES | NO | NO | NO | NO |
| chainId in domain | NO | YES | NO | NO | NO |
| address(this) in domain | NO | NO | YES | NO | NO |
| Deadline/expiry | NO | NO | NO | PARTIAL | YES |
| Full EIP-712 domain | NO | YES | YES | NO | NO |
| **All of the above** | **YES** | **YES** | **YES** | **YES** | **YES** |

**Minimum required for complete protection:**
1. Nonce (sequential or bitmap + deadline)
2. EIP-712 domain separator with chainId and verifyingContract
3. Deadline/expiry timestamp

