SNARKeling Treasure Hunt

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

[H-1] Mismatched variable in claimed mapping allows unlimited reward claiming and full contract drain

Author Revealed upon completion

Root + Impact

Description

  • The claim function is intended to prevent users from claiming the same treasure multiple times by checking a claimed mapping. Under normal behavior, a successful claim should mark the specific treasure hash as "true" in the mapping and revert any future attempts using that same hash.


  • However, a critical logic error exists where the duplicate check and the storage update use different variables. The contract checks _treasureHash (an incorrect or undefined identifier) but updates the mapping using treasureHash (the actual function parameter).Describe the normal behavior in one or more sentences


// @> The check reads from the wrong mapping key (_treasureHash)
if (claimed[_treasureHash]) revert AlreadyClaimed(treasureHash);
// @> The update writes to the correct key (treasureHash)
_markClaimed(treasureHash);h @> marks to highlight the relevant section

Risk

Likelihood:

  • Reason 1 This vulnerability is present in the core claim logic and is triggered every time a user attempts to claim a reward.

  • Reason 2 The duplicate check always evaluates to false because the slot being checked is never the slot being written to.

Impact:

  • Impact 1 An attacker can completely drain the contract's ETH balance with only one legitimate treasure discovery.

  • Impact 2 A single valid ZK proof can be reused infinitely by a single participant.

Proof of Concept

function test_replayAttack() public {
address attacker = makeAddr("attacker");
// Standard setup with one valid proof and its corresponding hash
bytes memory validProof = new bytes(0);
bytes32 treasureHash = bytes32(0);
uint256 contractBalanceBefore = address(treasureHunt).balance;
// First claim - legitimate
vm.prank(attacker);
treasureHunt.claim(validProof, treasureHash, payable(attacker));
// Second claim - the replay attack
// This should revert but passes due to the variable mismatch in the mapping check
vm.prank(attacker);
treasureHunt.claim(validProof, treasureHash, payable(attacker));
// Assert that the contract has been billed twice for the same treasure discovery
assertEq(address(treasureHunt).balance, contractBalanceBefore - 2 ether);
}
POC EXPLANATION
Double Claim Vulnerability.
i initialize the TreasureHunt contract and simulate a legitimate first claim by an attacker using a valid ZK proof.
Immediately after, the attacker calls claim() a second time with the exact same parameters.
Under secure conditions, the second call should revert with AlreadyClaimed. However, because the contract checks the wrong mapping key (_treasureHash instead of the actual treasureHash), the check passes.
The test uses an assertEq to prove that the contract's balance has decreased by twice the reward amount, confirming that the funds were successfully drained via replay.

Recommended Mitigation

- if (claimed[_treasureHash]) revert AlreadyClaimed(treasureHash);
+ if (claimed[treasureHash]) revert AlreadyClaimed(treasureHash);
_markClaimed(treasureHash);
MITIGATION EXPLANATION
To resolve this vulnerability, the contract must ensure that the identifier used to verify the claim status is the same identifier used to mark the claim as completed.
The fix involves replacing the incorrect identifier _treasureHash in the if statement with the function parameter treasureHash. This ensures the contract correctly points to the storage slot that is updated during the first successful claim, effectively enabling the intended replay protection.

Support

FAQs

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

Give us feedback!