SNARKeling Treasure Hunt

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

H-02: 9th treasure hash differs between the Noir circuit and the deploy script making that treasure permanently unclaimable

Root + Impact

Description

  • The deploy script publishes a list of 10 treasure hashes that participants use as public inputs when generating their ZK proofs off-chain. The Noir circuit bakes in its own list of allowed hashes that the on-chain verifier enforces. These two lists must be identical for every hash but the 9th entry (index 8) differs between them.

// Deploy.s.sol line 25 — hash published to participants for treasure #9
// @> -4417726114039171734934559783368726413190541565291523767661452385022043124552
// main.nr line 63 — hash enforced by the circuit for index 8
// @> -961435057317293580094826482786572873533235701183329831124091847635547871092

A participant who finds treasure #9 is given the deploy-script hash as their treasure_hash public input. The circuit's is_allowed() does not contain that value, so the proof fails verification and claim() reverts with InvalidProof.

Risk

Likelihood:

  • The mismatch is present in the committed code; it affects every attempt to claim treasure #9 from the moment of deployment.

  • No special attacker action is needed the ordinary happy-path claim fails automatically.

Impact:

  • Treasure #9 is permanently unclaimable regardless of whether the participant legitimately found it.

  • 10 ETH is locked in the contract and is recoverable only via emergencyWithdraw (which requires pausing the entire hunt).

  • The organizer faces reputational and potential legal liability for offering an uncollectable reward.

Proof of Concept

The failure path follows the normal participant workflow with no attacker tooling required. The participant generates a valid local proof using the hash advertised in the deploy script, but the on-chain verifier rejects it because the circuit enforces a different value for that index. The mismatch is deterministic and reproducible on every attempt.

1. Participant finds physical treasure #9.
2. Off-chain: participant uses hash from Deploy.s.sol index 8:
-4417726114039171734934559783368726413190541565291523767661452385022043124552
3. Proof generation succeeds locally (nargo execute passes).
4. On-chain: verifier checks is_allowed(treasure_hash).
The circuit's ALLOWED_TREASURE_HASHES[8] = -9614350... ≠ -4417726...
5. is_allowed() returns false → proof rejected → claim() reverts InvalidProof.

Recommended Mitigation

  • The deploy script values should be treated as the source of truth since they were publicly distributed to participants. Both main.nr index 8 (currently pointing to the wrong hash) and index 9 (currently a duplicate, as identified in H-01) must be corrected in a single coordinated update. After patching the circuit, regenerating Verifier.sol ensures the deployed verifier enforces the corrected list. No contract state changes are required only the circuit artifact and its derived verifier need to be updated before deployment.

  • Recompile the circuit and regenerate Verifier.sol after the fix.

- -961435057317293580094826482786572873533235701183329831124091847635547871092, // index 8 (wrong)
- -961435057317293580094826482786572873533235701183329831124091847635547871092 // index 9 (duplicate)
+ -4417726114039171734934559783368726413190541565291523767661452385022043124552, // index 8 (treasure 9 hash)
+ <correct pedersen hash of secret 9> // index 9 (treasure 10 hash)
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!