Scope: circuits/src/main.nr
The Noir circuit bakes in the set of allowed Pedersen hashes of the
treasures. It is declared as a 10-element array, but the last two entries
are identical, and the hash for the 9th treasure (secret 9) is
missing:
Cross-checking against circuits/Prover.toml.example (lines 18–29),
which the build.sh script treats as the authoritative witness source,
the intended 9th hash is:
That value is absent from the circuit's allowed set. Deploy.s.sol's
public-treasure-hashes block (lines 17–26) also enumerates the same 10
hashes and confirms -4417726... is the 9th one.
Proof: circuits/src/tests.nr::test_treasure_hunt_all_treasures_success
uses treasures = [1,2,3,4,5,6,7,8,10,10] — i.e. the authors wrote the
test around the bug (secret 9 never tested, secret 10 duplicated),
because is_allowed(pedersen_hash([9])) would return false.
Likelihood: HIGH
The bug manifests immediately on a legitimate claim attempt for
treasure 9. No special adversarial setup is required — an honest
snorkeler who finds the 9th physical treasure simply cannot produce a
valid proof.
The issue exists on every deployment of this circuit.
Impact: HIGH
Only 9 unique treasure secrets are provable, yet MAX_TREASURES
and the 100 ETH initial funding are sized for 10. The 10 ETH
reserved for the 9th treasure is unreachable via normal gameplay —
leaking the protocol's promised payout by 10%.
withdraw() requires claimsCount >= MAX_TREASURES, which is
unreachable through honest play because only 9 unique treasures can be
claimed. The owner therefore cannot reclaim the residual 10 ETH after
the hunt concludes. (emergencyWithdraw requires a pause and exists
for other reasons; it is a workaround, not the intended end-of-hunt
flow.)
The 10th physical treasure's finder and the owner both lose the
10 ETH: the finder gets nothing on-chain, and the owner cannot
reclaim via the documented post-hunt flow.
Using the values in circuits/Prover.toml.example, try to prove
treasure #9 with secret = 9:
The circuit's is_allowed(hash) check (main.nr line 30, which loops
over ALLOWED_TREASURE_HASHES) returns false because
-4417726... is not in the baked set. assert(is_allowed(...))
fails, so proof generation aborts. Secret 9 is unprovable.
Conversely, the circuit's own test file acknowledges this by using
[1, 2, 3, 4, 5, 6, 7, 8, 10, 10]:
Forward propagation:
With only 9 provable treasures, honest play caps claimsCount
at 9 (less the duplicates that the double-claim bug would
allow — but that's a separate finding).
withdraw() requires claimsCount >= MAX_TREASURES (10) →
unreachable via honest play.
Replace the duplicated entry with the intended hash of secret 9. Using
the value already in Prover.toml.example and Deploy.s.sol:
Additionally in tests.nr, restore the intended
let treasures: [Field; 10] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; so the
circuit's own unit test covers all 10 treasures. Regenerate the verifier
artifacts (circuits/scripts/build.sh) and Foundry fixtures after the
fix.
This finding was identified and written up with the assistance of an
autonomous AI auditor (Anthropic Claude).
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.