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 allows anyone to mint unlimited NFTs for free

Root + Impact

Snowman::mintSnowman is external with no access control, so anyone can mint arbitrary Snowman NFTs directly, bypassing the entire Snow-staking + Merkle + signature airdrop.

Description

The protocol intends that only the SnowmanAirdrop contract mints Snowman NFTs (after a user stakes/"burns" their Snow in claimSnowman). But mintSnowman has no onlyOwner / onlySnowmanAirdrop guard, so any address can mint any number of NFTs to anyone without owning Snow or providing a valid Merkle proof or signature.

function mintSnowman(address receiver, uint256 amount) external {
// @> no access control — ANY caller can mint
for (uint256 i = 0; i < amount; i++) {
_safeMint(receiver, s_TokenCounter);
emit SnowmanMinted(receiver, s_TokenCounter);
s_TokenCounter++;
}
}

Risk

Likelihood: High — any external account can call it at any time, with no preconditions.

Impact: High — unlimited free minting; NFT supply is uncapped, the staking/airdrop is fully bypassable, the collection is rendered worthless, and all legitimate recipients are diluted.

Proof of Concept

Add to test/TestSnowmanAirdrop.t.sol and run forge test --mt test_AnyoneCanMintSnowmanForFree. The test passes — 1000 NFTs are minted to an address that holds zero Snow:

function test_AnyoneCanMintSnowmanForFree() public {
address attacker = makeAddr("attacker");
assertEq(snow.balanceOf(attacker), 0); // owns ZERO Snow
vm.prank(attacker);
nft.mintSnowman(attacker, 1000); // no Snow, no proof, no signature
assertEq(nft.balanceOf(attacker), 1000); // 1000 NFTs minted for free
}

Recommended Mitigation

Restrict minting to the airdrop contract:

+ error SM__NotAirdrop();
+ address private i_airdrop;
+
+ function setAirdrop(address _airdrop) external onlyOwner {
+ i_airdrop = _airdrop;
+ }
​
function mintSnowman(address receiver, uint256 amount) external {
+ if (msg.sender != i_airdrop) revert SM__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!