SNARKeling Treasure Hunt

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

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

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
Updates

Lead Judging Commences

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

no zero-address check in updateVerifier()

The issue is that `updateVerifier()` allows the owner to replace the verifier with an arbitrary address, including `address(0)`, even though the constructor explicitly treats a zero verifier as invalid and reverts with `InvalidVerifier()` during initial deployment. In other words, the contract establishes at deployment time that a null verifier address is not an acceptable configuration, but then fails to preserve that same invariant when the verifier is later updated through the admin recovery path.

Support

FAQs

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

Give us feedback!