Wrong variable in claimed check allows same proof to be replayed, fully draining the contract.
The claim() function is designed to prevent the same treasure from
being claimed twice by checking claimed[treasureHash] before paying
out a reward.
However, the double-spend guard mistakenly checks claimed[_treasureHash]
where _treasureHash is an immutable state variable never assigned in
the constructor, making it permanently bytes32(0). Since claimed[bytes32(0)]
is never set to true, the check always passes and the same proof can
be replayed until all 100 ETH is drained.
Likelihood:
Any attacker can monitor the blockchain for the first legitimate claim() transaction and copy the proof, treasureHash, and recipient arguments directly from the calldata no special skills or tools required
The attack requires zero privileged access any externally owned account can call claim() with the copied arguments
All 10 claim slots can be exhausted in a single block since there is no cooldown, rate limiting, or time lock between callsImpact:
Impact:
The entire contract balance of 100 ETH can be drained by a single attacker using one valid proof submitted 10 times
All 9 remaining legitimate treasure finders who physically found real treasures are permanently denied their rewards since the contract is left with zero balance
The core protocol guarantee that only real treasure finders get paid is completely broken
Funds sent out via claim() are unrecoverable since no admin function can reverse completed ETH transfers
The result after test:
Replace _treasureHash with treasureHash in the double-spend guard so it checks the actual submitted treasure hash parameter instead of the permanently zero immutable variable.
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.