SNARKeling Treasure Hunt

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

Invalid Treasure Hash Logic Allows Double Claims / Invariant Break

Root + Impact

Description

  • Normal behavior: Each treasure is uniquely identified by its hash and can be claimed only once. The contract should prevent duplicate claims and preserve reward invariants across claims.

The issue is that the contract uses a single immutable _treasureHash for all treasures instead of mapping each valid treasure hash. This causes any valid proof for _treasureHash to permit repeated claims against the same stored key, breaking the uniqueness invariant.

// Root cause in TreasureHunt.sol
// @> Only one immutable treasure hash is stored
bytes32 private immutable _treasureHash;
// @> Claimed mapping only tracks this one hash
mapping(bytes32 => bool) public claimed;

Risk

Likelihood:

  • Occurs whenever multiple valid treasures exist but only one is recognized in state.

Appears during normal claim() flows when proofs correspond to distinct treasure secrets but match the same _treasureHash.

Impact:

  • Attackers may forge repeated claims using different valid proofs but identical public hash binding.

Allows extractor to drain ETH rewards for all MAX_TREASURES or more.

Proof of Concept

// Pseudocode
// All proofs that validate against the same _treasureHash allow repeated claims
for (i in 1..MAX_TREASURES) {
treasureHunt.claim(proofForSomeValidSecret, sameHash, attacker);
}
// claimsCount increments each time without tracking unique treasures

Recommended Mitigation

  • Track each valid treasure hash separately using the mapping and use that key to prevent duplicates.

- remove this code
+ add this code
+mapping(bytes32 => bool) private validTreasures;
-mapping(bytes32 => bool) public claimed;
+function claim(...) external {
+ require(validTreasures[treasureHash], "Unknown treasure");
+ require(!claimed[treasureHash], "Already claimed");
// ...
+claimed[treasureHash] = true;
}
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!