Snowman Merkle Airdrop

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

ETH Loss Possible via Ambiguous Payment Logic in `Snow.sol::buySnow` function

[High] ETH Loss Possible via Ambiguous Payment Logic in Snow.sol::buySnow function


Description

In the buySnow function, if msg.value is not exactly equal to the calculated fee, the contract's logic implicitly assumes the user intended to pay in WETH via i_weth.safeTransferFrom(). Critically, any ETH sent as msg.value in this scenario, which does not match the expected s_buyFee * amount, is neither used nor refunded. This unexpected ETH gets permanently locked within the contract.


Risk

Users who mistakenly send an incorrect msg.value (e.g., a partial amount or an amount not corresponding to the ETH fee) will lose their sent ETH, as it will become irretrievably trapped in the contract. This could lead to user fund loss, especially if there are UI bugs or user misunderstandings.


Proof of Concept

To demonstrate this vulnerability, we will add a Foundry test that calls buySnow() with an msg.value that does not match the expected fee, proving the ETH gets trapped.

  1. Add Test Case to TestSnow.t.sol:
    Create a new test function in your TestSnow.t.t.sol file:

    // In test/TestSnow.t.sol
    function testETHGetsTrapped() public {
    uint256 wrongEthAmount = FEE / 2; // An amount of ETH that doesn't match the fee
    vm.deal(jerry, 2 * FEE); // Ensure jerry has enough ETH and WETH
    vm.prank(jerry);
    weth.approve(address(snow), FEE); // Approve WETH transfer
    vm.prank(jerry);
    snow.buySnow{value: wrongEthAmount}(1); // User sends partial ETH but expects WETH payment
    assertEq(address(snow).balance, wrongEthAmount); // The ETH is trapped
    assertEq(snow.balanceOf(jerry), 1); // User still gets Snow, proving WETH payment
    }
  2. Reproduction Steps:

    • Ensure you have MockWETH.sol, Snow.sol, and TestSnow.t.sol in your Foundry project.

    • Deploy MockWETH and set its address as i_weth in the Snow constructor.

    • Run the test TestSnow.t.sol::testETHGetsTrapped().

    • Expected Result: The buySnow() function should revert or refund the excess ETH.

    • Actual Result: The buySnow() function executes successfully, the user receives Snow tokens (indicating WETH payment was processed), but the wrongEthAmount sent via msg.value remains trapped in the Snow contract (address(snow).balance is wrongEthAmount), confirming the silent ETH loss.


Recommended Mitigation

Implement strict checks for msg.value in payable functions. If ETH is sent but not explicitly required, or if the amount is incorrect, the transaction should revert or the excess ETH should be explicitly refunded. For this contract's logic, rejecting unexpected ETH is the most robust approach.

Proposed Fix in Snow.sol:

function buySnow(uint256 amount) external payable canFarmSnow {
+ if (msg.value > 0 && msg.value != (s_buyFee * amount)) {
+ revert S__UnexpectedETH(); // Add a custom error for clarity and revert unexpected ETH
+ }
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);
}
s_earnTimer = block.timestamp;
emit SnowBought(msg.sender, amount);
}
Updates

Lead Judging Commences

yeahchibyke Lead Judge
3 months ago
yeahchibyke Lead Judge 3 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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