Snowman Merkle Airdrop

First Flight #42
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Impact: high
Likelihood: high
Invalid

[H-05] Over-Payment in buySnow

Over-Payment in buySnow

Description

  • Normal behaviour: User pays either exact ETH or exact WETH fee.

  • Issue: If any ETH is attached but not equal to the fee, the function still
    charges full WETH and silently keeps the stray ETH.

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); // stray ETH stays in contract
}
}

Risk

Likelihood

  • Happens whenever a user (or dApp) mistakenly attaches ETH while selecting the WETH path.

  • Also exploitable by malicious actor sending dust ETH to others via eth_sendTransaction.

Impact

  • Users are over-charged (WETH + ETH).

  • Fee collector receives unintended ETH windfall.

Proof of Concept

test/PoC_Snow_BuySnow_Overpay.t.sol

// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.24;
import {Test} from "forge-std/Test.sol";
import {Snow} from "../src/Snow.sol";
import {DeploySnow} from "../script/DeploySnow.s.sol";
import {MockWETH} from "../src/mock/MockWETH.sol";
/**
* @title PoC_Snow_BuySnow_Overpay
* @notice Demonstrates that a user can accidentally or maliciously send ETH **and**
* WETH when calling `buySnow`, causing them to over-pay and the extra ETH
* becomes protocol revenue (transferred to the fee collector), violating
* the principle of exact-price purchases.
*/
contract PoC_Snow_BuySnow_Overpay is Test {
Snow snow;
MockWETH weth;
address feeCollector;
uint256 BUY_FEE;
address user;
function setUp() public {
DeploySnow deployer = new DeploySnow();
snow = deployer.run();
weth = deployer.weth();
feeCollector = deployer.collector();
BUY_FEE = deployer.FEE(); // This already includes 1e18 scaling inside Snow
user = makeAddr("user");
weth.mint(user, BUY_FEE);
}
function testOverpayWithETHandWETH() public {
// Fund user with some ETH for the stray payment
vm.deal(user, 1 ether);
// Approve the necessary WETH amount
vm.startPrank(user);
weth.approve(address(snow), BUY_FEE);
// Craft transaction: send 1 wei of ETH *plus* pay in WETH
snow.buySnow{value: 1}(1);
vm.stopPrank();
// User received 1 SNOW, but has paid BUY_FEE WETH + 1 wei ETH
assertEq(snow.balanceOf(user), 1);
assertEq(weth.balanceOf(address(snow)), BUY_FEE, "WETH collected");
assertEq(address(snow).balance, 1, "Unexpected ETH trapped in contract");
// When collector withdraws fees, they receive the stray ETH as windfall
vm.prank(feeCollector);
snow.collectFee();
assertEq(feeCollector.balance, 1, "Collector pocketed stray ETH - over-payment confirmed");
}
}

Recommended Mitigation

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);
- }
+ if (msg.value > 0) {
+ require(msg.value == s_buyFee * amount, "Exact ETH fee required");
+ _mint(msg.sender, amount);
+ } else {
+ i_weth.safeTransferFrom(msg.sender, address(this), s_buyFee * amount);
+ _mint(msg.sender, amount);
+ }
}
Updates

Lead Judging Commences

yeahchibyke Lead Judge
12 days ago
yeahchibyke Lead Judge 11 days ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.