HIGH - Enables replay attacks on privileged actions
Detects proof-gated privileged actions (governance votes, identity attestation, access control) that lack nonce or context-binding checks, allowing the same proof to be replayed across different transactions or contexts.
// ❌ BAD: Vote without nonce - can be replayed
pub fn vote(env: Env, proposal_id: u64, proof: BytesN<32>, inputs: Vec<u64>) {
verify_zk_proof(&env, proof, inputs);
// No nonce check - attacker can resubmit same proof!
let votes = env.storage().get(&proposal_id).unwrap_or(0);
env.storage().set(&proposal_id, votes + 1);
}Attack scenario:
- Attacker obtains valid voting proof once
- Resubmits the same proof multiple times
- Casts unlimited votes, manipulating governance
// ✅ GOOD: Nonce enforcement
pub fn vote_with_nonce(
env: Env,
proposal_id: u64,
proof: BytesN<32>,
inputs: Vec<u64>,
nonce: u64
) {
verify_zk_proof(&env, proof, inputs);
// Check nonce hasn't been used
let used_nonces: Set<u64> = env.storage()
.instance()
.get(&symbol_short!("nonces"))
.unwrap_or(Set::new(&env));
if used_nonces.contains(&nonce) {
panic!("Nonce already used");
}
// Mark nonce as used
let mut updated_nonces = used_nonces.clone();
updated_nonces.insert(nonce);
env.storage()
.instance()
.set(&symbol_short!("nonces"), &updated_nonces);
// Process vote
let votes = env.storage().get(&proposal_id).unwrap_or(0);
env.storage().set(&proposal_id, votes + 1);
}// ✅ GOOD: Context binding
pub fn vote_with_context(
env: Env,
proposal_id: u64,
proof: BytesN<32>,
inputs: Vec<u64>,
context: BytesN<32> // H(contract_address || proposal_id || "vote")
) {
// Verify proof includes context commitment
let expected_context = compute_context(&env, proposal_id);
if context != expected_context {
panic!("Invalid context");
}
verify_proof_with_context(&env, proof, inputs, context);
// Proof is bound to this specific context, can't be replayed elsewhere
let votes = env.storage().get(&proposal_id).unwrap_or(0);
env.storage().set(&proposal_id, votes + 1);
}
fn compute_context(env: &Env, proposal_id: u64) -> BytesN<32> {
let mut data = Bytes::new(env);
data.append(&env.current_contract_address().to_bytes());
data.append(&Bytes::from_array(env, &proposal_id.to_be_bytes()));
data.append(&Bytes::from_slice(env, b"vote"));
keccak256(env, &data)
}// ✅ GOOD: Time-bound proof validity
pub fn vote_with_expiry(
env: Env,
proposal_id: u64,
proof: BytesN<32>,
inputs: Vec<u64>,
proof_timestamp: u64,
signature: BytesN<64> // Signs proof + timestamp
) {
// Verify timestamp is recent
let current_time = env.ledger().timestamp();
let max_age = 3600; // 1 hour validity
if current_time > proof_timestamp + max_age {
panic!("Proof expired");
}
if current_time < proof_timestamp {
panic!("Proof timestamp in future");
}
// Verify signature over (proof, timestamp) to prevent timestamp manipulation
verify_signature(&env, proof, proof_timestamp, signature);
verify_zk_proof(&env, proof, inputs);
// Process vote (still need per-user tracking)
let votes = env.storage().get(&proposal_id).unwrap_or(0);
env.storage().set(&proposal_id, votes + 1);
}Without replay protection:
- Governance manipulation: Vote multiple times with one proof, swing elections
- Identity fraud: Reuse attestation across platforms/contracts
- Access abuse: Replay authorization indefinitely after revocation
- Delegation attacks: Resubmit delegation proof after intended expiry
- Z001: Value transfer replay (double-spend via missing nullifiers)
- Z006: Non-transfer replay (general privilege escalation)
Both are replay attacks but in different contexts:
- Z001 focuses on preventing double-spending of value
- Z006 focuses on preventing replay of authorizations/actions
This rule analyzes proof-verification call sites:
- Find proof verifications: Identify all
verify_proof/verify_zk_proofcalls - Check if privileged: Determine if action:
- Modifies storage
- Grants permissions
- Affects governance
- Changes access control
- Exclude transfers: Skip value-transfer patterns (covered by Z001)
- Check nonce: Look for:
- Nonce parameter in function signature
- Nonce validation against used-nonce storage
- Nonce marked as used after verification
- Check context binding: Look for:
- Context parameter (hash of contract+action)
- Context verification in proof
- Flag if missing: Report if neither protection mechanism present
- Governance voting
- Delegation/authorization
- Identity attestation
- Role assignment
- Permission grants
- Access control updates
- Z001: Missing nullifier checks (value-transfer specific)
- Z003: Public input leakage
- S012: Missing access control
- #1192: ZK infrastructure foundation
- #1194: Proof verification pattern detection
- #1197: Z001 implementation (to avoid duplicate findings)
See test fixtures in contracts/fixtures/finding-codes/z006_replay_missing_nonce.rs for comprehensive examples.