SNARKeling Treasure Hunt

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

Wrong variable in claim() double-spend check lets attacker drain all funds with one proof

Author Revealed upon completion

claim() checks _treasureHash (uninitialized immutable) instead of treasureHash (parameter), breaking the double-claim guard and allowing full fund drain

Description

The claim() function is supposed to prevent the same treasure from being claimed twice. But at line 88, it checks the wrong variable:

// TreasureHunt.sol line 88
@> if (claimed[_treasureHash]) revert AlreadyClaimed(treasureHash);

_treasureHash here is the immutable storage variable declared on line 35, which is never assigned anywhere in the constructor. So it defaults to bytes32(0). The function then marks the correct parameter on line 104 via _markClaimed(treasureHash), but since the check and the mark operate on different keys, the guard never actually blocks a repeat claim.

Basically: the check reads claimed[bytes32(0)] (always false), while the mark writes claimed[actualHash] (irrelevant to the check). So any valid proof can be replayed over and over.

The only thing that stops it eventually is claimsCount >= MAX_TREASURES on line 87, but by that point an attacker has already called claim() 10 times with one proof and drained all 100 ETH.

Risk

Likelihood: This will happen every time someone claims a treasure. There's no special condition needed — the bug is in the normal execution path. Any participant who has one valid proof can just call claim() in a loop.

Impact: Complete loss of contract funds (100 ETH). A single participant with one valid ZK proof drains the entire reward pool, leaving nothing for the other 9 treasure finders.

Proof of Concept

function testDoubleClaim() public {
(
bytes memory proof,
bytes32 treasureHash,
address payable recipient
) = _loadFixture();
uint256 balBefore = address(hunt).balance; // 100 ETH
// claim the same treasure 10 times with the same proof
for (uint256 i = 0; i < 10; i++) {
hunt.claim(proof, treasureHash, recipient);
}
// all 100 ETH gone to one recipient
assertEq(address(hunt).balance, 0);
assertEq(recipient.balance, balBefore);
}

Note: the existing test testClaimDoubleSpendReverts in the repo has vm.expectRevert() commented out on line 144, which masks this bug — the second claim silently succeeds.

Recommended Mitigation

- if (claimed[_treasureHash]) revert AlreadyClaimed(treasureHash);
+ if (claimed[treasureHash]) revert AlreadyClaimed(treasureHash);

Also consider removing the unused _treasureHash immutable variable entirely to avoid confusion.

Support

FAQs

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

Give us feedback!