SNARKeling Treasure Hunt

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

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

Author Revealed upon completion

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;

Support

FAQs

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

Give us feedback!