Normal behaviour: the Noir circuit must enforce that a submitted treasure_hash is one of the 10 pre-baked treasure commitments, so participants can only produce valid proofs for the 10 legitimate treasures hidden in the real-world hunt. The Solidity TreasureHunt.sol contract exposes a matching MAX_TREASURES = 10 and is funded with 10 × 10 ETH = 100 ETH so each legitimate finder gets a 10 ETH reward.
Specific issue: ALLOWED_TREASURE_HASHES in circuits/src/main.nr lists only 9 unique hashes — the last two entries (index 8 and index 9) are byte-for-byte identical. Cross-referencing with the canonical list in contracts/scripts/Deploy.s.sol (lines 17-26) confirms that the correct index-8 hash — -4417726114039171734934559783368726413190541565291523767661452385022043124552 (the hash of treasure secret #9) — is missing from the circuit and was replaced by a second copy of treasure #10's hash -961435057317293580094826482786572873533235701183329831124091847635547871092.
Because is_allowed(hash) iterates over the 10 slots and only returns true if hash equals one of the stored values, the hash of treasure #9 (pedersen_hash of secret 9) can never satisfy the circuit's membership check. The Noir main circuit therefore cannot produce a valid proof for treasure #9, and the Solidity claim() function can never accept one.
Likelihood:
Certain — the duplicate is compiled into the circuit at deployment time; any participant who physically finds treasure #9 and tries to claim their reward fails proof generation immediately (is_allowed returns false).
The bug is deterministic: no adversary, no timing, no edge case needed. It will manifest the first time a real participant locates treasure #9.
Impact:
10 ETH effectively locked in the contract: 10 rewards were funded but only 9 can ever be claimed. The 10th reward sits idle.
claimsCount can never reach MAX_TREASURES = 10, so the post-hunt withdraw() path (which gates on claimsCount >= MAX_TREASURES) is unreachable. The owner is forced to rely on the emergency pathway (pause() + emergencyWithdraw) to recover the stranded 10 ETH, which is not the intended operational flow.
Reputational / UX damage: the participant who locates treasure #9 in the real world believes they have found a valid treasure, invests the effort to generate a proof, and is inexplicably denied — with no way of knowing that the underlying circuit is to blame.
Inspect circuits/src/main.nr lines 55-66 and compare with contracts/scripts/Deploy.s.sol lines 17-26. The canonical list in Deploy.s.sol has 10 distinct hashes; the circuit array has ALLOWED_TREASURE_HASHES[8] == ALLOWED_TREASURE_HASHES[9].
Resulting membership check in the circuit:
Let H9 = pedersen_hash(9) be the canonical hash of treasure secret #9. Because H9 == -4417726...552 and -4417726...552 is absent from the circuit array, iterating over the 10 slots never finds a match. is_allowed(H9) returns false and the circuit's assertion assert(is_allowed(treasure_hash)) fails. No valid proof can be generated for treasure #9, so TreasureHunt.claim(proof, H9, recipient) can never succeed.
Observable end-state:
claimsCount caps at 9 (the other 9 distinct secrets still work).
address(hunt).balance permanently holds ≥ 10 ETH after all possible claims.
withdraw() reverts with HUNT_NOT_OVER forever.
Replace index 8 with the correct hash of treasure #9 and keep index 9 as the hash of treasure #10:
After the fix, the circuit accepts proofs for all 10 distinct treasures, the full 100 ETH treasury can be distributed, and claimsCount reaches MAX_TREASURES so the withdraw() post-hunt flow becomes reachable.
Additional hardening (defence in depth): add a compile-time check / unit test in the Noir project that asserts ALLOWED_TREASURE_HASHES contains 10 distinct entries (the Noir test file tests.nr already exists in the repo and is the natural place for this invariant). A similar off-chain parity test should confirm the circuit's array matches the deployment script's list element-for-element, so any future edit to one without the other is caught immediately.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.
The contest is complete and the rewards are being distributed.