SNARKeling Treasure Hunt

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

[H-02] Permanent lock of funds in TreasureHunt due to unattainable claimsCount

Root + Impact

Description

  • e TreasureHunt::withdraw function allows the owner to recover any remaining ETH once the hunt is finished. The completion criteria is defined
    as claimsCount >= MAX_TREASURES (where MAX_TREASURES is 10)

  • The issue is a logical deadlock caused by the duplicate hash in the Noir circuit (Finding [H-01]). Since the circuit only allows 9 unique
    treasures to be proved, and the contract (assuming the double-spend bug is fixed) only allows each hash to be claimed once, the claimsCount can
    never exceed 9. Consequently, the require statement in withdraw will always fail

function withdraw() external {
// @> Root Cause: claimsCount will max out at 9 due to circuit limitations
require(claimsCount >= MAX_TREASURES, "HUNT_NOT_OVER");
uint256 balance = address(this).balance;
// ...
}

Risk

Likelihood:

  • This is a guaranteed outcome if the Noir circuit bug is not fixed, as the claimsCount state variable is strictly incremented per successful
    claim

Impact:

  • Locked Funds: The owner will be unable to retrieve the remaining 10 ETH (allocated for the 10th treasure) or any excess funding. This results
    in a permanent loss of funds for the organizer

Proof of Concept

This test simulates the end-state of the hunt.

  1. We simulate 9 unique successful claims (the maximum possible unique claims).

  2. We verify claimsCount is 9 and 10 ETH remains.

  3. We call withdraw() as the owner and confirm it reverts with "HUNT_NOT_OVER", proving that the owner can never recover the final 10 ETH even
    after all possible treasures are found.

function test_WithdrawalPermanentLock() public {
// Claim all 9 unique treasures possible
for (uint256 i = 0; i < 9; i++) {
hunt.claim("", keccak256(abi.encode(i)), payable(address(uint160(0x100 + i))));
}
// Owner tries to withdraw the remaining 10 ETH
vm.prank(owner);
vm.expectRevert("HUNT_NOT_OVER");
hunt.withdraw();
}

Recommended Mitigation

The primary mitigation is to fix the Noir circuit bug (Finding [H-01]). Additionally, it is recommended to add a time-based recovery mechanism
(e.g., block.timestamp > huntEndTime) to the withdraw function so the owner can recover funds if some treasures are never found physically.

+ uint256 public immutable huntEndTime;
- constructor(address _verifier) payable {
+ constructor(address _verifier, uint256 _duration) payable {
if (_verifier == address(0)) revert InvalidVerifier();
owner = msg.sender;
verifier = IVerifier(_verifier);
paused = false;
+ huntEndTime = block.timestamp + _duration;
}
function withdraw() external {
- require(claimsCount >= MAX_TREASURES, "HUNT_NOT_OVER");
+ require(claimsCount >= MAX_TREASURES || block.timestamp > huntEndTime, "HUNT_NOT_OVER");
uint256 balance = address(this).balance;
Updates

Lead Judging Commences

s3mvl4d Lead Judge
20 days ago
s3mvl4d Lead Judge 18 days ago
Submission Judgement Published
Validated
Assigned finding tags:

unclaimable treasure / bricked withdraw path

The issue stems from a mismatch between the circuit and the contract’s economic assumptions: the Solidity contract is configured for `MAX_TREASURES = 10` and only allows the owner to call `withdraw()` once `claimsCount >= MAX_TREASURES`, while the Noir circuit’s baked-in `ALLOWED_TREASURE_HASHES` array does not actually contain ten distinct treasures because one hash is duplicated and another expected hash is missing. As a result, under the intended one-claim-per-treasure design described in the README, there are only nine uniquely claimable treasures even though the system is funded and accounted as if ten rewards can be legitimately redeemed. That creates two linked consequences from the same root cause: first, one treasure is effectively unclaimable because no valid proof can ever be generated for the missing allowed hash, and second, the normal “hunt over” withdrawal path becomes bricked because honest participants can never reach ten legitimate unique claims, leaving the post-hunt fund recovery logic via `withdraw` function permanently unreachable. The owner can still intervene through the emergency path.

Support

FAQs

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

Give us feedback!