SNARKeling Treasure Hunt

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

Duplicate Hash in ALLOWED_TREASURE_HASHES Makes Treasure 9 Unclaimable and Permanently Locks Owner's withdraw()

Author Revealed upon completion

Finding 2: Duplicate Hash in ALLOWED_TREASURE_HASHES Makes Treasure 9 Unclaimable and Permanently Locks Owner's withdraw()

Root + Impact

Duplicate hash in circuit array + permanent fund lock for owner

Description

  • The ALLOWED_TREASURE_HASHES array in the Noir circuit should contain 10 unique hashes, one for each of the 10 physical treasures. This allows any participant who finds a physical treasure to generate a valid ZK proof and claim their reward.

  • Indices 8 and 9 of the ALLOWED_TREASURE_HASHES array are both set to the same value -961435057317293580094826482786572873533235701183329831124091847635547871092 (the hash of treasure 10). The hash for treasure 9 (-4417726114039171734934559783368726413190541565291523767661452385022043124552, as listed in Prover.toml.example) is missing from the circuit. The is_allowed() function will therefore return false for treasure 9's hash, making it impossible to generate a valid proof for that treasure.

global ALLOWED_TREASURE_HASHES: [Field; 10] = [
1505662313093145631275418581390771847921541863527840230091007112166041775502,
-7876059170207639417138377068663245559360606207000570753582208706879316183353,
-5602859741022561807370900516277986970516538128871954257532197637239594541050,
2256689276847399345359792277406644462014723416398290212952821205940959307205,
10311210168613568792124008431580767227982446451742366771285792060556636004770,
-5697637861416433807484703347699404695743570043365849280798663758395067508,
-2009295789879562882359281321158573810642695913475210803991480097462832104806,
8931814952839857299896840311953754931787080333405300398787637512717059406908,
-961435057317293580094826482786572873533235701183329831124091847635547871092, // @> should be hash for treasure 9
-961435057317293580094826482786572873533235701183329831124091847635547871092 // @> duplicate of above — treasure 9's hash is missing
];

The test file tests.nr also confirms awareness of this issue — treasure 9 is omitted and replaced with a second 10:

let treasures: [Field; 10] = [1, 2, 3, 4, 5, 6, 7, 8, 10, 10]; // @> 9 is missing, 10 is duplicated

Risk

Likelihood:

  • The circuit always rejects a proof for treasure 9 since its hash is not in the allowed set — this occurs for every participant who finds treasure 9

  • claimsCount reaches at most 9 (from the remaining 9 unique valid hashes), never the required MAX_TREASURES (10), so the withdraw() condition is never satisfied

Impact:

  • The participant who physically finds treasure 9 cannot claim their 10 ETH reward — they are permanently disadvantaged despite valid real-world effort

  • The owner's withdraw() function requires claimsCount >= MAX_TREASURES (line 224), which can never be satisfied, so the owner's leftover funds are permanently locked in the contract (only recoverable via emergencyWithdraw while paused, which is a degraded fallback)

  • The duplicate hash means there are only 9 unique valid hashes, violating the stated invariant of 10 treasures

Proof of Concept

// In Noir — this will FAIL because treasure 9's hash is not in ALLOWED_TREASURE_HASHES
#[test(should_fail)]
fn test_treasure_9_is_unclaimable() {
let treasure: Field = 9;
// pedersen_hash([9]) — as listed in Prover.toml.example
let treasure_hash: Field = -4417726114039171734934559783368726413190541565291523767661452385022043124552;
let recipient: Field = 2;
main(treasure, treasure_hash, recipient); // FAILS: is_allowed() returns false
}
// After 9 legitimate claims, withdraw is permanently stuck
function testWithdrawPermanentlyLocked() public {
// Even after all claimable treasures are claimed, claimsCount == 9, not 10
// Since treasure 9's hash is missing from the circuit, claimsCount can never reach 10
vm.prank(owner);
vm.expectRevert("HUNT_NOT_OVER");
hunt.withdraw(); // Always reverts — owner funds are stuck
}

Recommended Mitigation

Replace the duplicate entry at index 8 with the correct hash for treasure 9:

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

Also fix the test to use the correct treasure:

- 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];

Support

FAQs

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

Give us feedback!