Description
-
The buySnow() function is intended to accept either exact ETH payment OR WETH transfer for purchasing Snow tokens.
-
The current implementation uses an if-else structure that mints tokens when msg.value matches the expected fee, and falls through to WETH transfer otherwise. This means any non-matching msg.value (including partial payments) will be accepted and kept by the contract while also charging WETH.
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:
-
Users may accidentally send partial ETH along with their transaction (wallet defaults, user error)
-
Front-end bugs or incorrect gas estimations could cause non-zero msg.value to be sent unintentionally
-
Users unfamiliar with the dual-payment system may send some ETH expecting partial payment
Impact:
-
Users lose both their sent ETH AND their WETH when msg.value doesn't exactly match
-
Sent ETH becomes trapped in the contract (only collector can withdraw)
-
Double-charging users for Snow tokens
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 PaymentFallthroughPoC is Test {
Snow snow;
MockWETH weth;
address alice = makeAddr("alice");
function setUp() public {
weth = new MockWETH();
snow = new Snow(address(weth), 1, address(this));
}
function testPartialEthPaymentDoubleCharges() public {
uint256 buyFee = snow.s_buyFee();
vm.deal(alice, 1 ether);
weth.mint(alice, 2 ether);
vm.startPrank(alice);
weth.approve(address(snow), type(uint256).max);
uint256 aliceEthBefore = alice.balance;
uint256 aliceWethBefore = weth.balanceOf(alice);
snow.buySnow{value: 0.5 ether}(1);
vm.stopPrank();
assertEq(alice.balance, aliceEthBefore - 0.5 ether);
assertEq(weth.balanceOf(alice), aliceWethBefore - buyFee);
assertEq(address(snow).balance, 0.5 ether);
}
}
Recommended Mitigation
function buySnow(uint256 amount) external payable canFarmSnow {
+ uint256 totalFee = s_buyFee * amount;
+
+ if (msg.value > 0 && msg.value != totalFee) {
+ revert S__InvalidPayment();
+ }
+
- if (msg.value == (s_buyFee * amount)) {
+ if (msg.value == totalFee) {
_mint(msg.sender, amount);
} else {
- i_weth.safeTransferFrom(msg.sender, address(this), (s_buyFee * amount));
+ i_weth.safeTransferFrom(msg.sender, address(this), totalFee);
_mint(msg.sender, amount);
}
s_earnTimer = block.timestamp;
emit SnowBought(msg.sender, amount);
}
Add new error:
error S__NotAllowed();
error S__ZeroAddress();
error S__ZeroValue();
error S__Timer();
error S__SnowFarmingOver();
+ error S__InvalidPayment();