title: Snowman Merkle Airdrop Audit Report
author: Joe LeFever (Sicher Height)
date: June 19, 2025
header-includes:
\usepackage{titling}
\usepackage{graphicx}
\begin{titlepage}
\centering
\vspace*{2cm}
{\Large\bfseries Joe LeFever - Sicher Height\par}
\vspace{2cm}
{\Huge\bfseries Snowman Merkle Airdrop Audit Report\par}
\vspace{1cm}
{\Large Version 1.0\par}
\vspace{2cm}
{\Large\itshape Independent Security Audit\par}
\vfill
{\large June 19, 2025\par}
\end{titlepage}
\maketitle
Prepared by: Joe LeFever - Sicher Height
Lead Auditor:
Joe LeFever
The Snowman Merkle Airdrop protocol is a token distribution system that combines ERC20 tokens (Snow), ERC721 NFTs (Snowman), and Merkle tree-based airdrops. Users can earn or purchase Snow tokens, which can then be staked in the SnowmanAirdrop contract to receive proportional Snowman NFTs. The protocol features a 12-week farming period where users can earn free Snow tokens weekly or purchase them with ETH/WETH. The airdrop system utilizes Merkle proofs for efficient distribution and supports signature-based claiming on behalf of others.
Key Components:
Snow.sol: ERC20 token with buy/earn mechanics and fee collection
Snowman.sol: ERC721 NFT with on-chain Base64 encoding
SnowmanAirdrop.sol: Merkle tree-based airdrop system with signature verification
The Joe LeFever - Sicher Height team makes all effort to find as many vulnerabilities in the code in the given time period, but holds no responsibilities for the findings provided in this document. A security audit by the team is not an endorsement of the underlying business or product. The audit was time-boxed and the review of the code was solely on the security aspects of the Solidity implementation of the contracts.
Impact | ||||
---|---|---|---|---|
High | Medium | Low | ||
High | H | H/M | M | |
Likelihood | Medium | H/M | M | M/L |
Low | M | M/L | L |
We use the CodeHawks severity matrix to determine severity. See the documentation for more details.
Date: June 19, 2025
Auditor: Joe LeFever (Sicher Height)
Classification: Competitive Audit
Duration: 2 days
The following contracts were in scope for this audit:
Lines of Code: ~300 total
Solidity Version: ^0.8.24
Owner: Deployer of Snow contract with administrative privileges
Collector: Address authorized to collect fees from Snow token purchases
Users: Can buy/earn Snow tokens and claim Snowman NFTs
Recipients: Addresses eligible for airdrops via Merkle proofs
This audit identified 6 High severity, 6 Medium severity, and 3 Low severity vulnerabilities across the three contracts in the Snowman Merkle Airdrop protocol. The most critical issues involve missing access controls that allow unlimited NFT minting, fund theft through arbitrary token transfers, and broken signature verification due to a typo in the EIP-712 hash.
The protocol's core functionality is severely compromised by these vulnerabilities, particularly the unrestricted minting capability that completely breaks the tokenomics model. Immediate remediation of High severity findings is strongly recommended before any mainnet deployment.
Severity | Number of issues found |
---|---|
High | 6 |
Medium | 6 |
Low | 3 |
Total | 15 |
Description:
The earnSnow()
function uses a single global timer s_earnTimer
that affects all users. When any user calls earnSnow()
, it prevents all other users from calling the function for one week. This breaks the intended functionality where each user should be able to earn one Snow token per week independently.
Impact:
Only one user per week can earn free Snow tokens across the entire protocol, preventing all other users from accessing this core feature and enabling griefing attacks.RetryClaude can make mistakes. Please double-check responses.
Proof of Concept:
Recommended Mitigation:
Replace the global timer with a per-user mapping
Description:
The buySnow()
function has flawed payment logic that can cause users to lose their ETH. If a user sends ETH but not the exact required amount, the function keeps their ETH and then attempts to charge them again via WETH transfer. This can result in double charging or fund loss when the WETH transfer fails.
Impact:
Users lose ETH permanently when sending incorrect amounts, face double charging (paying both ETH and WETH), and can have funds trapped in the contract due to failed WETH transfers.
Proof of Concept:
Recommended Mitigation:
Implement proper payment method validation and refund excess ETH.
Description:
The mintSnowman()
function in Snowman.sol has no access control restrictions, allowing any user to mint unlimited Snowman NFTs to any address. This completely breaks the intended tokenomics where NFTs should only be minted through the SnowmanAirdrop contract when users stake Snow tokens.
Impact:
Complete protocol failure. Attackers can mint unlimited NFTs, making them worthless and breaking the staking/airdrop mechanism. Total loss of protocol integrity.
Proof of Concept:
Recommended Mitigation
Add access control to ensure only the SnowmanAirdrop contract can mint:
Description:
The claimSnowman()
function uses user-controlled receiver
parameter in safeTransferFrom(receiver, address(this), amount)
, allowing attackers to drain any user's Snow tokens by setting receiver
to victim addresses.
Impact:
Direct fund theft. Attackers can steal Snow tokens from any user who has approved the airdrop contract or has sufficient balance.
Proof of Concept:
Recommended Mitigation:
Enforce that only msg.sender
can transfer their own tokens
Description:
The MESSAGE_TYPEHASH
constant contains a typo: "SnowmanClaim(addres receiver, uint256 amount)"
is missing the 's' in "address". This causes all EIP-712 signature verification to fail as the hash won't match user-signed messages.
Impact:
Complete airdrop system failure. No user can successfully claim through signature verification, making the entire signature-based claiming mechanism non-functional.
Recommended Mitigation:
Fix the typo -
Description:
The mintSnowman()
function is vulnerable to reentrancy via the _safeMint()
callback to onERC721Received()
. State variables are updated after the external call, allowing malicious contracts to reenter and mint additional NFTs.
Impact:
Attackers can mint more NFTs than intended, diluting the NFT supply and breaking the 1:1 Snow token to NFT ratio.
Proof of Concept:
Recommended Mitigation:
Use ReentrancyGuard or follow Checks-Effects-Interactions pattern.
Description:
The collectFee()
function uses a low-level call to transfer ETH to the collector without re-entrancy protection. This gives control to the collector contract during execution, potentially allowing re-entrancy attacks if the collector is a malicious contract.
Impact:
Potential for re-entrancy attacks if collector is a malicious contract.
Proof of Concept:
Recommended Mitigation:
Implement re-entrancy protection and follow checks-effects-interactions pattern.
Description:
In collectFee()
, if the WETH transfer succeeds but the ETH transfer fails, the entire transaction reverts. This can lead to situations where fee collection repeatedly fails due to ETH transfer issues, even when WETH transfer would succeed.
Impact:
Fee collection can be permanently blocked if ETH transfer consistently fails.
Proof of Concept:
Recommended Mitigation:
Handle WETH and ETH transfers independently.
Description:
The claimSnowman()
function sets s_hasClaimedSnowman[receiver] = true
but never checks this mapping before processing claims, allowing users to potentially claim multiple times.
Impact:
Users could claim NFTs multiple times if they can generate multiple valid signatures, leading to unfair distribution and NFT inflation.
Proof of Concept:
Recommended Mitigation:
Add double-claim check
Description:
The mintSnowman()
function contains a user-controlled loop with external calls (_safeMint)
, allowing attackers to cause out-of-gas errors by providing large amount
values.
Impact:
Function becomes unusable with large inputs, preventing legitimate minting operations and potentially locking the contract.
Proof of Concept:
Recommended Mitigation:
Add reasonable limits
Description:
The collectFee()
function ignores the return value of i_weth.transfer()
, causing silent failures if the WETH transfer fails while continuing to execute the rest of the function.
Impact:
Collected fees could be permanently lost if WETH transfer fails, resulting in financial loss for the protocol.
Recommended Mitigation:
Check return value or use SafeERC20
Description:
The SnowmanAirdrop constructor accepts _merkleRoot
parameter without validating it's not zero, potentially allowing deployment with an invalid merkle root that would make all proofs fail.
Impact:
If deployed with zero merkle root, the entire airdrop system becomes non-functional as all merkle proofs would be invalid.
Recommended Mitigation:
Add validation in constructor
Description:
The SnowmanMinted
event parameter is named numberOfSnowman
(suggesting count) but emits s_TokenCounter
(token ID), creating semantic confusion and breaking off-chain integrations expecting count data.
Impact:
Broken off-chain integrations, misleading analytics, and poor user experience as dApps expecting mint counts receive token IDs instead.
Recommended Mitigation:
Either emit count or rename parameter.
Description:
The earnSnow()
function does not emit an event when tokens are earned, unlike buySnow()
which emits a SnowBought
event. This inconsistency makes it difficult to track earning activities off-chain.
Impact:
Reduced transparency and auditability.
Recommended Mitigation:
Add event emission to earnSnow().
Description:
The collectFee()
function transfers all ETH in the contract to the collector using address(this).balance
, including any ETH accidentally sent directly to the contract address. This ETH was not paid as fees for Snow tokens but gets collected as if it were.
Impact:
Users who accidentally send ETH to the contract lose their funds and collector receives funds that weren't legitimate fees.
Recommended Mitigation:
Track legitimate fees separately from accidental transfers.
The mint function of the Snowman contract is unprotected. Hence, anyone can call it and mint NFTs without necessarily partaking in the airdrop.
The claim function of the Snowman Airdrop contract doesn't check that a recipient has already claimed a Snowman. This poses no significant risk as is as farming period must have been long concluded before snapshot, creation of merkle script, and finally claiming.
A typo in the `MESSAGE_TYPEHASH` variable of the `SnowmanAirdrop` contract will prevent signature verification claims. Used `addres` instead of `address`
The mint function of the Snowman contract is unprotected. Hence, anyone can call it and mint NFTs without necessarily partaking in the airdrop.
The claim function of the Snowman Airdrop contract doesn't check that a recipient has already claimed a Snowman. This poses no significant risk as is as farming period must have been long concluded before snapshot, creation of merkle script, and finally claiming.
A typo in the `MESSAGE_TYPEHASH` variable of the `SnowmanAirdrop` contract will prevent signature verification claims. Used `addres` instead of `address`
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.