Snowman Merkle Airdrop

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

[L-1] Gas Cost of `mintSnowman` Loop Can Lead to DoS for Large Mint Amounts

[L-1] Gas Cost of mintSnowman Loop Can Lead to DoS for Large Mint Amounts

Description

  • Protocol Behavior: The SnowmanAirdrop.sol contract is intended to call Snowman.sol::mintSnowman(receiver, amount) where amount is "equal to their Snow balance" which the recipient staked.

  • Specific Issue: The mintSnowman function in Snowman.sol uses a for loop to mint NFTs one by one: for (uint256 i = 0; i < amount; i++). The gas cost of this function call will therefore scale linearly with the amount parameter. If a recipient has a very large Snow balance and is entitled to a correspondingly large number of Snowman NFTs, the amount can be substantial. Minting a large number of NFTs in a single transaction via this loop could consume gas exceeding the block gas limit.

// Snowman.sol
function mintSnowman(address receiver, uint256 amount) external /* onlyMinter - assuming prior fix */ {
for (uint256 i = 0; i < amount; i++) { // @> GAS: Cost scales linearly with 'amount'
_safeMint(receiver, s_TokenCounter);
emit SnowmanMinted(receiver, s_TokenCounter);
s_TokenCounter++;
}
}

Risk

Likelihood: Medium

  • Depends on the distribution of Snow tokens. If "whales" (holders of very large amounts of Snow tokens) exist and are eligible for the airdrop, they are likely to trigger this condition.

Impact: Low

  • Denial of Service for Large Claims: Legitimate users with entitlements to a large number of Snowman NFTs might be unable to have them minted in a single transaction if the gas cost exceeds the block gas limit. Their transaction would revert, effectively denying them their airdrop.

  • No Direct Fund Loss: This issue does not directly lead to a loss of funds but rather a failure to receive entitled assets.

Proof of Concept

The mintSnowman function's for loop causes gas costs to scale linearly with the number of NFTs minted. The testGas_MintSnowman_LargeAmount function in test/Snowman.t.sol demonstrates an authorized minter (simulating the SnowmanAirdrop contract) attempting to mint a large batch of 500 NFTs. The gas consumed by this single transaction will be logged.

// test/Snowman.t.sol
function testGas_MintSnowman_LargeAmount() public {
vm.txGasPrice(1);
address largeRecipient = makeAddr("large_recipient");
uint256 largeMintAmount = 500; // A reasonably large amount for a test.
// In a real scenario, this could be much larger.
uint256 initialTotalSupply = nft.getTokenCounter();
uint256 initialRecipientBalance = nft.balanceOf(largeRecipient);
console2.log("Attempting to mint %s NFTs to %s", largeMintAmount, largeRecipient);
uint256 gasStart = gasleft();
console2.log("Initial gas start for minting: ", gasStart);
vm.prank(contractOwner); // Simulate authorized minter
nft.mintSnowman(largeRecipient, largeMintAmount);
uint256 gasEnd = gasleft();
console2.log("Final gas end for minting: ", gasEnd);
uint256 finalTotalSupply = nft.getTokenCounter();
uint256 finalRecipientBalance = nft.balanceOf(largeRecipient);
uint256 gasUsed = gasStart - gasEnd; // Internal gas consumed
console2.log("Gas used for minting %s NFTs: %s", largeMintAmount, gasUsed);
assertEq(finalTotalSupply, initialTotalSupply + largeMintAmount, "Total supply should increase by the large mint amount.");
assertEq(finalRecipientBalance, initialRecipientBalance + largeMintAmount, "Recipient's balance should increase by the large mint amount.");
console2.log("Successfully minted %s NFTs. Gas cost will be proportional to this amount.", largeMintAmount);
}

When this test is run, the console output will show the gasUsed for minting 500 NFTs in a single call. While this specific amount (500) might not exceed typical block gas limits in a test environment, the gasUsed value will be substantial. If a user were entitled to a significantly larger number of NFTs (e.g., several thousands), the aggregated gas cost from each iteration of the loop in a single transaction could easily surpass the network's block gas limit, causing the transaction to revert. This demonstrates the potential for a gas-related Denial of Service for users with large NFT entitlements.

Recommended Mitigation

To prevent transactions from reverting due to block gas limits when minting for recipients with large entitlements, implement a batching mechanism:

  1. Batching in SnowmanAirdrop.sol (Recommended):

    • Modify SnowmanAirdrop.sol's logic. When a user claims and is entitled to a very large number of NFTs (e.g., totalEntitlement), SnowmanAirdrop.sol should not call Snowman.sol::mintSnowman(recipient, totalEntitlement) in one go.

    • Instead, SnowmanAirdrop.sol could either:

      • Perform multiple calls to Snowman.sol::mintSnowman(recipient, batchSize) in a loop, where batchSize is a safe number of NFTs to mint per transaction (e.g., 100-200). This would require SnowmanAirdrop.sol to manage the iteration and potentially be callable multiple times by the user or an admin to continue a large minting process.


  2. Gas-Efficient NFT Standards (Long-Term Consideration):

    • For future projects or major upgrades, consider using gas-efficient NFT implementations like ERC721A by Azuki, which are specifically designed to reduce gas costs for minting multiple NFTs in a single transaction. This would require a more significant architectural change to Snowman.sol.

Updates

Lead Judging Commences

yeahchibyke Lead Judge 3 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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