mintSnowman reads from and writes to the s_TokenCounter storage slot on every loop iteration. Each s_TokenCounter++ executes an SSTORE opcode — the first write in a transaction costs 2,900 gas, and each subsequent dirty-slot write costs 100 gas. Because s_TokenCounter only needs its final post-loop value to be persisted, all intermediate writes are avoidable.
The same result can be achieved by reading s_TokenCounter once before the loop into a local variable, iterating with that local variable, and writing the accumulated final value back to storage exactly once after the loop completes.
Likelihood:
Every call to mintSnowman with amount > 1 pays this unnecessary cost — the SnowmanAirdrop contract calls mintSnowman(receiver, amount) where amount equals the receiver's Snow token balance, meaning any user with more than 1 Snow token triggers the expensive path.
Batch minting is a core protocol operation because Snowman NFTs are distributed according to a user's Snow token balance. Therefore the issue is guaranteed to occur whenever multiple NFTs are minted in a single transaction.
Impact:
Users pay more gas than necessary when claiming multiple Snowman NFTs.
Large batch mints become increasingly expensive due to repeated storage writes.
The test calls mintSnowman for a batch of 10 tokens and measures the gas consumed via gasleft() deltas before and after the call. The inline annotations document the per-iteration opcode breakdown: two warm SLOADs at 100 gas each for the _safeMint and emit arguments, one first-write SSTORE at 2,900 gas on iteration zero, and one dirty-slot SSTORE at 100 gas on each of the nine subsequent iterations — totalling approximately 3,800 gas in avoidable storage writes and 2,000 gas in redundant reads for a 10-token batch. The second test function documents the estimated saving of approximately 1,800 gas for 10 tokens that the single-read-single-write optimised version would achieve, with the saving growing linearly with batch size.
To run: forge test --match-test test_BatchMintGasCost -vvvv
A more gas-efficient approach is to cache s_TokenCounter into a local uint256 tokenId variable before the loop begins. Use and increment tokenId (a memory variable) inside each loop iteration. Write the final value back to s_TokenCounter storage exactly once after the loop completes.
The contest is live. Earn rewards by submitting a finding.
Submissions are being reviewed by our AI judge. Results will be available in a few minutes.
View all submissionsThe contest is complete and the rewards are being distributed.