Snowman Merkle Airdrop

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

payable fallback logic in buySnow traps unmatched ETH value and double-charges users in WETH

Root + Impact

Description

Normal behavior expects that if a user incorrectly calculates their msg.value and sends the wrong amount of ETH to a payable purchasing function, the transaction should revert to protect their funds.

However, the buySnow function uses an if/else block that checks for exact parity with the required fee. If the user sends ETH (msg.value > 0), but the amount does not exactly equal s_buyFee * amount, the else block automatically triggers. The contract proceeds to keep the wrongfully sent ETH, while simultaneously pulling the full correct required amount in WETH from the user's wallet via safeTransferFrom.

function buySnow(uint256 amount) external payable canFarmSnow {
@> if (msg.value == (s_buyFee * amount)) {
_mint(msg.sender, amount);
@> } else {
// Unmatched msg.value is completely ignored/trapped but NOT refunded
@> i_weth.safeTransferFrom(msg.sender, address(this), (s_buyFee * amount));
_mint(msg.sender, amount);
}

Risk

Likelihood:

  • This will occur whenever an average user misconfigures their transaction value or simply misestimates the required exact ETH cost.

Impact:

  • Users who submit a transaction with an incorrect msg.value will secretly lose their sent ETH, as it becomes permanently stuck in the contract's balance (until incidentally swept by collectFee()), AND they will be double-charged by having their WETH pulled anyway.

Proof of Concept

This Proof of Concept shows Alice mistakenly sending 4 ETH to the contract when the total fee required is 5 ETH. Instead of reverting her transaction to protect her, the else block catches it. It doesn't use her 4 ETH, doesn't refund it, and proceeds to completely wipe 5 WETH from her wallet. Alice ultimately loses 9 full tokens of value for a 5-token purchase.

function test_ETHStealing() public {
uint256 fee = snow.s_buyFee();
vm.deal(alice, fee);
weth.mint(alice, fee);
vm.startPrank(alice);
weth.approve(address(snow), fee);
// Alice accidentally sends slightly less ETH than required
snow.buySnow{value: fee - 1 ether}(1);
// WETH was successfully taken
assertEq(weth.balanceOf(alice), 0);
// ETH was successfully taken AND trapped
assertEq(alice.balance, 1 ether);
console2.log("Alice lost both her msg.value ETH and her wallet WETH!");
vm.stopPrank();
}

Recommended Mitigation

Recommended Mitigation: Restrict the msg.value handling. If msg.value > 0, explicitly demand that it perfectly matches the cost, reverting if it doesn't, so users don't trigger the WETH fallback while sending ETH.

function buySnow(uint256 amount) external payable canFarmSnow {
- if (msg.value == (s_buyFee * amount)) {
+ uint256 cost = (s_buyFee * amount) / PRECISION; // assuming precision is fixed
+ if (msg.value > 0) {
+ require(msg.value == cost, "Incorrect ETH value");
_mint(msg.sender, amount);
} else {
- i_weth.safeTransferFrom(msg.sender, address(this), (s_buyFee * amount));
+ i_weth.safeTransferFrom(msg.sender, address(this), cost);
_mint(msg.sender, amount);
}
Updates

Lead Judging Commences

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