Root + Impact
buySnow selects the ETH branch only on exact equality. Any other non-zero msg.value silently falls through to the WETH branch, which pulls the full fee in WETH and leaves the sent ETH in the contract with no refund. There is also no zero-amount guard.
Description
79: function buySnow(uint256 amount) external payable canFarmSnow {
@>80: if (msg.value == (s_buyFee * amount)) {
81: _mint(msg.sender, amount);
82: } else {
@>83: i_weth.safeTransferFrom(msg.sender, address(this), (s_buyFee * amount));
84: _mint(msg.sender, amount);
@>85: }
Risk
Likelihood: Medium — any user who overpays or under-sends ETH relative to the exact fee.
Impact: Medium — the buyer is charged twice (full WETH pull plus retained ETH); retained ETH is later swept to the collector by collectFee.
Proof of Concept
function test_PoC3b_PartialEthStranded() public {
_deploy();
address buyer = makeAddr("buyer");
uint256 feeWei = snow.s_buyFee();
vm.deal(buyer, feeWei);
weth.mint(buyer, feeWei);
vm.startPrank(buyer);
weth.approve(address(snow), feeWei);
snow.buySnow{value: feeWei - 1}(1);
vm.stopPrank();
assertEq(weth.balanceOf(address(snow)), feeWei);
assertEq(address(snow).balance, feeWei - 1);
}
Recommended Mitigation
Require amount > 0; allow exactly one payment asset per call; refund excess.
function buySnow(uint256 amount) external payable canFarmSnow {
+ if (amount == 0) revert S__ZeroValue();
if (msg.value == (s_buyFee * amount)) {
_mint(msg.sender, amount);
- } else {
+ } else if (msg.value == 0) {
i_weth.safeTransferFrom(msg.sender, address(this), (s_buyFee * amount));
_mint(msg.sender, amount);
+ } else {
+ revert S__ZeroValue(); // reject mixed/partial payments
}