Describe the normal behavior in one or more sentences
Explain the specific issue or problem in one or more sentences
The TreasureHunt contract declares bytes32 private immutable _treasureHash but never assigns it in the constructor. It permanently equals bytes32(0).
The double-spend guard in claim() reads claimed[_treasureHash] — this always checks the zero slot, which is never set to true. Meanwhile _markClaimed(treasureHash) correctly writes the parameter hash — but the guard never reads that slot. The protection is a permanent no-op.
Description
Normal behavior: after a treasure is claimed, claimed[treasureHash] is set to true, and subsequent calls with the same hash should revert with AlreadyClaimed.
Actual behavior: the guard checks claimed[bytes32(0)] which is always false, so the same proof can be replayed indefinitely until claimsCount reaches 10.
Likelihood:
Reason 1 // Describe WHEN this will occur (avoid using "if" statements)
Reason 2
Impact:
Impact 1
Impact 2
Likelihood:
Any holder of a single valid ZK proof can exploit this immediately with no special tools
The attack requires only calling claim() 10 times in sequence — no flashloan, no special contract needed
Impact:
A single treasure secret drains the entire 100 ETH contract balance (10 claims x 10 ETH)
All 10 claim slots are consumed, permanently locking out legitimate treasure finders
The hunt organizer loses all deposited funds
In `claim()`, the guard uses `claimed[_treasureHash]`, where `_treasureHash` is an immutable state variable that is never initialized to the caller-supplied treasure identifier, while the contract later marks `claimed[treasureHash] = true` using the function argument instead. As a result, the duplicate-claim check and the state update are performed against different keys, which means a previously claimed treasure is not actually blocked from being claimed again with the same valid proof and `treasureHash`. This breaks a core invariant of the protocol described in the README, namely, that each treasure can only be redeemed once, and allows one valid treasure/proof pair to be reused to drain rewards repeatedly until either the `MAX_TREASURES` cap or the contract balance is exhausted.
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.