Snowman Merkle Airdrop

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

buySnow keeps mismatched msg.value - buyer loses ETH on top of the WETH charge, no refund

Description

Snow.buySnow chooses the ETH vs WETH payment path by comparing msg.value to the exact price:

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);
}
...
}

If a buyer sends any non-zero msg.value that isn't exactly s_buyFee * amount, execution falls into the else branch: it charges the full price in WETH and keeps the attached ETH. The stray ETH is never refunded — it stays in the contract and is later swept to the collector as fees. The buyer double-pays.

Risk

Likelihood: Medium

  • Any buyer who mis-sizes msg.value — rounding, a slightly stale quote, or intending to pay in WETH but attaching dust — hits it.

Impact: Medium

  • Direct, silent loss of the buyer's attached ETH on top of the full WETH charge.

Proof of Concept

The buyer approves WETH, sends 1 wei with the call, is charged full WETH, and never gets the ETH back:

function test_M2_buySnowNoEthRefund() public {
MockWETH w = new MockWETH();
Snow s = new Snow(address(w), 5, address(0xFEE)); // s_buyFee = 5e18
address u = makeAddr("buyer");
w.mint(u, s.s_buyFee()); vm.deal(u, 1 ether);
uint256 ethBefore = u.balance;
vm.startPrank(u);
w.approve(address(s), type(uint256).max);
s.buySnow{value: 1}(1); // 1 wei != price -> WETH branch
vm.stopPrank();
assertEq(w.balanceOf(u), 0); // full WETH charged
assertEq(u.balance, ethBefore - 1); // the 1 wei ETH is gone
assertEq(address(s).balance, 1); // stuck, no refund
}

Recommended Mitigation

Don't silently accept mismatched ETH — require the WETH path to carry no ETH (or refund any excess):

} else {
+ if (msg.value != 0) revert S__ZeroValue();
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!