Snowman Merkle Airdrop

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

Partial ETH Sent to buySnow Gets Stuck in Contract

Root + Impact

Description

  • Normal behavior: Users should either pay the exact ETH amount OR use WETH, with incorrect ETH amounts being rejected.

  • Specific issue: If a user sends ETH that doesn't match the exact fee, the function falls through to the WETH payment path without refunding the ETH. User loses the ETH AND pays WETH.

// Root cause in src/Snow.sol:79-90
function buySnow(uint256 amount) external payable canFarmSnow {
@> if (msg.value == (s_buyFee * amount)) {
_mint(msg.sender, amount);
@> } else {
// @> If msg.value != 0 but not exact, ETH is kept and WETH is also taken!
i_weth.safeTransferFrom(msg.sender, address(this), (s_buyFee * amount));
_mint(msg.sender, amount);
}
s_earnTimer = block.timestamp;
emit SnowBought(msg.sender, amount);
}

Risk

Likelihood:

  • User sends partial ETH by mistake (e.g., miscalculates fee)

  • User has WETH approved, so the WETH path succeeds

  • Both ETH and WETH are taken

Impact:

  • Users lose funds when sending incorrect ETH amounts

  • ETH becomes permanently stuck in the contract (no withdrawal mechanism)

  • User effectively pays 1.5x or more for tokens

Proof of Concept

The following test demonstrates that when a user sends partial ETH (not the exact amount), the function does not revert. Instead, it falls through to the WETH payment path, taking the user's WETH while keeping the partial ETH in the contract. The user ends up paying both partial ETH and full WETH, effectively overpaying.

function test_PartialEthGetsStuck() public {
MockWETH testWeth = new MockWETH();
Snow testSnow = new Snow(address(testWeth), 5, makeAddr("collector"));
address user = makeAddr("user");
uint256 buyFee = testSnow.s_buyFee(); // 5 * 10^18
uint256 partialEth = buyFee / 2;
vm.deal(user, partialEth + buyFee);
testWeth.mint(user, buyFee);
vm.prank(user);
testWeth.approve(address(testSnow), buyFee);
uint256 contractEthBefore = address(testSnow).balance;
vm.prank(user);
testSnow.buySnow{value: partialEth}(1);
// Partial ETH is STUCK in contract!
assertEq(address(testSnow).balance - contractEthBefore, partialEth);
// User paid BOTH partial ETH AND full WETH!
}

Recommended Mitigation

Add an explicit check for incorrect ETH amounts. The function should only accept exact ETH payment or zero ETH (for WETH payment). Any other amount should revert to prevent users from losing funds.

+ error S__IncorrectEthAmount();
function buySnow(uint256 amount) external payable canFarmSnow {
if (msg.value == (s_buyFee * amount)) {
_mint(msg.sender, amount);
- } else {
+ } else if (msg.value == 0) {
i_weth.safeTransferFrom(msg.sender, address(this), (s_buyFee * amount));
_mint(msg.sender, amount);
+ } else {
+ revert S__IncorrectEthAmount();
}
s_earnTimer = block.timestamp;
emit SnowBought(msg.sender, amount);
}
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 1 day 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!