Snowman Merkle Airdrop

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

Unrestricted Access to NFT Minting Function

Description

  • The Snowman NFT contract is designed to be used exclusively by the SnowmanAirdrop contract, which enforces proper verification and claiming rules before minting NFTs to users.

  • The mintSnowman function in the Snowman contract has no access control, allowing any address to mint an unlimited number of NFTs to any recipient, completely bypassing the intended airdrop claiming process.

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

Risk

Likelihood:

  • Any external actor can call the function directly at any time, bypassing all verification mechanisms in the SnowmanAirdrop contract.

  • The vulnerability exists from deployment until the contract is upgraded or replaced.

Impact:

  • Unlimited minting of Snowman NFTs, completely undermining the tokenomics and intended distribution model.

  • The airdrop claiming process becomes meaningless as anyone can mint NFTs without going through the proper verification.

  • The value of legitimately claimed NFTs is diluted by the unlimited supply that can be minted by attackers.

Proof of Concept

function testUnrestrictedMinting() public {
// Create an attacker address
address attacker = makeAddr("attacker");
// Initial token counter should be 0
assertEq(snowman.getTokenCounter(), 0);
// Attacker can mint NFTs directly without any restrictions
vm.prank(attacker);
snowman.mintSnowman(attacker, 100);
// Attacker now has 100 Snowman NFTs
assertEq(snowman.getTokenCounter(), 100);
// Attacker can mint to any address
address victim = makeAddr("victim");
vm.prank(attacker);
snowman.mintSnowman(victim, 50);
// Total minted is now 150
assertEq(snowman.getTokenCounter(), 150);
}

Recommended Mitigation

contract Snowman is ERC721, Ownable {
// >>> ERROR
error ERC721Metadata__URI_QueryFor_NonExistentToken();
error SM__NotAllowed();
// >>> VARIABLES
uint256 private s_TokenCounter;
string private s_SnowmanSvgUri;
+ address private immutable i_airdropContract;
// >>> EVENTS
event SnowmanMinted(address indexed receiver, uint256 indexed numberOfSnowman);
// >>> CONSTRUCTOR
- constructor(string memory _SnowmanSvgUri) ERC721("Snowman Airdrop", "SNOWMAN") Ownable(msg.sender) {
+ constructor(string memory _SnowmanSvgUri, address _airdropContract) ERC721("Snowman Airdrop", "SNOWMAN") Ownable(msg.sender) {
s_TokenCounter = 0;
s_SnowmanSvgUri = _SnowmanSvgUri;
+ if (_airdropContract == address(0)) {
+ revert SM__ZeroAddress();
+ }
+ i_airdropContract = _airdropContract;
}
+ modifier onlyAirdrop() {
+ if (msg.sender != i_airdropContract) {
+ revert SM__NotAllowed();
+ }
+ _;
+ }
// >>> EXTERNAL FUNCTIONS
- function mintSnowman(address receiver, uint256 amount) external {
+ function mintSnowman(address receiver, uint256 amount) external onlyAirdrop {
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.