SNARKeling Treasure Hunt

First Flight #59
Beginner FriendlyGameFiFoundry
100 EXP
View results
Submission Details
Severity: high
Valid

Circuit omits one valid treasure hash and duplicates another, making one treasure impossible to claim

Root + Impact

Description

  • The circuit is expected to support 10 unique treasure hashes, but ALLOWED_TREASURE_HASHES duplicates the hash for treasure 10 and omits the hash for treasure 9.

  • As a result, one legitimate treasure can never be proven or claimed.

global ALLOWED_TREASURE_HASHES: [Field; 10] = [
1505662313093145631275418581390771847921541863527840230091007112166041775502,
-7876059170207639417138377068663245559360606207000570753582208706879316183353,
-5602859741022561807370900516277986970516538128871954257532197637239594541050,
2256689276847399345359792277406644462014723416398290212952821205940959307205,
10311210168613568792124008431580767227982446451742366771285792060556636004770,
-5697637861416433807484703347699404695743570043365849280798663758395067508,
-2009295789879562882359281321158573810642695913475210803991480097462832104806,
8931814952839857299896840311953754931787080333405300398787637512717059406908,
-961435057317293580094826482786572873533235701183329831124091847635547871092,
-961435057317293580094826482786572873533235701183329831124091847635547871092
];

Risk

Likelihood:

  • This occurs deterministically for every deployment using the current circuit.

  • The hash for treasure 9 is not included in the allowed set, so a legitimate finder of treasure 9 cannot generate a proof accepted by the verifier.

Impact:

  • One of the 10 advertised treasures is permanently unclaimable.

  • This breaks the core protocol promise that all valid treasures can be claimed, and can also prevent the hunt from honestly reaching MAX_TREASURES = 10, leaving post-hunt withdrawal dependent on invalid or duplicate claims.

Proof of Concept

The PoC shows that the protocol’s expected treasure 9 hash is present in the deployment script and prover fixture source, but missing from the actual circuit allowlist. Since the verifier is generated from the circuit, any proof for treasure 9 will fail and that reward cannot be claimed by a legitimate participant.

The deployment script and prover fixture list include the expected hash for treasure 9:
// -4417726114039171734934559783368726413190541565291523767661452385022043124552
However, the circuit does not include that hash. Instead, it repeats the hash for treasure 10 twice:
-961435057317293580094826482786572873533235701183329831124091847635547871092,
-961435057317293580094826482786572873533235701183329831124091847635547871092
This can also be seen in the circuit test, which tests treasure 10 twice and skips treasure 9:
let treasures: [Field; 10] = [1, 2, 3, 4, 5, 6, 7, 8, 10, 10];
A legitimate proof for treasure 9 would use:
treasure = 9;
treasure_hash = -4417726114039171734934559783368726413190541565291523767661452385022043124552;
But is_allowed(treasure_hash) returns false because the hash is absent from ALLOWED_TREASURE_HASHES, causing the circuit to reject:
assert(is_allowed(treasure_hash));

Recommended Mitigation

Replace the duplicated treasure 10 hash with the missing treasure 9 hash.

Also update the circuit tests to cover all 10 unique treasures:

- -961435057317293580094826482786572873533235701183329831124091847635547871092,
+ -4417726114039171734934559783368726413190541565291523767661452385022043124552,
-961435057317293580094826482786572873533235701183329831124091847635547871092
- let treasures: [Field; 10] = [1, 2, 3, 4, 5, 6, 7, 8, 10, 10];
+ let treasures: [Field; 10] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
Updates

Lead Judging Commences

s3mvl4d Lead Judge 18 days ago
Submission Judgement Published
Validated
Assigned finding tags:

unclaimable treasure / bricked withdraw path

The issue stems from a mismatch between the circuit and the contract’s economic assumptions: the Solidity contract is configured for `MAX_TREASURES = 10` and only allows the owner to call `withdraw()` once `claimsCount >= MAX_TREASURES`, while the Noir circuit’s baked-in `ALLOWED_TREASURE_HASHES` array does not actually contain ten distinct treasures because one hash is duplicated and another expected hash is missing. As a result, under the intended one-claim-per-treasure design described in the README, there are only nine uniquely claimable treasures even though the system is funded and accounted as if ten rewards can be legitimately redeemed. That creates two linked consequences from the same root cause: first, one treasure is effectively unclaimable because no valid proof can ever be generated for the missing allowed hash, and second, the normal “hunt over” withdrawal path becomes bricked because honest participants can never reach ten legitimate unique claims, leaving the post-hunt fund recovery logic via `withdraw` function permanently unreachable. The owner can still intervene through the emergency path.

Support

FAQs

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

Give us feedback!