Snowman Merkle Airdrop

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

[M-1] Users who send an incorrect ETH amount to `Snow::buySnow` while having a WETH approval lose their ETH and have their WETH taken as well

Root + Impact

Description

If a user has approved WETH to the Snow contract and then calls Snow::buySnow with ETH, but sends an amount that doesn't exactly match s_buyFee * amount, the if branch fails and execution falls through to the else branch. The else branch pulls WETH via safeTransferFrom and mints the tokens, but the ETH sent with the call is not refunded — it remains stuck in the Snow contract.

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);
}

Risk

Likelihood:

When a userhas approved some weth, and then chosen to pay in eth, but doesnt send the exact correct number of weth

Impact:

The user pays twice — once in ETH (which is locked in the contract) and once in WETH. The ETH can only be recovered if the fee collector calls collectFee, at which point it goes to the collector, not back to the user.

Proof of Concept

function testCanUserSendMoreEthThanNecessary() public {
weth.mint(victory, FEE);
vm.deal(victory, FEE + 1 ether);
assert(snow.balanceOf(victory) == 0);
assert(victory.balance == FEE + 1 ether);
assert(address(snow).balance == 0);
vm.startPrank(victory);
weth.approve(address(snow), FEE);
snow.buySnow{value: FEE + 1 ether}(1);
vm.stopPrank();
assert(snow.balanceOf(victory) == 1);
assert(victory.balance == 0);
assert(address(snow).balance == FEE + 1 ether);
}

Recommended Mitigation

Refund any excess ETH back to the caller, and do not fall through to the WETH branch when ETH is sent. For example:

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));
+ uint256 cost = s_buyFee * amount;
+ if (msg.value > 0) {
+ require(msg.value >= cost, "Insufficient ETH");
_mint(msg.sender, amount);
+ uint256 refund = msg.value - cost;
+ if (refund > 0) {
+ (bool sent,) = payable(msg.sender).call{value: refund}("");
+ require(sent, "ETH refund failed");
+ }
+ } else {
+ i_weth.safeTransferFrom(msg.sender, address(this), cost);
+ _mint(msg.sender, amount);
}
s_earnTimer = block.timestamp;
}
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 12 days 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!