Snowman Merkle Airdrop

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

double payment/spend vulnerability

Root + Impact

Description

  • The buySnow function is designed to accept payment in either ETH (if msg.value equals the exact required amount) or WETH (if msg.value does not equal the required amount).

  • The function fails to validate that msg.value == 0 when using the WETH payment path, allowing users to send both ETH and WETH simultaneously, resulting in overpayment and ETH being stuck in the contract.

  • It should be EITHER ETH OR WETH, never both.

// Root cause in the codebase with @> marks to highlight the relevant section
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)); // @> WETH payment path
_mint(msg.sender, amount);
}
// @> msg.value is never validated to be 0 in WETH path, allowing double payment
s_earnTimer = block.timestamp;
emit SnowBought(msg.sender, amount);
}

Risk

Likelihood:

  • Users will accidentally send ETH when intending to pay with WETH, especially if they send an incorrect ETH amount

  • The function logic is confusing as it doesn't clearly separate ETH and WETH payment methods

Impact:

  • Users lose additional ETH on top of their WETH payment, resulting in financial loss

  • ETH becomes permanently stuck in the contract (until collector calls collectFee())

Proof of Concept

// User sends wrong ETH amount (not exact), so it uses WETH path

// But msg.value is not validated to be 0, so user pays both

// Send 0.5 ETH + 1 WETH for 1 token

// User got tokens but paid both ETH and WETH

function testDoublePayment() public {
vm.prank(user);
// User sends wrong ETH amount (not exact), so it uses WETH path
// But msg.value is not validated to be 0, so user pays both
snow.buySnow{value: 0.5 ether}(1); // Send 0.5 ETH + 1 WETH for 1 token
// User got tokens but paid both ETH and WETH
assertEq(snow.balanceOf(user), 1);
assertEq(user.balance, 9.5 ether); // Lost 0.5 ETH
assertEq(weth.balanceOf(user), 9e18); // Lost 1 WETH
assertEq(address(snow).balance, 0.5 ether); // Contract has ETH
assertEq(weth.balanceOf(address(snow)), 1e18); // Contract has WETH
}

Recommended Mitigation

add a check "require(msg.value == 0"
This fix adds a guard clause that says:

  • "If you're not paying the exact ETH amount..."

  • "...then you MUST send 0 ETH to use the WETH path"

  • "If you send any ETH at all, I'll reject the transaction"

This prevents the double payment because:

  • ETH path: msg.value == exact_amount → takes ETH only

  • WETH path: msg.value == 0 → takes WETH only

- remove this code
+ add this code
function buySnow(uint256 amount) external payable canFarmSnow {
if (msg.value == (s_buyFee * amount)) {
_mint(msg.sender, amount);
} else {
+ require(msg.value == 0, "Cannot send ETH when paying with WETH");
i_weth.safeTransferFrom(msg.sender, address(this), (s_buyFee * amount));
_mint(msg.sender, amount);
}
s_earnTimer = block.timestamp;
emit SnowBought(msg.sender, amount);
}
Updates

Lead Judging Commences

yeahchibyke Lead Judge about 2 months ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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