SNARKeling Treasure Hunt

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

[H-01] Duplicate hash in ALLOWED_TREASURE_HASHES renders treasure "9" unclaimable

Root + Impact

Description

  • The Noir circuit defines the validity of the treasure hunt. It contains a hardcoded array ALLOWED_TREASURE_HASHES representing the 10 valid
    physical treasures. For a claim to be valid, the prover must show they know a secret whose hash is present in this list

  • The issue is a manual entry error in the circuit's global array. Indices 8 and 9 contain the exact same hash value (which corresponds to secret
    "10"). Consequently, the unique hash for secret "9" is missing from the circuit

// circuits/src/main.nr
global ALLOWED_TREASURE_HASHES: [Field; 10] = [
// ... index 0-7 ...
8931814952839857299896840311953754931787080333405300398787637512717059406908,
-961435057317293580094826482786572873533235701183329831124091847635547871092, // @> Index 8
-961435057317293580094826482786572873533235701183329831124091847635547871092 // @> Index 9 (Duplicate)
];

Risk

Likelihood:

  • This error is static and exists in the compiled circuit artifacts. It will always trigger if a participant tries to prove knowledge of secret
    "9".

Impact:

  • Unclaimable Treasure: The participant who physically finds treasure "9" will be unable to generate a valid proof, as the circuit will fail the
    is_allowed check

  • Protocol Integrity: The game is advertised as having 10 treasures, but mathematically only 9 exist in the ZK logic

Proof of Concept

This Noir test case (added to tests.nr) proves the vulnerability by:

  1. Comparing the values at index 8 and 9 to confirm they are identical.

  2. Generating the hash for secret "9" and asserting that the is_allowed helper function returns false, meaning the circuit would reject a claim
    for this valid physical treasure

#[test]
fn prove_duplicate_and_missing_hashes() {
// 1. Prove index 8 and 9 are duplicates
assert(ALLOWED_TREASURE_HASHES[8] == ALLOWED_TREASURE_HASHES[9]);
// 2. Prove secret "9" hash is not allowed by the circuit
let hash_9 = std::hash::pedersen_hash([9]);
assert(is_allowed(hash_9) == false);
}

Recommended Mitigation

Update the ALLOWED_TREASURE_HASHES array in main.nr to include the correct hash for the 9th treasure. This ensures that all 10 treasures found in
the real world can be successfully verified by the ZK circuit

- -961435057317293580094826482786572873533235701183329831124091847635547871092,
+ -4417726114039171734934559783368726413190541565291523767661452385022043124552,
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!