SNARKeling Treasure Hunt

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

Critical Logic Error: Uninitialized _treasureHash allows unlimited reward replays

Author Revealed upon completion

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);

Support

FAQs

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

Give us feedback!