claim() checks _treasureHash (uninitialized immutable) instead of treasureHash (parameter), breaking the double-claim guard and allowing full fund drainThe claim() function is supposed to prevent the same treasure from being claimed twice. But at line 88, it checks the wrong variable:
_treasureHash here is the immutable storage variable declared on line 35, which is never assigned anywhere in the constructor. So it defaults to bytes32(0). The function then marks the correct parameter on line 104 via _markClaimed(treasureHash), but since the check and the mark operate on different keys, the guard never actually blocks a repeat claim.
Basically: the check reads claimed[bytes32(0)] (always false), while the mark writes claimed[actualHash] (irrelevant to the check). So any valid proof can be replayed over and over.
The only thing that stops it eventually is claimsCount >= MAX_TREASURES on line 87, but by that point an attacker has already called claim() 10 times with one proof and drained all 100 ETH.
Likelihood: This will happen every time someone claims a treasure. There's no special condition needed — the bug is in the normal execution path. Any participant who has one valid proof can just call claim() in a loop.
Impact: Complete loss of contract funds (100 ETH). A single participant with one valid ZK proof drains the entire reward pool, leaving nothing for the other 9 treasure finders.
Note: the existing test testClaimDoubleSpendReverts in the repo has vm.expectRevert() commented out on line 144, which masks this bug — the second claim silently succeeds.
Also consider removing the unused _treasureHash immutable variable entirely to avoid confusion.
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.