Normally, after the treasure hunt concludes, the owner is expected to call withdraw() to retrieve any leftover/unclaimed ETH once claimsCount >= MAX_TREASURES (10), at which point the hunt is considered "over".
The specific issue is that withdraw() has a hard-coded dependency on all 10 treasures being successfully claimed. However, the Noir circuit hard-codes ALLOWED_TREASURE_HASHES with a duplicate entry (the last two hashes are identical), so at most 9 unique treasures can ever be claimed. Even without the duplicate, a real-world treasure hunt may naturally result in fewer than 10 claims (not every treasure is found). In either case, claimsCount can never reach 10, making the only intended withdrawal path for the owner permanently impossible.
(Note: the duplicate is in circuits/src/main.nr, but the vulnerability manifests directly in TreasureHunt.sol.)
Likelihood:
The duplicate hash is permanently baked into the deployed circuit, guaranteeing claimsCount can never exceed 9
Even if the circuit were fixed, treasure hunts are inherently incomplete by nature. Some treasures may simply never be found/claimed
Impact:
Owner cannot recover any remaining ETH (e.g. the unclaimed portion of the initial 100 ETH funding or any extra fund() calls)
Funds are permanently locked in the contract unless the owner uses the restricted emergencyWithdraw (which requires pausing the entire contract first, halting all future claims)
Because the ALLOWED_TREASURE_HASHES array in the Noir circuit contains a duplicate value (the last two entries are identical), there are only 9 distinct treasures that can ever be successfully claimed. The contract tracks claims via a mapping, so attempting to claim the duplicate hash reverts or is ignored. Even after claiming every possible unique treasure, claimsCount remains stuck at 9. The withdraw() function strictly requires claimsCount >= 10 (which is mathematically impossible), permanently blocking the owner from recovering any leftover ETH through the normal path. This reproduces the bug in any testing environment (Foundry/Hardhat) or on a live deployment.
Alternatively, add a time-based unlock (e.g. huntEndTime set in constructor) or make emergencyWithdraw usable without requiring the contract to be paused.
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.
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.