Snowman Merkle Airdrop

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

`buySnow` never refunds `msg.value` in the WETH branch, so a user who sends non-exact ETH pays in both ETH and WETH

Description

  • buySnow is meant to let a caller pay for amount of Snow either in ETH (by sending the exact price in msg.value) or in WETH.

  • If msg.value is not exactly s_buyFee * amount, execution enters the else branch, which pulls the full price in WETH but never refunds the ETH already sent in msg.value. The caller is charged twice and receives tokens only once.

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);
@> // msg.value sent by the caller is never refunded in this branch
}
s_earnTimer = block.timestamp;
emit SnowBought(msg.sender, amount);
}

Risk

Likelihood:

  • Occurs whenever a caller sends a non-zero msg.value that is not exactly s_buyFee * amount (wrong price assumption or rounding mistake).

  • The price is scaled by 1e18 (s_buyFee = buyFee * PRECISION), so sending a slightly wrong ETH amount is easy.

Impact:

  • The caller pays the full price in WETH and additionally loses the ETH sent in msg.value, while receiving Snow only once.

  • The stranded ETH is later withdrawn by the collector via collectFee, so the user's ETH is effectively taken.

Proof of Concept

This Foundry test sends half the price as ETH, forcing the else branch. The user ends up down both 0.5 ETH and the full WETH price, and the 0.5 ETH is stranded in the contract:

function test_userPaysBothEthAndWeth() public {
uint256 amount = 1;
uint256 exactPrice = snow.s_buyFee() * amount;
weth.mint(user, exactPrice);
vm.deal(user, exactPrice);
uint256 ethSent = exactPrice / 2; // not equal to exactPrice -> else branch
vm.startPrank(user);
weth.approve(address(snow), exactPrice);
snow.buySnow{value: ethSent}(amount);
vm.stopPrank();
// user paid full price in WETH AND lost the ETH they sent:
assertEq(weth.balanceOf(user), 0);
assertEq(address(snow).balance, ethSent); // ETH stranded in contract
assertEq(user.balance, exactPrice - ethSent); // user also down the ETH
}

Result: test passes. user ETH after = 0.5e18, user WETH after = 0, contract ETH = 0.5e18.

Recommended Mitigation

Reject any ETH in the WETH branch (or refund it), so a caller cannot be charged in both assets:

} else {
+ require(msg.value == 0, "Snow: send either ETH or WETH, not both");
i_weth.safeTransferFrom(msg.sender, address(this), (s_buyFee * amount));
_mint(msg.sender, amount);
}
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!