The claim() function is designed to verify a ZK proof and dispense a 10 ETH reward for finding a treasure. To prevent double-claiming, it checks if the treasure has already been claimed. However, a typographical error causes the function to check the claim status of an uninitialized state variable (_treasureHash) instead of the user-provided argument (treasureHash).
Because _treasureHash is never initialized in the constructor, it defaults to bytes32(0). When a user calls claim(), the check evaluates claimed[bytes32(0)], which will always pass. Later in the function, the contract correctly marks the user-provided treasureHash as claimed. Because the check and the state update use different variables, the state update has no effect on future checks. An attacker can submit the exact same valid proof and treasureHash multiple times, bypassing the check and draining the contract.
Likelihood:
Any user who legitimately finds a single treasure (or obtains a valid proof from the mempool) can execute this attack.
The vulnerability requires no special preconditions other than possessing one valid ZK proof.
Impact:
The attacker can repeatedly call claim() in a loop until the contract is completely drained of all ETH.
The intended MAX_TREASURES limit (10) will be reached, but the attacker will receive all 100 ETH instead of the intended 10 ETH per treasure, stealing funds meant for other participants.
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.