Snowman Merkle Airdrop

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

Unrestricted Snowman.mintSnowman allows anyone to mint unlimited NFTs, fully bypassing the SnowmanAirdrop Merkle/signature/staking economy

Description

Snowman.mintSnowman is declared external with no access control whatsoever — no onlyOwner, no onlyAirdrop, no caller check. Any account can call it directly and mint any number of Snowman NFTs to any address without holding Snow tokens, without a Merkle proof, and without a valid signature.

The intended flow requires passing through SnowmanAirdrop.claimSnowman, which enforces Merkle tree membership, EIP-712 signature verification, and Snow token staking. However, Snowman.mintSnowman is directly callable by anyone:

contract Snowman is ERC721, Ownable {
error SM__NotAllowed(); // declared, never used
function mintSnowman(address receiver, uint256 amount) external { // no modifier
for (uint256 i = 0; i < amount; i++) {
_safeMint(receiver, s_TokenCounter);
s_TokenCounter++;
}
}
}

The unused SM__NotAllowed error is a clear indicator that access control was intended but never implemented.

Risk

Total break of NFT supply control. The entire Snowman collection becomes infinitely mintable by any anonymous account at zero cost beyond gas. The downstream SnowmanAirdrop machinery — Merkle verification, signature verification, Snow staking — is rendered economically meaningless. An attacker can also front-run legitimate claimants to grab the lowest tokenIds since s_TokenCounter starts at 0.

Proof of Concept

function test_AnyoneCanMintUnlimitedSnowman() public {
address attacker = makeAddr("attacker");
assertEq(nft.balanceOf(attacker), 0);
vm.prank(attacker);
nft.mintSnowman(attacker, 1000);
assertEq(nft.balanceOf(attacker), 1000);
assertEq(nft.ownerOf(0), attacker);
assertEq(nft.ownerOf(999), attacker);
}

Result: [PASS] — attacker holds 1,000 freshly-minted Snowman NFTs without any Snow tokens, Merkle proof, or signature.

Recommended Mitigation

Restrict mintSnowman so it can only be called by the SnowmanAirdrop contract. The contract already declares an unused error SM__NotAllowed() — clearly intended for this gate:

+ address private immutable i_airdrop;
+ constructor(string memory _SnowmanSvgUri, address _airdrop)
+ ERC721("Snowman Airdrop", "SNOWMAN") Ownable(msg.sender) {
+ s_TokenCounter = 0;
+ s_SnowmanSvgUri = _SnowmanSvgUri;
+ i_airdrop = _airdrop;
+ }
+ modifier onlyAirdrop() {
+ if (msg.sender != i_airdrop) revert SM__NotAllowed();
+ _;
+ }
- function mintSnowman(address receiver, uint256 amount) external {
+ function mintSnowman(address receiver, uint256 amount) external onlyAirdrop {
Updates

Lead Judging Commences

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