Strict ETH Value Check in Snow.sol::buySnow()
Causes Fund Loss
Description
The `Snow.sol::buySnow` function uses strict equality (==), leading to:
1. Permanent loss of excess ETH if msg.value > (s_buyFee * amount) or if msg.value < (s_buyFee * amount)
2. Unnecessary reverts when users accidentally send more or less ETH than needed
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:
Reason 1: This occurs when a user directly interacts with the contract (e.g., via Etherscan, a script, or wallet) and sends more ETH than required while intending to use the WETH payment path.
Reason 2: There's no strict validation enforcing msg.value == 0
in the WETH path, so the contract silently accepts unintended ETH.
Impact:
Users lose ETH permanently, as it becomes irrecoverable unless the contract owner manually refunds or exposes a withdrawal mechanism.
Increases contract balance without corresponding state accounting, making audits and fund tracking harder.
Proof of Concept :
This test shows how overpaying with ETH while having WETH approved causes funds to get stuck:
function test_ETHGetsStuckWhenOverpaying() public {
address user = makeAddr("overpayingUser");
uint256 amount = 1;
uint256 requiredFee = snow.s_buyFee() * amount;
uint256 overpaymentAmount = requiredFee + 1 ether;
vm.deal(user, overpaymentAmount);
weth.mint(user, requiredFee);
vm.prank(user);
weth.approve(address(snow), requiredFee);
vm.prank(user);
snow.buySnow{value: overpaymentAmount}(amount);
assertEq(address(snow).balance, overpaymentAmount, "ETH stuck in contract");
assertEq(weth.balanceOf(address(snow)), requiredFee, "WETH was taken");
assertEq(weth.balanceOf(user), 0, "User's WETH was spent");
assertEq(snow.balanceOf(user), amount, "User received tokens");
assertEq(user.balance, 0, "User's ETH was taken");
}
Recommended Mitigation
The original logic checked msg.value == totalFee
and defaulted to WETH transfer in all other cases — including cases where users overpaid in ETH, leading to silent acceptance of ETH and WETH being pulled, causing ETH to get stuck in the contract.
The updated logic mitigates this by:
-
Validating ETH flow upfront:
-
If any ETH is sent, it must exactly match the expected amount (totalFee
), or the transaction reverts.
-
This eliminates the accidental case of overpayment going unnoticed.
-
Using else
for WETH path only:
-
Adding nonReentrant
:
This ensures that:
-
ETH is only accepted intentionally and correctly.
-
WETH is only pulled when no ETH is sent.
-
No asset is accidentally stuck or double-charged.
- 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);
- }
+ function buySnow(uint256 amount) external payable canFarmSnow nonReentrant {
+ require(amount > 0, "Amount must be > 0");
+ uint256 totalFee = s_buyFee * amount;
+
+ if (msg.value > 0) {
+ require(msg.value == totalFee, "Incorrect ETH sent");
+ } else {
+ i_weth.safeTransferFrom(msg.sender, address(this), totalFee);
+ }
+
+ _mint(msg.sender, amount);
+ s_earnTimer = block.timestamp;
+
+ emit SnowBought(msg.sender, amount);
+ }