Root + Impact:
When msg.value is not exactly the fee, the WETH branch still runs (charging WETH) and the sent ETH is neither refunded nor rejected, so the user pays twice and loses the ETH.
Description:
buySnow branches on msg.value == s_buyFee * amount. If not equal, it falls into the else branch which pulls the full WETH fee via safeTransferFrom and mints, while the ETH sent in msg.value is retained by the contract (later swept to the collector by collectFee). There is no check that msg.value is 0 or exactly the fee.
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;
}
Risk: Incorrect funds handling / user overpayment loss.
Likelihood: Routinely — a caller attaches ETH that is not exactly s_buyFee * amount (user error or malicious frontend).
Impact: User loses the mis-sent ETH and still pays the WETH fee — effective double payment.
Proof of Concept:
pragma solidity ^0.8.24;
import {Test} from "forge-std/Test.sol";
import {Snow} from "../src/Snow.sol";
import {MockWETH} from "../src/mock/MockWETH.sol";
contract PoC is Test {
Snow snow; MockWETH weth; address attacker;
function setUp() public {
weth = new MockWETH();
snow = new Snow(address(weth), 5, makeAddr("collector"));
attacker = makeAddr("attacker");
}
function test_medium_buySnowDoubleCharge() public {
weth.mint(attacker, 1000e18);
vm.prank(attacker);
weth.approve(address(snow), 1000e18);
uint256 ethBefore = address(snow).balance;
uint256 wethBefore = weth.balanceOf(attacker);
uint256 overpay = 10e18;
vm.prank(attacker);
snow.buySnow{value: overpay}(1);
assertEq(address(snow).balance, ethBefore + overpay);
assertEq(weth.balanceOf(attacker), wethBefore - snow.s_buyFee());
assertEq(snow.balanceOf(attacker), 1);
}
}
Recommended Mitigation:
error S__BadPayment();
function buySnow(uint256 amount) external payable canFarmSnow {
uint256 owed = s_buyFee * amount;
if (msg.value == owed) {
_mint(msg.sender, amount);
} else if (msg.value == 0) {
i_weth.safeTransferFrom(msg.sender, address(this), owed);
_mint(msg.sender, amount);
} else {
@> revert S__BadPayment();
}
s_earnTimer = block.timestamp;
}