Root + Impact
Description
If a user has approved WETH to the Snow contract and then calls Snow::buySnow with ETH, but sends an amount that doesn't exactly match s_buyFee * amount, the if branch fails and execution falls through to the else branch. The else branch pulls WETH via safeTransferFrom and mints the tokens, but the ETH sent with the call is not refunded — it remains stuck in the Snow contract.
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);
}
Risk
Likelihood:
When a userhas approved some weth, and then chosen to pay in eth, but doesnt send the exact correct number of weth
Impact:
The user pays twice — once in ETH (which is locked in the contract) and once in WETH. The ETH can only be recovered if the fee collector calls collectFee, at which point it goes to the collector, not back to the user.
Proof of Concept
function testCanUserSendMoreEthThanNecessary() public {
weth.mint(victory, FEE);
vm.deal(victory, FEE + 1 ether);
assert(snow.balanceOf(victory) == 0);
assert(victory.balance == FEE + 1 ether);
assert(address(snow).balance == 0);
vm.startPrank(victory);
weth.approve(address(snow), FEE);
snow.buySnow{value: FEE + 1 ether}(1);
vm.stopPrank();
assert(snow.balanceOf(victory) == 1);
assert(victory.balance == 0);
assert(address(snow).balance == FEE + 1 ether);
}
Recommended Mitigation
Refund any excess ETH back to the caller, and do not fall through to the WETH branch when ETH is sent. For example:
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));
+ uint256 cost = s_buyFee * amount;
+ if (msg.value > 0) {
+ require(msg.value >= cost, "Insufficient ETH");
_mint(msg.sender, amount);
+ uint256 refund = msg.value - cost;
+ if (refund > 0) {
+ (bool sent,) = payable(msg.sender).call{value: refund}("");
+ require(sent, "ETH refund failed");
+ }
+ } else {
+ i_weth.safeTransferFrom(msg.sender, address(this), cost);
+ _mint(msg.sender, amount);
}
s_earnTimer = block.timestamp;
}