Snowman Merkle Airdrop

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

Unauthorized Minting of NFTs Due to Missing Access Control in mintSnowman

Root + Impact

Description

  • The mintSnowman function in the Snowman The ERC721 contract is publicly accessible. Under normal behavior, this function should only be called by a trusted contract, such as SnowmanAirdrop to mint NFTs for eligible users who have staked Snow tokens.

  • The specific issue is that there is no access control in place. As a result, any external account can call mintSnowman and mint any number of NFTs to any address, without staking or validation. This completely bypasses the staking and eligibility logic, allowing malicious actors to drain or devalue the NFT collection.

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++;
}
}
@> // No access control present to restrict who can call this function

Risk

Likelihood:

  • Any external user can discover and directly call mintSnowman() via a transaction or script.

  • Since the contract is deployed and public, there are no obstacles to interacting with the function, making this issue trivially exploitable.

Impact:

  • A malicious user can mint an unlimited number of Snowman NFTs for free, diluting the value of the collection and breaking the staking reward mechanism.

  • This may also cause financial damage to holders and degrade trust in the system by invalidating the designed staking-to-reward relationship.

Proof of Concept

function testUnauthorizedMint() public {
// StartPrank With Attacker
vm.startPrank(attacker);
// Mint Snowman With Attacker
nft.mintSnowman(attacker, 1);
// Check Attacker's Balance
assert(nft.balanceOf(attacker) == 1);
// Check Attacker's TokenCounter
assert(nft.getTokenCounter() == 1);
// StopPrank
vm.stopPrank();
}

  • Starts a prank as an external attacker using vm.startPrank(attacker)

  • Calls the public mintSnowman function to mint 1 NFT

  • Successfully mints the NFT without any access control or validation

  • Verifies the attacker's balance is now 1 NFT using assert

  • Confirms the global token counter has incremented to 1

  • Ends the prank session with vm.stopPrank()

Console Output

Traces:
[86774] TestSnowman::testUnauthorizedMint()
├─ [0] VM::startPrank(attacker: [0x9dF0C6b0066D5317aA5b38B36850548DaCCa6B4e])
│ └─ ← [Return]
├─ [73671] Snowman::mintSnowman(attacker: [0x9dF0C6b0066D5317aA5b38B36850548DaCCa6B4e], 1)
│ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: attacker: [0x9dF0C6b0066D5317aA5b38B36850548DaCCa6B4e], tokenId: 0)
│ ├─ emit SnowmanMinted(receiver: attacker: [0x9dF0C6b0066D5317aA5b38B36850548DaCCa6B4e], numberOfSnowman: 0)
│ └─ ← [Stop]
├─ [604] Snowman::balanceOf(attacker: [0x9dF0C6b0066D5317aA5b38B36850548DaCCa6B4e]) [staticcall]
│ └─ ← [Return] 1
├─ [412] Snowman::getTokenCounter() [staticcall]
│ └─ ← [Return] 1
├─ [0] VM::stopPrank()
│ └─ ← [Return]
└─ ← [Return]

Recommended Mitigation

This change ensures only the designated SnowmanAirdrop contract can mint NFTs, maintaining the intended reward flow based on staked Snow tokens.

address public immutable i_airdrop;
constructor(address airdrop) {
i_airdrop = airdrop;
}
modifier onlyAirdrop() {
require(msg.sender == i_airdrop, "Not authorized");
_;
}
function mintSnowman(address receiver, uint256 amount) external {
+ function mintSnowman(address receiver, uint256 amount) external onlyAirdrop {
Updates

Lead Judging Commences

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