SNARKeling Treasure Hunt

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

H-01: Duplicate treasure hash in the Noir circuit means only 9 unique treasures exist and one finder can claim two reward slots

Root + Impact

Description

  • The circuit's is_allowed() function checks whether a submitted treasure_hash belongs to a hardcoded array of 10 allowed hashes. This array is intended to represent 10 distinct physical treasures.

  • The 9th and 10th entries of ALLOWED_TREASURE_HASHES are identical. There are therefore only 9 distinct hashes, making treasure #9 (secret value 9) impossible to prove, while the finder of the treasure associated with the duplicated hash can satisfy is_allowed() for two different claim slots.

global ALLOWED_TREASURE_HASHES: [Field; 10] = [
1505662313093145631275418581390771847921541863527840230091007112166041775502,
-7876059170207639417138377068663245559360606207000570753582208706879316183353,
-5602859741022561807370900516277986970516538128871954257532197637239594541050,
2256689276847399345359792277406644462014723416398290212952821205940959307205,
10311210168613568792124008431580767227982446451742366771285792060556636004770,
-5697637861416433807484703347699404695743570043365849280798663758395067508,
-2009295789879562882359281321158573810642695913475210803991480097462832104806,
8931814952839857299896840311953754931787080333405300398787637512717059406908,
// @> index 8 and index 9 are identical
-961435057317293580094826482786572873533235701183329831124091847635547871092,
-961435057317293580094826482786572873533235701183329831124091847635547871092
];

The circuit test confirms that treasure 9 is replaced by a second 10:

// circuits/src/tests.nr line 30
// @> treasure 9 missing; 10 appears twice
let treasures: [Field; 10] = [1, 2, 3, 4, 5, 6, 7, 8, 10, 10];

Risk

Likelihood:

  • The duplicate is present in the deployed circuit artifacts; any participant inspecting the public hash list will notice two identical entries.

  • The finder of the duplicated treasure can exploit this without any additional tooling.

Impact:

  • Treasure #9 is unclaimable. 10 ETH is permanently locked for that slot.

  • The finder of the duplicated hash can claim two 10 ETH rewards (20 ETH total) with a single physical treasure find, at the expense of other participants.

  • The hunt cannot be completed by 10 unique participants as designed.

Proof of Concept

The circuit's own test suite exposes the duplication: test_treasure_hunt_all_treasures_success is supposed to prove all ten unique treasures, but it silently skips secret 9 and passes secret 10 twice (once for index 8, once for index 9). Both iterations hash to the same ALLOWED_TREASURE_HASHES[8] / ALLOWED_TREASURE_HASHES[9] value, so the test passes without ever verifying that treasure #9 is provable.

// circuits/src/tests.nr
#[test]
fn test_treasure_hunt_all_treasures_success() {
// treasure 9 is skipped; index 8 and 9 both use treasure 10
let treasures: [Field; 10] = [1, 2, 3, 4, 5, 6, 7, 8, 10, 10];
for i in 0..10 {
// Both i=8 and i=9 use the same treasure (10) and the same hash
main(treasures[i], ALLOWED_TREASURE_HASHES[i], 2);
}

Recommended Mitigation

  • The last entry in ALLOWED_TREASURE_HASHES must be replaced with the Pedersen hash of secret 9. The correct value is already recorded in the deploy script (index 8), confirming it was computed correctly there. After patching main.nr, the circuit must be recompiled and Verifier.sol regenerated so the on-chain verifier enforces the corrected hash list. The corresponding test should also be updated to use [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] to cover all unique secrets.

  • then recompile the circuit and regenerate Verifier.sol after the fix.

- -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!