Snowman Merkle Airdrop

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

[M-1] claimSnowman Uses Live Balance Instead of Snapshot Amount, Breaking Merkle Proof Verification

Root + Impact

Description:

Description:
The buySnow function is intended to allow users to purchase Snow tokens using either native ETH (msg.value) or WETH (i_weth.safeTransferFrom). However, the current logic fails to properly differentiate between these two payment methods and mishandles excess ETH.

solidity

// Snow.sol
function buySnow(uint256 amount) external payable canFarmSnow {
if (msg.value == (s_buyFee * amount)) {
_mint(msg.sender, amount);
} else {
i_weth.safeTransferFrom(msg.sender, address(this), (s_buyFee * amount));
_mint(msg.sender, amount);
}
s_earnTimer = block.timestamp;
emit SnowBought(msg.sender, amount);
}

There are two critical flaws in this logic:

  1. Permanent ETH Lock (Fund Loss): If a user sends slightly more ETH than required (i.e., msg.value > s_buyFee * amount), the if condition fails. The excess ETH is not refunded and remains permanently locked in the contract.

  2. Double Payment: If a user sends any amount of ETH that does not exactly match the required cost (e.g., msg.value > 0 but not equal to s_buyFee * amount), the code falls into the else block. It will then attempt to pull the full WETH amount from the user, while the ETH sent is also kept by the contract. The user effectively pays twice (once in ETH, once in WETH) and the ETH is lost forever.

Risk

Likelihood:
Medium. Users frequently use the "Max" button or manually input ETH amounts, which can easily lead to sending slightly more than the exact required s_buyFee * amount.

Impact:
High. Any ETH sent that does not exactly match the required amount is permanently locked in the contract with no way to recover it, resulting in a direct loss of funds for the user.

Impact:

. Direct Fund Loss: Users who accidentally send the wrong amount of ETH will lose their ETH permanently, as there is no refund mechanism or withdrawal function for native ETH in this contract.

  • Double Payment: Users will be forced to pay via WETH even after sending ETH, draining their WETH balance unnecessarily.

Proof of Concept:

// Add this test to Snow.t.sol
function testDoublePaymentAndETHLock() public {
// 1. Setup: User wants to buy 1 Snow token
uint256 amountToBuy = 1;
uint256 requiredCost = snow.s_buyFee() * amountToBuy; // e.g., 1 ether
// User has plenty of WETH and approves the contract
vm.deal(user, 2 ether);
vm.prank(user);
weth.approve(address(snow), requiredCost); // Approve WETH spend
​
// 2. Attack/Error: User sends slightly MORE ETH than required (e.g., 1.1 ether)
vm.prank(user);
snow.buySnow{value: requiredCost + 0.1 ether}(amountToBuy);
​
// 3. Consequence:
// - User's ETH is locked in the contract (contract balance is now 1.1 ether)
assert(address(snow).balance == requiredCost + 0.1 ether);
// - User's WETH is ALSO deducted (Double Payment)
assert(weth.balanceOf(user) == 0);
// - User received only 1 Snow token, but paid both ETH and WETH
}

Recommended Mitigation:

function buySnow(uint256 amount) external payable canFarmSnow {
+ uint256 totalCost = s_buyFee * amount;
+ require(totalCost > 0, "Zero amount");
​
- if (msg.value == (s_buyFee * amount)) {
+ if (msg.value > 0) {
+ require(msg.value == totalCost, "Incorrect ETH amount");
_mint(msg.sender, amount);
} else {
- i_weth.safeTransferFrom(msg.sender, address(this), (s_buyFee * amount));
+ i_weth.safeTransferFrom(msg.sender, address(this), totalCost);
_mint(msg.sender, amount);
}
s_earnTimer = block.timestamp;
emit SnowBought(msg.sender, amount);
}
References
CodeHawks Submission #341: Snow.sol - User loses ETH and pays in WETH (Rejected by Judge)
​
CodeHawks Submission #452: buySnow() allows ETH to be locked in the contract (Rejected by Judge)
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 1 hour 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!