SNARKeling Treasure Hunt

First Flight #59
Beginner FriendlyGameFiFoundry
100 EXP
Submission Details
Impact: high
Likelihood: high

Duplicate Hash in Circuit — Treasure 9 Is Permanently Unclaimable, Hunt Cannot Complete

Author Revealed upon completion

Duplicate Hash in Circuit — Treasure 9 Is Permanently Unclaimable, Hunt Cannot Complete

Root Cause + Impact

The Noir circuit's ALLOWED_TREASURE_HASHES array has identical values at indices [8] and [9]. Treasure 9's correct hash is replaced by a duplicate of treasure 10's hash. Treasure 9 can never be claimed, its finder loses their 10 ETH reward, and the hunt's normal completion path (withdraw()) is permanently blocked.

Description

  • The circuit should contain 10 unique Pedersen hashes corresponding to 10 unique treasure secrets. is_allowed() checks that a submitted treasure_hash matches one of these 10 hashes.

  • Indices [8] and [9] contain the same value. The correct hash for treasure 9 (from Deploy.s.sol:25) was replaced by treasure 10's hash. A participant who finds treasure 9 computes pedersen_hash([9]), but this hash is not in the allowed set, so is_allowed() returns false and proof generation fails.

// circuits/src/main.nr, lines 64-65
global ALLOWED_TREASURE_HASHES: [Field; 10] = [
// ... indices 0-7 are correct ...
@> -961435057317293580094826482786572873533235701183329831124091847635547871092, // index 8: WRONG — treasure 10's hash
@> -961435057317293580094826482786572873533235701183329831124091847635547871092 // index 9: treasure 10's hash (duplicate)
];
// Deploy.s.sol line 25 shows the correct hash for treasure 9:
// -4417726114039171734934559783368726413190541565291523767661452385022043124552

circuits/src/tests.nr:30 confirms the bug with generating secrets [1, 2, 3, 4, 5, 6, 7, 8, 10, 10] — treasure 9 is missing from the test fixture too.

Risk

Likelihood: Affects every attempt to claim treasure 9. The bug is baked into the compiled circuit; fixing it requires recompiling main.nr, regenerating Verifier.sol, pausing, calling updateVerifier(), and unpausing.

Impact:

  • Treasure 9's finder permanently loses 10 ETH with no recourse.

  • withdraw() requires claimsCount >= MAX_TREASURES (10), but at most 9 unique treasures can be claimed. The owner's normal end-of-hunt withdrawal path is permanently disabled and can only be worked around via pause() + emergencyWithdraw().

  • The hunt promises 10 treasures but only 9 are solvable — a core product guarantee is broken.

Proof of Concept

When the finder of treasure 9 runs nargo prove with treasure = 9, the circuit evaluates assert(is_allowed(pedersen_hash([9]))) on line 31. is_allowed() loops over the 10 allowed hashes; treasure 9's hash is nowhere in the set, so all 10 comparisons fail, the assert fails, and proof generation aborts before any on-chain interaction. No valid proof can ever be produced for treasure 9, so claim() cannot be called.

fn test_treasure_9_unclaimable() {
let treasure: Field = 9;
let treasure_hash = std::hash::pedersen_hash([treasure]);
// treasure_hash = -4417726114039171734934559783368726413190541565291523767661452385022043124552
// This value does NOT exist in ALLOWED_TREASURE_HASHES
assert(is_allowed(treasure_hash)); // FAILS — treasure 9 is unsolvable
}

Recommended Mitigation

Replace the duplicated entry at index [8] with treasure 9's correct hash, then recompile the circuit and redeploy the verifier via updateVerifier():

global ALLOWED_TREASURE_HASHES: [Field; 10] = [
// ... indices 0-7 unchanged ...
- -961435057317293580094826482786572873533235701183329831124091847635547871092,
+ -4417726114039171734934559783368726413190541565291523767661452385022043124552,
-961435057317293580094826482786572873533235701183329831124091847635547871092
];

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!