The normal behavior is one reward payout per unique treasureHash. After a successful claim, the same hash must be blocked forever.
The issue is that claim() checks claimed[_treasureHash] where _treasureHash is an uninitialized immutable (bytes32(0)), while the write path marks claimed[treasureHash]. Because the read and write keys differ, the same valid proof/hash can be replayed until MAX_TREASURES is reached.
Likelihood:
Any user with one valid proof can call claim() repeatedly with the same treasureHash and different recipients.
The exploit path is direct and requires no privileged role.
Impact:
Full 100 ETH pool can be drained using one unique treasure proof.
Legitimate winners are blocked once claimsCount reaches MAX_TREASURES.
Standalone reproduction:
Deploy TreasureHunt with 100 ether.
Use a verifier that returns true for a valid (proof, treasureHash, recipient) tuple.
Call claim(proof, HASH_1, recipient1) once.
Call claim(proof, HASH_1, recipient2) again with the same proof and same HASH_1.
Repeat until 10 successful claims. Contract balance reaches zero while only one unique hash was used.
Use the function claim(bytes calldata proof, bytes32 treasureHash, address payable recipient).
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.
The contest is complete and the rewards are being distributed.