Snowman Merkle Airdrop

AI First Flight #10
Beginner FriendlyFoundrySolidityNFT
EXP
View results
Submission Details
Impact: low
Likelihood: low
Invalid

Low Severity Findings: Code Quality and State-Handling Issues Across Snow.sol and SnowmanAirdrop.sol

Root + Impact

Description

  • The following three findings do not put funds directly at risk, but reflect inconsistencies in the codebase relative to its own established patterns and unused state that increases deployment cost and code surface without benefit.

L-1: Snow::collectFee() uses raw .transfer() instead of .safeTransfer()

Snow.sol declares using SafeERC20 for IERC20; at the top of the contract and relies on safeTransferFrom elsewhere in the codebase (e.g., within SnowmanAirdrop::claimSnowman()), but collectFee() calls the raw, unwrapped .transfer() method instead:

function collectFee() external onlyCollector {
uint256 collection = i_weth.balanceOf(address(this));
@> i_weth.transfer(s_collector, collection);
...
}

Raw .transfer() does not correctly handle ERC20 tokens that fail to return a bool or that return false on failure rather than reverting. In this specific instance, since i_weth is fixed to a well-behaved WETH implementation with no setter to change it after deployment, this pattern does not currently expose an exploitable path — but it remains an inconsistency with the rest of the codebase's stated intent to use SafeERC20 throughout, and this Foundry linter independently flags the same pattern (warning[erc20-unchecked-transfer]).

L-2: SnowmanAirdrop::SnowmanClaimedSuccessfully event has no indexed parameters

@> event SnowmanClaimedSuccessfully(address receiver, uint256 amount);

Every other event across Snow.sol and Snowman.sol indexes its address parameter (and often the amount as well) — SnowBought, SnowEarned, and SnowmanMinted all index at least the relevant address. SnowmanClaimedSuccessfully is the sole exception, with zero indexed fields. This limits the ability of off-chain indexers, frontends, and block explorers to efficiently filter claim events by receiver, requiring them to fetch and decode every emitted event rather than querying by topic.

L-3: SnowmanAirdrop::s_claimers array declared but never used

@> address[] private s_claimers; // array to store addresses of claimers

This array is declared with a clear comment indicating it's intended purpose, but is never written to or read from anywhere in the contract. Its presence increases contract deployment size for no functional benefit and may indicate an incomplete feature — such as an on-chain enumeration of all claimants — that was planned but not implemented.

Risk

Likelihood: Low

  • None of these three findings are exploitable under any input or sequence of calls; they merely represent static "code-quality" observations rather than conditions that can be triggered.

Impact:

  • No funds are at risk in any of the three cases.

  • The impact is limited to reduced off-chain queryability ( for L-2), inconsistent defensive coding practice with no current exploitable consequence ( for L-1), and unnecessary deployment gas cost (for L-3).


Recommended Mitigation

Each change is independent and low-risk: L-1 aligns collectFee() with the contract's existing SafeERC20 usage pattern for defense-in-depth against non-standard tokens should i_weth ever change; L-2 adds the indexed keyword to receiver, matching the convention used by every other event in the codebase; L-3 removes the unused array entirely, reducing deployment bytecode size with no functional impact, since nothing in the contract reads from or writes to it.

// L-1
- i_weth.transfer(s_collector, collection);
+ i_weth.safeTransfer(s_collector, collection);
// L-2
- event SnowmanClaimedSuccessfully(address receiver, uint256 amount);
+ event SnowmanClaimedSuccessfully(address indexed receiver, uint256 amount);
// L-3
- address[] private s_claimers; // array to store addresses of claimers
+ // removed - unused
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 8 hours ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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

Give us feedback!