Snowman Merkle Airdrop

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

Incorrect ETH payment can charge both ETH and WETH

Incorrect ETH payment can charge both ETH and WETH

Severity

Medium

Description

Snow.buySnow() supports payment with either native ETH or WETH. The function decides which payment path to use by checking whether msg.value exactly equals the expected ETH price:

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 a user sends a non-zero ETH amount that is not exactly equal to s_buyFee * amount, the function enters the WETH branch. It pulls the full WETH fee, but it does not reject or refund the ETH sent with the transaction.

Affected code:

  • src/Snow.sol:79

  • src/Snow.sol:80

  • src/Snow.sol:83

Risk

A user with WETH allowance can accidentally pay both ETH and WETH for the same Snow purchase. The incorrect ETH remains in the contract and can later be collected by the collector.

Impact:

  • users can overpay,

  • funds are silently accepted in the wrong payment branch,

  • collector can collect both the incorrect ETH and the WETH fee.

Proof of Concept

The PoC gives a buyer both ETH and enough WETH allowance to buy 1 Snow. The buyer calls buySnow(1) with an ETH amount that is non-zero but not exactly equal to the expected fee. Because the ETH amount is not exact, the function enters the WETH branch and pulls the full WETH fee. The incorrect ETH sent with the call remains in the Snow contract.

Add this test to test/AuditFuzz.t.sol:

function testFuzz_WrongEthAmountWithWethAllowanceChargesBothEthAndWeth(uint256 wrongEthAmount) public {
wrongEthAmount = bound(wrongEthAmount, 1, 4 ether);
MockWETH weth = new MockWETH();
Snow snow = new Snow(address(weth), 5, makeAddr("collector"));
uint256 fee = snow.s_buyFee();
vm.assume(wrongEthAmount != fee);
address buyer = makeAddr("buyer");
vm.deal(buyer, wrongEthAmount);
weth.mint(buyer, fee);
vm.startPrank(buyer);
weth.approve(address(snow), fee);
snow.buySnow{value: wrongEthAmount}(1);
vm.stopPrank();
assertEq(address(snow).balance, wrongEthAmount);
assertEq(weth.balanceOf(address(snow)), fee);
assertEq(snow.balanceOf(buyer), 1);
}

Run:

forge test --match-test testFuzz_WrongEthAmountWithWethAllowanceChargesBothEthAndWeth --fuzz-runs 10000 -vvv

Result:

[PASS] testFuzz_WrongEthAmountWithWethAllowanceChargesBothEthAndWeth(uint256) (runs: 10000)

The final assertions show that the Snow contract receives both the wrong ETH amount and the full WETH fee while the buyer receives only 1 Snow.

Mitigation

Separate ETH and WETH payment paths explicitly. If ETH is sent, require the exact ETH amount. If no ETH is sent, charge WETH.

Example fix:

function buySnow(uint256 amount) external payable canFarmSnow {
if (amount == 0) revert S__ZeroValue();
uint256 cost = s_buyFee * amount;
if (msg.value > 0) {
if (msg.value != cost) revert S__InvalidPayment();
_mint(msg.sender, amount);
} else {
i_weth.safeTransferFrom(msg.sender, address(this), cost);
_mint(msg.sender, amount);
}
s_earnTimer = block.timestamp;
emit SnowBought(msg.sender, amount);
}
Updates

Lead Judging Commences

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