SNARKeling Treasure Hunt

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

Incorrect mapping key used in claim check allows infinite treasure claims and complete fund drain

Author Revealed upon completion

Root + Impact

Description

The intended behavior of the claim() function is to ensure that each treasureHash can only be claimed once by checking the claimed mapping before allowing a reward payout.

However, the contract incorrectly checks the mapping using _treasureHash instead of the user-supplied treasureHash. Since _treasureHash is never initialized and defaults to bytes32(0), the duplicate-claim protection is broken, allowing the same treasure to be claimed multiple times.

// Root cause in the codebase with marks to highlight the relevant section

bytes32 private immutable _treasureHash; // never initialized

function claim(bytes calldata proof, bytes32 treasureHash, address payable recipient) external nonReentrant {
// Incorrect key used for validation
if (claimed[_treasureHash]) revert AlreadyClaimed(treasureHash);

...
// Correct key used for state update (mismatch)
_markClaimed(treasureHash);

}

## Risk
Likelihood:
The issue occurs on every call to claim() because _treasureHash always evaluates to bytes32(0)
Any user with a valid proof can repeatedly call the function without restriction
Impact:
The same treasureHash can be claimed multiple times, breaking core protocol logic
An attacker can drain the entire contract balance (up to 100 ETH) by repeatedly claiming rewards
## Proof of Concept
```solidity
function testExploit_InfiniteClaimsPossible() public {
( bytes memory proof,
bytes32 treasureHash,
address payable recipient
) = _loadFixture();
uint256 beforeBal = recipient.balance;
// First claim
hunt.claim(proof, treasureHash, recipient);
// Second claim (should revert but does not)
hunt.claim(proof, treasureHash, recipient);
// Third claim
hunt.claim(proof, treasureHash, recipient);
uint256 afterBal = recipient.balance;
// Multiple rewards received incorrectly
assertEq(afterBal, beforeBal + (3 * hunt.REWARD()));
// claimsCount incorrectly increments
assertEq(hunt.claimsCount(), 3);
// Contract balance drained
assertEq(address(hunt).balance, 100 ether - (3 * hunt.REWARD()));
}
## Recommended Mitigation
```diff
- 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!