SNARKeling Treasure Hunt

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

H-01 Broken double-claim protection in claim()

Author Revealed upon completion

Description

The claim() function is intended to prevent the same treasure from being claimed more than once through the claimed mapping. However, the current implementation checks claimed[_treasureHash] instead of claimed[treasureHash].

Since _treasureHash is declared as an immutable state variable but never initialized, the duplicate-claim check does not operate on the caller-supplied treasure identifier. As a result, the contract does not correctly enforce the one-claim-per-treasure invariant.

Risk

This issue directly affects the reward-distribution logic. A valid treasure may be claimed repeatedly, limited only by:

  • remaining contract balance

  • claimsCount < MAX_TREASURES

This can lead to repeated reward payouts for the same treasure.

Proof of Concept

An attack path is straightforward:

  1. Obtain one valid proof for a legitimate treasureHash.

  2. Call claim(proof, treasureHash, recipient) successfully.

  3. The contract sets claimed[treasureHash] = true, but future calls still check claimed[_treasureHash].

  4. Re-submit the same valid claim until the contract reaches MAX_TREASURES or runs out of funds.

The relevant flawed line is:

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

Recommended Mitigation

Update the duplicate-claim check so that it uses the function argument treasureHash, and remove the unused _treasureHash state variable.

diff --git a/contracts/src/TreasureHunt.sol b/contracts/src/TreasureHunt.sol
--- a/contracts/src/TreasureHunt.sol
+++ b/contracts/src/TreasureHunt.sol
@@ -32,7 +32,6 @@ contract TreasureHunt {
// ----- immutable config -----
IVerifier private verifier;
address private immutable owner;
- bytes32 private immutable _treasureHash;
@@ -85,7 +84,7 @@ contract TreasureHunt {
if (paused) revert ContractPaused();
if (address(this).balance < REWARD) revert NotEnoughFunds();
if (recipient == address(0) || recipient == address(this) || recipient == owner || recipient == msg.sender) revert InvalidRecipient();
if (claimsCount >= MAX_TREASURES) revert AllTreasuresClaimed();
- if (claimed[_treasureHash]) revert AlreadyClaimed(treasureHash);
+ if (claimed[treasureHash]) revert AlreadyClaimed(treasureHash);
if (msg.sender == owner) revert OwnerCannotClaim();

Support

FAQs

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

Give us feedback!