SNARKeling Treasure Hunt

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

### [H-1] Wrong variable used in claim() breaks duplicate protection (replay issue)

Author Revealed upon completion

### [H-1] Wrong variable used in claim() breaks duplicate protection (replay issue)

Description

  • The TreasureHunt:claim function is supposed to stop users from claiming the same treasure twice by checking the claimed mapping.

    But instead of checking the function input treasureHash, it incorrectly checks an uninitialized state variable _treasureHash.

    Since _treasureHash is never set in the constructor, it is always zero, so the check always looks at the wrong mapping key.

    @> if (claimed[_treasureHash]) revert AlreadyClaimed(treasureHash);

    Because of this mistake, the contract does not properly track whether a specific treasure has already been claimed

Risk

Likelihood:

. This issue will occur every single time the claim() function is executed.

Because _treasureHash is never initialized, it always remains bytes32(0), meaning the contract consistently checks the same incorrect mapping key regardless of the input provided by users.

As a result, duplicate claims are not blocked in any scenario, and the issue is not dependent on any specific attacker behavior, timing, or external condition.

Impact:

  • Same treasure can be claimed multiple times

  • ZK proof can be reused again and again

  • Reward funds can be drained multiple times

  • Core rule of “one claim per treasure” is broken

Proof of Concept

function test_ReplayBug_AllowsDuplicateClaims() public {
address user = address(0xBEEF);
vm.deal(user, 100 ether);
bytes32 treasureHash = keccak256("TREASURE_A");
bytes memory fakeProof = hex"1234"; // assumed valid proof for test
vm.startPrank(user);
// First claim → succeeds
treasureHunt.claim(
fakeProof,
treasureHash,
payable(user)
);
uint256 balanceAfterFirst = user.balance;
// Second claim with SAME treasureHash → also succeeds (BUG)
treasureHunt.claim(
fakeProof,
treasureHash,
payable(user)
);
uint256 balanceAfterSecond = user.balance;
vm.stopPrank();
assertEq(balanceAfterFirst, 10 ether);
assertEq(balanceAfterSecond, 20 ether);
}

Recommended Mitigation

Replace the incorrect state variable _treasureHash with the function parameter treasureHash in the mapping check so each treasure is tracked properly.

Additionally, remove the unused _treasureHash state variable from the contract entirely to prevent confusion and reduce the risk of similar logical errors in future implementations.

This issue is marked High severity because it directly breaks a core security invariant of the protocol and allows repeated valid claims, leading to potential loss of funds without any special conditions or privileged access.

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

Support

FAQs

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

Give us feedback!