Snowman Merkle Airdrop

First Flight #42
Beginner FriendlyFoundrySolidityNFT
100 EXP
Submission Details
Impact: high
Likelihood: high

`mintSnowman()` in `Snowman.sol` lacks access control, allowing arbitrary NFT minting by any external caller

Author Revealed upon completion

Root + Impact

Description

Normal behavior:
The Snowman contract is intended to mint NFTs as rewards, likely only callable by the SnowmanAirdrop contract after users complete staking and Merkle verification.

The problem:
The mintSnowman(address, uint256) function is marked as external and has no access control, allowing any external caller to mint any number of NFTs to themselves or others.

function mintSnowman(address receiver, uint256 amount) external {
for (uint256 i = 0; i < amount; i++) {
_safeMint(receiver, s_TokenCounter); // @> Anyone can mint unlimited NFTs
emit SnowmanMinted(receiver, s_TokenCounter);
s_TokenCounter++;
}
}

Risk

Likelihood: High

  • This occurs whenever any external contract or externally owned account (EOA) calls the mintSnowman() function, since it is marked external and lacks any restriction like onlyOwner or onlyMinter.


  • A malicious actor can deploy a simple contract or use a script to mint unlimited NFTs to themselves or others without interacting with the intended staking or airdrop flow

Impact: High

  • Unauthorized users can mint an unlimited number of Snowman NFTs, completely bypassing the staking and Merkle-based reward logic defined in the protocol.

  • The value, rarity, and fairness of the Snowman NFT distribution system is destroyed, leading to inflation, loss of trust, and potential devaluation of user-earned NFTs.

Proof of Concept

Below here , the PoC shows that anyone can call mintSnowman() directly and mint arbitrary quantities of NFTs to any address. The contract lacks access control, so it does not verify whether the caller is authorized, nor does it check if the recipient has completed staking or earned the NFTs. This allows attackers to mint fake NFTs indistinguishable from legitimate ones, breaking the integrity of the reward system.

// Any attacker or script can directly call mintSnowman() and bypass all reward logic
// Contract interface (can be done from a script or contract)
interface ISnowman {
function mintSnowman(address receiver, uint256 amount) external;
}
// Assume attacker already knows the Snowman contract address
ISnowman snowman = ISnowman(0xYourSnowmanContract);
// Attacker mints 1,000 NFTs to themselves without stake or Merkle verification
snowman.mintSnowman(attackerAddress, 1000);
// All NFTs are valid and indistinguishable from real earned ones

Recommended Mitigation

To prevent unauthorized minting, restrict the mintSnowman() function to be callable only by a designated trusted contract (such as the SnowmanAirdrop contract). This ensures that NFTs can only be minted after proper staking, Merkle verification, and signature checks.

Access control should be implemented using a minter address that can only be set by the contract owner.

+ address private s_minter;
+ error SM__NotAuthorized();
+ modifier onlyMinter() {
+ if (msg.sender != s_minter) revert SM__NotAuthorized();
+ _;
+ }
+ function setMinter(address _minter) external onlyOwner {
+ if (_minter == address(0)) revert S__ZeroAddress();
+ s_minter = _minter;
+ }
function mintSnowman(address receiver, uint256 amount) external
+ onlyMinter
{
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.