Snowman Merkle Airdrop

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

Missing access control in Snowman::mintSnowman lets anyone mint unlimited reward NFTs

Description

The Snowman NFT is the airdrop's reward asset. It is intended to be minted only by SnowmanAirdrop::claimSnowman, after the caller has proven a Merkle entitlement and staked (burned) their Snow tokens.

However, Snowman::mintSnowman is declared external with no access control — no onlyOwner, and no restriction to the airdrop contract:

// src/Snowman.sol
function mintSnowman(address receiver, uint256 amount) external { // @> anyone can call this
for (uint256 i = 0; i < amount; i++) {
_safeMint(receiver, s_TokenCounter);
emit SnowmanMinted(receiver, s_TokenCounter);
s_TokenCounter++;
}
}

Because the function is callable by anyone, the entire staking / Merkle-proof path is bypassable: any address can mint any number of reward NFTs to any recipient, for free, without ever holding Snow or appearing in the Merkle tree.

Risk

Likelihood: High — the function is permissionless and directly reachable by any externally-owned account; no special conditions are required.

Impact: High — the reward asset that the whole protocol is built to distribute can be minted arbitrarily by anyone, so the scarcity, fairness, and economic meaning of the airdrop are completely destroyed.

Impact

An attacker can mint the entire intended reward supply (and beyond) to themselves at will. Legitimate claimers gain nothing of value, and the airdrop's distribution guarantees are void.

Proof of Concept

The following Foundry test passes: an attacker with no Snow balance and no Merkle entitlement mints 5 Snowman NFTs by calling mintSnowman directly (it is not the airdrop contract).

function test_F1_anyoneCanMintSnowman() public {
assertEq(snowman.getTokenCounter(), 0);
vm.prank(attacker);
snowman.mintSnowman(attacker, 5); // attacker, not the airdrop contract
assertEq(snowman.balanceOf(attacker), 5);
assertEq(snowman.getTokenCounter(), 5);
}

Recommended Mitigation

Bind the minter to the airdrop contract at construction time (or make it onlyOwner), so only the authorized claim path can mint:

+ error Snowman__NotAirdrop();
​
function mintSnowman(address receiver, uint256 amount) external {
+ if (msg.sender != i_airdrop) revert Snowman__NotAirdrop();
for (uint256 i = 0; i < amount; i++) {
_safeMint(receiver, s_TokenCounter);
emit SnowmanMinted(receiver, s_TokenCounter);
s_TokenCounter++;
}
}
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 2 hours 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!