## Root + Impact
### Description
**Normal behavior:** `buySnow` sells Snow tokens for a fee payable in native ETH (exact `msg.value == s_buyFee * amount`) or in WETH. A well-behaved payable function should either refund excess ETH or use it as payment.
**Issue:** The branch condition requires `msg.value` to be *exactly* the fee. If a user sends any ETH with the transaction, the equality check fails and execution falls into the WETH branch: the full fee is pulled from the user's WETH balance **and** the attached ETH is never refunded — it stays in the contract, from where it can only be swept wholesale by the fee collector via `collectFee`.
```solidity
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);
}
```
## Risk
**Likelihood:**
- Any user whose wallet defaults to sending a small ETH dust value, or any contract integrating `buySnow` that forwards `msg.value`, triggers it on every purchase.
**Impact:**
- User loses both the WETH fee and the attached ETH; the ETH is permanently stranded unless the collector sweeps it, and the collector receives funds it never earned.
- The contract holds native ETH that `collectFee` mixes into the collector's fee accounting, corrupting fee attribution.
## Proof of Concept
The following Foundry test demonstrates the bug: a user pays the fee in WETH but accidentally attaches 0.1 ETH. The strict equality check fails, so the WETH branch executes — and the 0.1 ETH is stranded in the contract on top of the WETH fee. Run with `forge test --match-test test_F4_BuySnowEthLocked -vv`.
```solidity
function test_F4_BuySnowEthLocked() public {
MockWETH realWeth = new MockWETH();
Snow snow2 = new Snow(address(realWeth), 5, makeAddr("collector"));
uint256 fee = snow2.s_buyFee();
vm.deal(bob, 1 ether);
vm.startPrank(bob);
realWeth.mint(bob, fee * 2);
realWeth.approve(address(snow2), type(uint256).max);
snow2.buySnow{value: 0.1 ether}(2);
vm.stopPrank();
assertEq(address(snow2).balance, 0.1 ether);
assertEq(snow2.balanceOf(bob), 2);
}
```
Result: PASS — 0.1 ETH is stuck in `Snow` and the user paid the fee twice (once in WETH, once in ETH).
## Recommended Mitigation
The fix: treat any attached ETH as an exact-payment requirement (revert on mismatch) instead of silently falling through to the WETH branch, so user ETH can never be stranded:
```diff
function buySnow(uint256 amount) external payable canFarmSnow {
- if (msg.value == (s_buyFee * amount)) {
- _mint(msg.sender, amount);
- } else {
+ if (msg.value > 0) {
+ if (msg.value != (s_buyFee * amount)) revert S__IncorrectEthValue();
+ _mint(msg.sender, amount);
+ } else {
i_weth.safeTransferFrom(msg.sender, address(this), (s_buyFee * amount));
_mint(msg.sender, amount);
}
```