Snowman Merkle Airdrop

First Flight #42
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Impact: medium
Likelihood: low
Invalid

Missing Zero Address Check

Root + Impact

Description

  • The mintSnowman function should validate input parameters to prevent unintended minting behaviour.

  • The function accepts address(0) as a valid receiver, even though sending tokens to this address effectively burns them as they can never be recovered. While OpenZeppelin's _safeMint includes some safety checks, it doesn't prevent minting to address(0). This is intentional in the OpenZeppelin implementation to allow flexibility, but it means the responsibility for this validation falls on the implementing contract. The lack of validation means that a simple frontend error or incorrect parameter passing could result in permanent loss of tokens.

@> 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: Low

  • Function can be called with zero address as receiver

  • No validation exists for the amount parameter

  • Occurs in any direct contract interaction where parameters aren't validated

Impact:

  • Tokens could be minted to the zero address, effectively burning them

  • Zero amount of mints waste gas but completes successfully

  • No maximum batch size limit could lead to gas issues

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
import {Test} from "forge-std/Test.sol";
import {Snowman} from "../src/Snowman.sol";
contract SnowmanTest is Test {
Snowman snowman;
address alice = makeAddr("alice");
function setUp() public {
// Deploy Snowman contract
snowman = new Snowman("test-uri");
}
function testMintToZeroAddress() public {
// Case 1: Direct mint to zero address
snowman.mintSnowman(address(0), 1);
// Verify token was minted to zero address
assertEq(snowman.ownerOf(0), address(0));
// Token is effectively burned - no one can access it
// Case 2: Batch mint to zero address
snowman.mintSnowman(address(0), 3);
// Verify multiple tokens are lost
assertEq(snowman.ownerOf(1), address(0));
assertEq(snowman.ownerOf(2), address(0));
assertEq(snowman.ownerOf(3), address(0));
// Case 3: Zero amount mint (wastes gas)
vm.prank(alice);
snowman.mintSnowman(alice, 0);
// Transaction succeeds but no tokens are minted
// Case 4: Common error scenario - uninitialized address
address unsetAddress; // defaults to address(0)
snowman.mintSnowman(unsetAddress, 1);
// Token is lost due to common programming oversight
}
}

Recommended Mitigation

// Add custom errors
- // ... existing code ...
+ error ZeroAddressNotAllowed();
+ error InvalidMintAmount();
// Add constants
+ uint256 public constant MAX_BATCH_SIZE = 20;
// Modify minting function
- 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++;
- }
- }
+ function mintSnowman(address receiver, uint256 amount) external {
+ // Validate receiver address
+ if (receiver == address(0)) revert ZeroAddressNotAllowed();
+
+ // Validate mint amount
+ if (amount == 0 || amount > MAX_BATCH_SIZE) revert InvalidMintAmount();
+
+ // Store starting token ID for event
+ uint256 startTokenId = s_TokenCounter;
+
+ // Perform minting
+ for (uint256 i = 0; i < amount; i++) {
+ _safeMint(receiver, s_TokenCounter);
+ s_TokenCounter++;
+ }
+
+ // Emit single event for batch mint
+ emit SnowmanMinted(receiver, startTokenId, amount);
+ }
Updates

Lead Judging Commences

yeahchibyke Lead Judge about 1 month ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.