Snowman Merkle Airdrop

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

Unrestricted Minting Vulnerability Enables Infinite NFT Inflation In Snowman::mintSnowman

Description

The mintSnowman function has no access controls, allowing any external address to mint arbitrary quantities of NFTs to any recipient. This violates the protocol's design where Snowman NFTs should only be minted as rewards for staking Snow tokens via the SnowmanAirdrop contract.

Impact:

  • Infinite NFT Supply: Anyone can mint unlimited NFTs, destroying scarcity

  • Economic Collapse: Renders NFTs worthless (0 market value)

  • Gas Griefing Attack: Malicious actors can mint NFTs to victims' wallets

  • Protocol Integrity Failure: Breaches core tokenomics (NFTs should represent staked value)

Risk

Likelihood:

• Exploitable by any Ethereum address
• No financial cost to attack

Impact:

• Permanent destruction of NFT value
• Protocol tokenomics rendered useless

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "forge-std/Test.sol";
import "../src/Snowman.sol";
contract SnowmanTest is Test {
Snowman snowman;
address deployer = makeAddr("deployer");
address attacker = makeAddr("attacker");
address victim = makeAddr("victim");
function setUp() public {
vm.prank(deployer);
snowman = new Snowman("ipfs://Qm...");
}
// Vulnerability Proof-of-Concept
function testUnrestrictedMintingAttack() public {
uint256 initialSupply = snowman.getTokenCounter();
// 1. Attacker mints 10k NFTs to victim's address
vm.prank(attacker);
snowman.mintSnowman(victim, 10_000);
// 2. Verify victim received unwanted NFTs
assertEq(snowman.balanceOf(victim), 10_000);
// 3. Attacker inflates supply by 50k NFTs
vm.prank(attacker);
snowman.mintSnowman(attacker, 50_000);
// 4. Verify total supply explosion
uint256 finalSupply = snowman.getTokenCounter();
assertEq(finalSupply, initialSupply + 60_000);
// 5. Log attack results
console.log("[+] Initial NFT supply: ", initialSupply);
console.log("[+] Victim balance: ", snowman.balanceOf(victim));
console.log("[+] Attacker balance: ", snowman.balanceOf(attacker));
console.log("[+] Final NFT supply: ", finalSupply);
console.log("[+] Attack cost: ", tx.gasprice * (gasleft() + 50000), "wei");
}
}

Recommended Mitigation

// STEP 1: Add access control modifier
modifier onlyMinter() {
if (msg.sender != s_airdropContract) revert SM__NotAllowed();
_;
}
// STEP 2: Implement batch size limit
function mintSnowman(address receiver, uint256 amount) external onlyMinter {
require(amount <= 100, "Exceeds batch limit"); // Prevent gas griefing
for (uint256 i = 0; i < amount; i++) {
_safeMint(receiver, s_TokenCounter);
emit SnowmanMinted(receiver, s_TokenCounter);
s_TokenCounter++;
}
}
// STEP 3: Initialize minter in constructor
constructor(string memory _SnowmanSvgUri, address airdropContract)
ERC721("Snowman Airdrop", "SNOWMAN") Ownable(msg.sender)
{
s_TokenCounter = 0;
s_SnowmanSvgUri = _SnowmanSvgUri;
s_airdropContract = airdropContract; // Set authorized minter
}
  • Token Counter Vulnerability
    ** Use OpenZeppelin's Counters utility:

using Counters for Counters.Counter;
Counters.Counter private s_TokenCounter;
// In minting:
uint256 tokenId = s_TokenCounter.current();
s_TokenCounter.increment();
  • Add check for zero-address receiver

Fix:

require(receiver != address(0), "Invalid receiver");
Updates

Lead Judging Commences

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