Scope: contracts/src/TreasureHunt.sol
TreasureHunt.claim() is supposed to prevent the same treasure from being
claimed twice, but the per-treasure uniqueness check reads the wrong state
variable.
The contract declares
and never assigns it — there is no assignment in the constructor, and it is
not a function parameter. In Solidity 0.8.x an unset immutable defaults
to the zero value, so _treasureHash == bytes32(0) for every deployment.
Inside claim():
The duplicate-claim guard therefore only triggers once the attacker has
claimed bytes32(0) as a treasure hash — which a legitimate proof never
targets — while the claimed[...] mapping is correctly populated with the
real treasureHash but never read.
Likelihood: HIGH
The bug is reachable on any claim path — the wrong lookup happens in the
only function that distributes ETH.
It is triggered by normal usage: anyone who finds a single physical
treasure can call claim() repeatedly with different recipient
arguments, producing a valid ZK proof for each (the proof only binds
(treasureHash, recipient); the secret is reusable).
Impact: HIGH
Direct theft of up to REWARD * MAX_TREASURES = 10 × 10 = 100 ETH (the
entire funded pool) from a single honestly-found secret.
The claimsCount >= MAX_TREASURES ceiling is the only remaining
safety net; it caps the drain at 100 ETH but does not prevent it.
The PoC below deploys TreasureHunt with a mock verifier (any valid
verifier is equivalent here because the bug is a pure state-management
issue in TreasureHunt.sol independent of ZK proof contents). Using a
SINGLE valid (proof, treasureHash) tuple, an attacker with 10 recipient
EOAs drains the entire 100 ETH pool.
Running this test results in 100 ETH drained with 10 successful claim()
calls reusing one treasure.
Read the duplicate-claim check against the function parameter, and remove
the unused _treasureHash immutable:
Consider adding a post-mitigation regression test (see the existing
testClaimDoubleSpendReverts in contracts/test/TreasureHunt.t.sol,
which has its vm.expectRevert() commented out — uncomment it after
fixing the bug).
This finding was identified and written up with the assistance of an
autonomous AI auditor (Anthropic Claude). The bug, the PoC, and the
mitigation were manually reviewed for correctness against the contest
repo snapshot.
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.