SNARKeling Treasure Hunt

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

No Zero-Address Validation for newVerifier in Constructor — Verifier Can Be Set to Zero

Author Revealed upon completion

Root + Impact

Description

  • TheupdateVerifier()function allows the owner to replace the critical ZK proof verifier contract that validates all treasure claims

  • The function lacks any validation that the new verifier address is non-zero or contains contract code, allowing the owner to accidentally or maliciously set an invalid verifier that breaks the protocol or enables fund theft

// Root cause in the codebase
function updateVerifier(IVerifier newVerifier) external {
require(paused, "THE_CONTRACT_MUST_BE_PAUSED");
require(msg.sender == owner, "ONLY_OWNER_CAN_UPDATE_VERIFIER");
verifier = newVerifier; // @> No validation of newVerifier address
emit VerifierUpdated(address(newVerifier));
}
// Compare to constructor which DOES validate:
constructor(address _verifier) payable {
if (_verifier == address(0)) revert InvalidVerifier(); // @> Constructor validates
...
verifier = IVerifier(_verifier);
}

Risk

Likelihood: MEDIUM

  • This will occur when the owner accidentally passes address(0) or an EOA address during verifier update

  • This will occur when a compromised owner intentionally sets a malicious verifier to drain funds

Impact: MEDIUM to HIGH

  • Setting verifier to address(0) causes permanent DoS - all claim() calls revert

  • Setting verifier to an EOA causes unpredictable behavior and likely reverts

  • Setting verifier to a malicious contract that always returns true allows draining all 100 ETH

  • Recovery requires pausing and updating again, but damage may already be done

Proof of Concept

The vulnerability allows both accidental misconfiguration and intentional exploitation through invalid verifier addresses.

// Scenario 1: Accidental zero address (DoS)
TreasureHunt hunt = new TreasureHunt{value: 100 ether}(validVerifier);
// Owner accidentally sets verifier to zero address
vm.startPrank(owner);
hunt.pause();
hunt.updateVerifier(IVerifier(address(0))); // No validation - succeeds!
hunt.unpause();
vm.stopPrank();
// Now all claims fail
bytes memory validProof = generateValidProof();
vm.expectRevert(); // Calling address(0).verify() reverts
hunt.claim(validProof, treasureHash, recipient);
// Contract is permanently DoS'd until owner pauses and fixes
// Scenario 2: Malicious verifier (fund theft)
contract MaliciousVerifier {
// Always returns true regardless of proof
function verify(bytes calldata, bytes32[] memory) external pure returns (bool) {
return true; // @> Accept any "proof"
}
}
// Deploy malicious verifier
MaliciousVerifier evil = new MaliciousVerifier();
// Compromised owner updates to malicious verifier
vm.startPrank(compromisedOwner);
hunt.pause();
hunt.updateVerifier(IVerifier(address(evil))); // No validation - succeeds!
hunt.unpause();
vm.stopPrank();
// Now anyone can drain the contract with fake proofs
for (uint i = 0; i < 10; i++) {
bytes memory fakeProof = ""; // Empty proof
bytes32 fakeHash = keccak256(abi.encode(i));
hunt.claim(fakeProof, fakeHash, payable(attacker));
// Malicious verifier returns true, claim succeeds
// Attacker receives 10 ETH per iteration
}
// Contract drained of 100 ETH using no valid proofs
// Scenario 3: EOA address (unpredictable)
hunt.updateVerifier(IVerifier(address(0xDEADBEEF))); // EOA, not a contract
// Calls to EOA.verify() will likely revert or return unexpected data

Recommended Mitigation

Add comprehensive validation for the new verifier address to match or exceed the constructor's validation:

*// Add these validation checks:* 
- remove this code
+ require(address(newVerifier) != address(0), "INVALID_VERIFIER"); // @> Check non-zero
+ require(address(newVerifier).code.length > 0, "VERIFIER_NOT_CONTRACT"); // @> Check is con

Support

FAQs

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

Give us feedback!