TreasureHunt::claim permanently disables replay protection, allowing a single proof to drain all 100 ETHA recipient is supposed to be able to claim one treasure reward, after submitting a zero-knowledge proof showing they know the correct treasure secret in TreasureHunt::claim.
The duplicate-claim check in TreasureHunt::claim() reads from claimed[_treasureHash], where _treasureHash is a private immutable set at construction. However, _markClaimed writes to claimed[treasureHash], where treasureHash is the calldata argument passed by the caller. Because the check and the write operate on different keys, claimed[_treasureHash] starts false and is never set to true. The replay protection is permanently disabled.
Likelihood:
claimed[_treasureHash] checks the immutable _treasureHash key which is never written to by _markClaimed, then writes to claimed[treasureHash] (the calldata arg), meaning the duplicate guard never triggers on every single call.
A fresh caller replaying the same valid proof MAX_TREASURES times will always succeed, as the replay protection is permanently disabled from the moment of deployment.
Impact:
A single valid (proof, treasureHash, recipient) tuple can be replayed MAX_TREASURES times in sequence, draining the full 100 ETH from the contract.
The claimsCount counter does bound the damage to 10 calls, but within those 10 slots anyone's proof can sweep every reward from the contract.
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.