SNARKeling Treasure Hunt

First Flight #59
Beginner FriendlyGameFiFoundry
100 EXP
View results
Submission Details
Severity: high
Valid

Incorrect Use of Uninitialized Immutable Breaks Claim Tracking Logic

Description

The contract incorrectly uses an uninitialized immutable variable _treasureHash when checking whether a treasure has already been claimed.

Instead of using the function parameter treasureHash, the contract performs the check using _treasureHash, which is never assigned in the constructor and therefore defaults to bytes32(0).

As a result, the duplicate claim protection does not correctly apply to individual treasures, and the mapping logic becomes inconsistent with the intended design.

This breaks the invariant that each treasure should be uniquely claimable only once.


Risk

The contract incorrectly uses an uninitialized immutable variable _treasureHash when checking whether a treasure has already been claimed.

Instead of using the function parameter treasureHash, the contract performs the check using _treasureHash, which is never assigned in the constructor and therefore defaults to bytes32(0).

This breaks the intended claim uniqueness logic and causes incorrect state tracking for treasure claims.

Impact:

  • Claim tracking is incorrect

  • Only bytes32(0) is checked for duplication

  • Real treasure hashes are not properly protected

  • Claim logic diverges from intended design

Severity:

High


๐Ÿงช Proof of Concept

The issue occurs because the contract checks an uninitialized storage variable instead of the input parameter.


Step 1 โ€” User submits a valid claim

claim(proof, treasureHash, recipient);

Step 2 โ€” Incorrect state check is performed

Inside the claim function:

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

However:

  • _treasureHash is never initialized

  • It defaults to bytes32(0)


Step 3 โ€” Actual behavior

The contract always evaluates:

claimed[bytes32(0)]

instead of:

claimed[treasureHash]

Step 4 โ€” Result

  • Only the zero hash key is ever checked

  • Real treasure hashes are not properly tracked

  • Claim uniqueness enforcement is broken


๐Ÿ› ๏ธ Recommended Mitigation

The contract should correctly use the function parameter treasureHash for claim tracking.


โœ… Fix

Replace:

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

With:

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

๐Ÿงน Optional Improvement

Remove the unused variable to prevent confusion:

bytes32 private immutable _treasureHash;

๐Ÿง  Expected Outcome After Fix

  • Each treasure is tracked correctly

  • Duplicate claims are properly prevented

  • Contract behavior matches intended design

Updates

Lead Judging Commences

s3mvl4d Lead Judge 18 days ago
Submission Judgement Published
Validated
Assigned finding tags:

broken double-claim protection

In `claim()`, the guard uses `claimed[_treasureHash]`, where `_treasureHash` is an immutable state variable that is never initialized to the caller-supplied treasure identifier, while the contract later marks `claimed[treasureHash] = true` using the function argument instead. As a result, the duplicate-claim check and the state update are performed against different keys, which means a previously claimed treasure is not actually blocked from being claimed again with the same valid proof and `treasureHash`. This breaks a core invariant of the protocol described in the README, namely, that each treasure can only be redeemed once, and allows one valid treasure/proof pair to be reused to drain rewards repeatedly until either the `MAX_TREASURES` cap or the contract balance is exhausted.

Support

FAQs

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

Give us feedback!