Snowman Merkle Airdrop

AI First Flight #10
Beginner FriendlyFoundrySolidityNFT
EXP
View results
Submission Details
Impact: low
Likelihood: low
Invalid

### [L-2] `Snow::buySnow` with a wrong `msg.value` still charges full WETH and traps the sent ETH

Description

buySnow is meant to let a user pay for Snow with either native ETH or WETH.

It mints via ETH only when msg.value == s_buyFee * amount exactly; otherwise it falls through to pull the full WETH price. A buyer who sends a partial/incorrect msg.value therefore pays the full amount in WETH and loses the ETH they sent — it stays in the contract with no refund.

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)); // full WETH pulled
_mint(msg.sender, amount);
}
@> // any ETH sent in the else branch is kept, never refunded
}

Risk

Likelihood: Low

  • Requires the buyer to send a non-exact msg.value while also having WETH approved; a UX slip rather than an attacker path.

Impact: Low

  • The buyer is charged twice (full WETH + the stray ETH), and the stray ETH is only recoverable by the fee collector. User fund loss, bounded by the amount of ETH mistakenly sent.

Proof of Concept

The attacker has WETH approved and sends 1 wei of ETH with the buy; the contract charges full WETH and traps the 1 wei.

Run forge test --mt test_L2_buySnow_double_pay_stuck_eth -vvv:

function test_L2_buySnow_double_pay_stuck_eth() public {
uint256 fee = snow.s_buyFee(); // 1 * 1e18 per token
uint256 amount = 1;
weth.mint(attacker, fee * amount);
vm.prank(attacker); weth.approve(address(snow), fee * amount);
vm.deal(attacker, 1 wei);
vm.prank(attacker);
snow.buySnow{value: 1 wei}(amount); // 1 wei != fee => WETH branch
assertEq(snow.balanceOf(attacker), amount); // minted 1 Snow
assertEq(weth.balanceOf(address(snow)), fee * amount); // paid FULL price in WETH
assertEq(address(snow).balance, 1 wei); // AND ETH stuck, no refund
}

Result: PASS — buyer paid full WETH and the 1 wei ETH is trapped in the contract.

Recommended Mitigation

Branch on explicit intent and reject unexpected value:

function buySnow(uint256 amount) external payable canFarmSnow {
+ uint256 cost = s_buyFee * amount;
+ if (msg.value != 0) {
+ require(msg.value == cost, "incorrect ETH amount");
+ } else {
+ i_weth.safeTransferFrom(msg.sender, address(this), cost);
+ }
- 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);
- }
+ _mint(msg.sender, amount);
}

Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 2 hours ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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

Give us feedback!