SNARKeling Treasure Hunt

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

Uninitialized immutable field makes double-claim check ineffective

Author Revealed upon completion

Root + Impact

Description

Line 35 declares the immutable field:

bytes32 private immutable _treasureHash;

But the constructor (lines 67-75) NEVER initializes it:

constructor(address _verifier) payable {
if (_verifier == address(0)) revert InvalidVerifier();
owner = msg.sender;
verifier = IVerifier(_verifier);
paused = false;
// _treasureHash is left uninitialized!
}

In Solidity, uninitialized immutable fields default to 0x0000...0000.
The double-claim check at line 88 becomes if (claimed[0x0000...0000]) revert
this checks if 0x0 is claimed, but 0x0 is never marked as claimed.
Meanwhile, actual treasures ARE marked as claimed, but the check bypasses them.

This creates state inconsistency:

  • claimed[treasureHash] = true (marked correctly)

  • claimed[_treasureHash] = always false (check uses this wrong key)

Risk

Likelihood:

  • Triggered on every single claim() call from deployment

  • No special conditions needed — uninitialized immutable is permanent

Impact:

  • Double-claim check is permanently bypassed

  • Same treasure can be claimed unlimited times

  • All 100 ETH in the contract is at risk of full drainage

Proof of Concept

This test proves the state inconsistency. After a treasure is claimed,
claimed[treasureHash] is correctly true, but claimed[bytes32(0)]
which is what line 88 actually checks — remains false, so the next
claim for the same treasure still succeeds.

function testVerify_StateInconsistency() public {
// Before claim
assertFalse(hunt.claimed(treasureHash));
assertFalse(hunt.claimed(bytes32(0))); // _treasureHash defaults to 0x0
// Claim the treasure
vm.prank(participant);
hunt.claim(proof, treasureHash, payable(recipient));
// After claim
assertTrue(hunt.claimed(treasureHash)); // Correctly marked
assertFalse(hunt.claimed(bytes32(0))); // But this is what's actually checked!
}

Recommended Mitigation

The field is fundamentally broken — never initialized, never set, and
only used in the broken check. Deleting it is the cleanest fix.

- bytes32 private immutable _treasureHash;

Option 2 would be to initialize it in the constructor, but the field
has no valid purpose so deletion is recommended.

Support

FAQs

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

Give us feedback!