Snowman Merkle Airdrop

AI First Flight #10
Beginner FriendlyFoundrySolidityNFT
EXP
View results
Submission Details
Impact: low
Likelihood: low
Invalid

`SnowmanAirdrop` constructor does not validate `_merkleRoot != bytes32(0)`, risking a permanently non-functional airdrop if misconfigured at deployment

`SnowmanAirdrop` constructor does not validate `_merkleRoot != bytes32(0)`, risking a permanently non-functional airdrop if misconfigured at deployment

Description

`SnowmanAirdrop`'s constructor validates that `_snow` and `_snowman` are non-zero addresses, but performs no equivalent check on `_merkleRoot`. Since `i_merkleRoot` is immutable, a deployment with an accidentally-zero root cannot be corrected after the fact. Every call to `claimSnowman` would fail `MerkleProof.verify` permanently, requiring a full contract redeployment and migration to recover.

constructor(bytes32 _merkleRoot, address _snow, address _snowman) EIP712("Snowman Airdrop", "1") {
if (_snow == address(0)) {
revert SA__ZeroAddress();
}
if (_snowman == address(0)) {
revert SA__ZeroAddress();
}
i_merkleRoot = _merkleRoot; // no validation at all
i_snow = Snow(_snow);
i_snowman = Snowman(_snowman);
}

Risk

Likelihood:

  • Low. This requires a deployment-time operator error rather than any attacker action, and is not something a malicious third party can trigger — it depends entirely on correct off-chain deployment tooling and process discipline.


Impact:

A deployment misconfiguration, unset environment variables, or copy-paste errors, all of which are realistic operational risks rather than purely theoretical ones, results in a completely non-functional airdrop contract with no on-chain recovery path. The only remedy would be a full redeployment of `SnowmanAirdrop`.

Proof of Concept

Add the folllowing test to the `TestSnowmanAirdrop.t.sol`:

function test_ZeroMerkleRoot_PermanentlyBricksAllClaims() public {
// Deploy with an accidentally-zero root — the constructor accepts this without complaint.
SnowmanAirdrop brokenAirdrop = new SnowmanAirdrop(bytes32(0), address(snow), address(nft));
assertEq(brokenAirdrop.getMerkleRoot(), bytes32(0), "Constructor accepted a zero root with no revert");
// Any legitimate claim attempt is now permanently impossible.
vm.prank(alice);
snow.approve(address(brokenAirdrop), 1);
bytes32 alDigest = brokenAirdrop.getMessageHash(alice);
(uint8 alV, bytes32 alR, bytes32 alS) = vm.sign(alKey, alDigest);
vm.expectRevert(SnowmanAirdrop.SA__InvalidProof.selector);
brokenAirdrop.claimSnowman(alice, AL_PROOF, alV, alR, alS);
}

Run the test in the terminal:

forge test --mt test_ZeroMerkleRoot_PermanentlyBricksAllClaims -vvvv

If the test passes, the vulnerability exists.

Recommended Mitigation

Add a corresponding validation check to the constructor for the merkel root:

+ error SA__ZeroMerkleRoot();
constructor(bytes32 _merkleRoot, address _snow, address _snowman) EIP712("Snowman Airdrop", "1") {
+ if (_merkleRoot == bytes32(0)) {
+ revert SA__ZeroMerkleRoot();
+ }
if (_snow == address(0)) {
revert SA__ZeroAddress();
}
if (_snowman == address(0)) {
revert SA__ZeroAddress();
}
i_merkleRoot = _merkleRoot;
i_snow = Snow(_snow);
i_snowman = Snowman(_snowman);
}
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 1 day ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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

Give us feedback!