Snowman Merkle Airdrop

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

Unexpected ETH sent during WETH purchases becomes permanently locked

Root + Impact

Description

Normal behavior

The buySnow() function allows users to purchase SNOW using either native ETH or WETH. When paying with ETH, the transaction value should exactly match the purchase price. When paying with WETH, the transaction should not include any native ETH.

Issue

The function determines the payment method solely by comparing msg.value against the required ETH amount. When msg.value is non-zero but does not exactly equal the expected value, execution falls back to the WETH payment path.

As a result, any unexpected ETH sent together with a WETH purchase remains permanently locked inside the contract. The user pays with WETH while simultaneously losing the accidentally supplied ETH, since the contract neither refunds the excess ETH nor rejects the transaction.

function buySnow(uint256 amount) external payable {
...
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);
}
...
}

The WETH execution path does not validate that msg.value is zero, allowing native ETH to become permanently trapped inside the contract.

Risk

Likelihood:

  • Users can unintentionally send native ETH while attempting to purchase SNOW using WETH.

  • Wallets, scripts, or frontend integrations may include a non-zero msg.value during contract interactions.

Impact:

  • Native ETH sent alongside a WETH purchase becomes permanently locked inside the contract.

  • Users suffer an irreversible loss of funds because the transaction succeeds without refunding or rejecting the unexpected ETH.

Proof of Concept

// Assume 1 SNOW costs 1 WETH.
uint256 amount = 1;
// User accidentally sends 0.5 ETH together with the transaction.
vm.deal(alice, 1 ether);
vm.startPrank(alice);
weth.approve(address(snow), 1 ether);
snow.buySnow{value: 0.5 ether}(amount);
// Result:
// - 1 WETH is transferred from Alice.
// - Alice receives 1 SNOW.
// - 0.5 ETH remains locked inside the contract.
// - No refund is performed.
vm.stopPrank();

Recommended Mitigation

The contract should explicitly distinguish between ETH and WETH payments. When purchasing with WETH, the transaction should reject any non-zero msg.value to prevent accidental ETH loss.

function buySnow(uint256 amount) external payable {
- if (msg.value == s_buyFee * amount) {
+ if (msg.value == s_buyFee * amount) {
_mint(msg.sender, amount);
} else {
+ require(msg.value == 0, "Unexpected ETH sent");
+
i_weth.safeTransferFrom(
msg.sender,
address(this),
s_buyFee * amount
);
_mint(msg.sender, amount);
}
}
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 3 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!