Snowman Merkle Airdrop

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

Unrestricted NFT Minting

Root + Impact

  • No access control modifiers (onlyOwner, role-based, etc.)

  • No validation of caller identity

  • No limits on minting amount

  • Complete bypass of intended airdrop mechanism

function mintSnowman(address receiver, uint256 amount) external {
for (uint256 i = 0; i < amount; i++) {
_safeMint(receiver, s_TokenCounter);
emit SnowmanMinted(receiver, s_TokenCounter);
s_TokenCounter++;
}
}

Impact:

  • Any address can mint unlimited NFTs

  • Intended flow completely circumvented

  • System integrity compromised

Description

Normal Behavior:
The Snowman NFT contract should only allow authorized entities
(specifically the SnowmanAirdrop contract) to mint NFTs after users
have staked Snow tokens and provided valid Merkle proofs and
signatures through the proper airdrop mechanism.

Specific Issue: The mintSnowman() function in Snowman.sol (lines 36-44) lacks any
access control restrictions, allowing any external address to directly
call the function and mint unlimited NFTs without staking Snow
tokens, providing proofs, or going through any validation process,
completely bypassing the intended airdrop economics and rendering the
entire system worthless.

Risk

Likelihood:

  • Any user can discover this vulnerability by simply reading the
    contract code or attempting to call the mintSnowman() function
    directly

  • Attackers will exploit this immediately upon contract deployment
    since no technical barriers or validations prevent unauthorized
    minting

Impact:

  • Complete economic collapse of the airdrop system as unlimited free
    NFTs destroy the value proposition of purchasing Snow tokens

  • Total bypass of the intended Merkle tree validation and Snow token
    staking mechanism, rendering the entire project architecture
    meaningless

Proof of Concept

The following PoC demonstrates how any unauthorized address can mint
unlimited Snowman NFTs, completely bypassing the intended airdrop
mechanism.

forge test --match-contract PoC_Simple -vvv

Test File: test/PoC_Simple.t.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Test, console2} from "forge-std/Test.sol";
import {Snowman} from "../src/Snowman.sol";
contract PoC_Simple is Test {
Snowman public snowmanNFT;
address public attacker = makeAddr("attacker");
function setUp() public {
snowmanNFT = new Snowman("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCI+PC9zdmc+");
}
function test_AttackerMintsForFree() public {
vm.startPrank(attacker);
console2.log("Attacker balance before:", snowmanNFT.balanceOf(attacker));
console2.log("Is attacker owner?", snowmanNFT.owner() == attacker);
snowmanNFT.mintSnowman(attacker, 1000);
console2.log("Attacker balance after:", snowmanNFT.balanceOf(attacker));
console2.log("ATTACKER SUCCESS: Minted 1000 NFTs for FREE");
vm.stopPrank();
}
}

Expected Output

Attacker balance before: 0
Is attacker owner? false
Attacker balance after: 1000
ATTACKER SUCCESS: Minted 1000 NFTs for FREE

Recommended Mitigation

This fix implements strict access control by restricting the
mintSnowman() function to only be callable by the designated
SnowmanAirdrop contract. The AIRDROP_CONTRACT address is set once
during deployment as an immutable variable, ensuring it cannot be
changed later. The require() statement validates that msg.sender
matches this authorized contract address before allowing any minting
operations.

This approach restores the intended architecture where users must go
through the proper airdrop flow (stake Snow tokens → validate Merkle
proofs → provide signatures) via the SnowmanAirdrop contract, which
then calls mintSnowman() as the only authorized caller. This
completely eliminates the ability for random addresses to bypass the
economic model and mint free NFTs directly

Remove this vulnerable code:
function mintSnowman(address receiver, uint256 amount) external {
for (uint256 i = 0; i < amount; i++) {
_safeMint(receiver, s_TokenCounter);
emit SnowmanMinted(receiver, s_TokenCounter);
s_TokenCounter++;
}
}
Add this secure code:
address public immutable AIRDROP_CONTRACT;
constructor(string memory _SnowmanSvgUri, address _airdropContract)
ERC721("Snowman Airdrop", "SNOWMAN")
Ownable(msg.sender)
{
require(_airdropContract != address(0), "Invalid airdrop
contract");
s_TokenCounter = 0;
s_SnowmanSvgUri = _SnowmanSvgUri;
AIRDROP_CONTRACT = _airdropContract;
}
function mintSnowman(address receiver, uint256 amount) external {
require(msg.sender == AIRDROP_CONTRACT, "Only airdrop contract can
mint");
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 5 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.