Snowman Merkle Airdrop

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

Missing Refund in `buySnow` Leads to Stolen Native ETH

Description

The buySnow function in Snow.sol checks if the msg.value is strictly equal to the required cost (s_buyFee * amount). If it matches, the user pays in native ETH. If it does not match, the contract assumes the user is paying in WETH and transfers WETH from their balance. However, if the user sent native ETH that does not perfectly match the required cost (e.g., sending excess ETH), the contract takes their WETH but does not refund the sent msg.value.

Risk

Medium Likelihood, High Impact: Users interacting with the contract via a misconfigured front-end or making minor calculation errors will permanently lose their sent ETH, as there is no mechanism to rescue or refund it.

Proof of Concept

File: src/Snow.sol

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));
_mint(msg.sender, amount);
// VULNERABILITY: If msg.value > 0, it is permanently locked in the contract
}

Exploit Execution:

  1. The buyFee is 5 ether.

  2. A user mistakenly attaches 6 ether of native ETH as msg.value while having 5 WETH in their wallet.

  3. The contract evaluates msg.value == (s_buyFee * amount) (6 ether == 5 ether), which returns false.

  4. The contract proceeds to the else block, taking 5 WETH from the user.

  5. The 6 ether in native ETH remains locked inside the Snow contract forever.

Recommended Mitigation

Explicitly check msg.value and refund any excess ETH, or ensure that the user either pays entirely in ETH or entirely in WETH, but not both concurrently.

function buySnow(uint256 amount) external payable canFarmSnow {
uint256 totalCost = s_buyFee * amount;
if (msg.value > 0) {
require(msg.value >= totalCost, "S__InsufficientFunds");
_mint(msg.sender, amount);
if (msg.value > totalCost) {
(bool success, ) = payable(msg.sender).call{value: msg.value - totalCost}("");
require(success, "Refund failed");
}
} else {
i_weth.safeTransferFrom(msg.sender, address(this), totalCost);
_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!