SNARKeling Treasure Hunt

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

Critical Logic Error: Uninitialized _treasureHash allows unlimited reward replays

The TreasureHunt contract intended to ensure each treasure can only be claimed once using the claimed mapping. However, the claim function incorrectly checks an uninitialized immutable variable _treasureHash instead of the function parameter treasureHash. Since _treasureHash is never set in the constructor, it defaults to 0x0.

Risk- High. This logic error is present in the core claim function and will be triggered by every caller. The contract incorrectly checks an uninitialized state variable instead of the provided function argument, making the vulnerability deterministic and easy to exploit.

Likelihood:

High. This vulnerability is deterministic. The logic error exists in the core claim function and will be triggered every time the function is called. Because the contract compares the claimed status of an uninitialized variable (0x0) instead of the actual treasureHash provided by the user, the check will never function as intended for any real treasure hash.

Impact:

High. A malicious actor can drain the entire 100 ETH reward pool by replaying a single valid ZK proof. The "one reward per treasure" invariant is completely broken, allowing the same proof to be used up to MAX_TREASURES times.

impact

Proof of Concept

function test_ReplayAttackDueToWrongVariable() public {
// Первый клейм проходит успешно
vm.prank(user);
treasureHunt.claim(new bytes(0), TREASURE_HASH_1, payable(recipient1));
// Второй клейм с ТЕМ ЖЕ хешем тоже проходит, так как контракт
// проверяет неинициализированную переменную (0x0)
vm.prank(user);
treasureHunt.claim(new bytes(0), TREASURE_HASH_1, payable(recipient2));
// Если клеймов 2 — баг подтвержден
assertEq(treasureHunt.claimsCount(), 2);
}

Recommended Mitigation

Update the check to use the treasureHash parameter provided in the function call.

- if (claimed[_treasureHash]) revert AlreadyClaimed(treasureHash);
+ if (claimed[treasureHash]) revert AlreadyClaimed(treasureHash);
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!