Snowman Merkle Airdrop

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

Unlimited minting of Snowman NFTs without prior Snow token balance due to missing access controls in `Snowman::mintSnowman`

Summary

The mintSnowman function can be called by anyone in order to mint any number of Snowman NFTs without having any Snow token balance. This is possible due to the mintSnowman function missing access controls on who can execute.

Description

The Protocol has the invariant "By staking Snow tokens, users receive Snowman NFTs proportional to their Snow token holdings." However, the current implementation logic does not enforce this statement. The Snowman::mintSnowman function can be called directly by anyone who does not have any Snow tokens allowing them to freely mint any number of Snowman NFTs without a prior investment into the protocol.

This is possible due to missing access controls on the mintSnowman function - mainly onlyOwner. This function is supposed to only be invoked via the SnowmanAirdrop:claimSnowman function after the user has transferred their Snow tokens into the SnowmanAirdrop contract. Therefore the Snowman contract is expected to be owned by SnowmanAirdrop but is currently not.

Vulnerable Code

The related vulnerable function is described below:

function mintSnowman(address receiver, uint256 amount) external { //@audit-high: Broken-Invariant due to missing access control: Anyone can mint a snowman NFT. No need to hold Snow tokens
for (uint256 i = 0; i < amount; i++) {
_safeMint(receiver, s_TokenCounter);
emit SnowmanMinted(receiver, s_TokenCounter);
s_TokenCounter++;
}
}

Impact

The severity of this issue can be classified as High for the following reasons:

  • Impact: High - There is broken core invariant - users must exchange snow tokens for NFTs. This invariant is also the expected source of profit for the protocol meaning the Protocol will loose funds/profits from sales of Snow tokens.

  • Likelihood: High - the likelihood of exploitation is high due to the attack path being straightforward and no prior requirements.

Proof of Concept

As proof of the validity of this issue, I have provided a runnable PoC.

Description

  1. The script shows the attacker - Bob - holds 0 snow tokens.

  2. The mintSnowman is invoked by Bob to create 10 Snowman NFTs.

  3. The script confirms that Bob holds and owns all 10 minted Snowman NFTs.

Code

Run with: forge test --mt testUnauthorizedMintingOfSnowmanNFTs -vvv

// 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";
import {DeploySnow} from "script/DeploySnow.s.sol";
import {Snow} from "src/Snow.sol";
contract TestSnowman is Test {
Snowman nft;
Snow snow;
DeploySnowman deployer;
DeploySnow deploySnow;
address bob = makeAddr("bob");
string constant NAME = "Snowman Airdrop";
string constant SYMBOL = "SNOWMAN";
function setUp() public {
deployer = new DeploySnowman();
nft = deployer.run();
deploySnow = new DeploySnow();
snow = deploySnow.run();
}
function testUnauthorizedMintingOfSnowmanNFTs() public {
//Bob has No snow tokens
assertEq(snow.balanceOf(bob), 0);
//Bob miniting NFTs unauthorized
uint256 amount_of_NFTs_to_mint = 10;
vm.prank(bob);
nft.mintSnowman(bob, amount_of_NFTs_to_mint);
//Confirming each NFT owned by bob
for (uint256 i; i < amount_of_NFTs_to_mint; i++) {
assert(nft.ownerOf(i) == bob);
}
//Confirming how many NFTs owned by bob
assert(nft.balanceOf(bob) == amount_of_NFTs_to_mint);
//Confirming number of unauthorized NFTs minted
assert(nft.getTokenCounter() == amount_of_NFTs_to_mint);
//Logging
console2.log("Number of NFTs successfully minted unauthorized and owned by Bob: ", amount_of_NFTs_to_mint);
console2.log("Number of Snow tokens held before and after by Bob: ", snow.balanceOf(bob));
}
}

Mitigation

The recommended mitigation for this unauthorized NFT minting is to implement Access controls into the Snowman contract ensuring only SnowmanAirdrop can call mintSnowman.

The protocol logic requires that in order to mint an NFT , a user must hold Snow tokens and these are exchanged for the Snowman NFT. The transfer logic and checks are contained in the SnowmanAirdrop contract. Therefore, the owner of Snowman should be SnowmanAirdrop. This can be implemented directly in the constructor.

Furthermore, the mintSnowman function should have the onlyOwner modifier.

+ constructor(string memory _SnowmanSvgUri, address _snowManAirdrop) ERC721("Snowman Airdrop", "SNOWMAN") Ownable(_snowManAirdrop) { //@dev: added snowmanAirdrop as input
s_TokenCounter = 0;
s_SnowmanSvgUri = _SnowmanSvgUri;
}
+ function mintSnowman(address receiver, uint256 amount) external onlyOwner { //@dev: added onlyOwner modifier
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 26 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.