The claim() function checks claimed[_treasureHash] to prevent
double-claims, but _treasureHash is an immutable field that is
never initialized in the constructor, so it permanently defaults
to bytes32(0).
Meanwhile, when a treasure IS claimed, it marks claimed[treasureHash]
(the function parameter). These are two different mapping keys — the
check always reads claimed[0x0] which is always false, so the
protection never triggers.
Likelihood:
Any caller with a valid ZK proof can call claim() repeatedly
No special access or conditions needed — exploitable immediately
on deployment
Impact:
Same treasure can be claimed unlimited times with different
recipient addresses
Each claim pays 10 ETH from the contract
All 100 ETH can be fully drained in a single transaction sequence
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.