Snowman Merkle Airdrop

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

buySnow() charges WETH on any non-exact ETH payment and keeps the ETH, so a user can pay twice

Root + Impact

Description

// src/Snow.sol - buySnow() treats any non-exact ETH value as a WETH payment
@> function buySnow(uint256 amount) external payable {
@> if (block.timestamp > i_deploymentTimestamp + FARMING_DURATION) {
@> revert S__TimeLimit();
@> }
@> 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;
@> }
​
The README says Snow can be bought with either WETH or native ETH. The code decides which by exact equality: only msg.value == s_buyFee * amount takes the ETH path. Every other value, including a user who sends too much ETH, too little ETH, or ETH by mistake while intending to pay WETH, falls into the else branch, which pulls the full WETH price and mints, while the ETH that was sent stays in the contract (it can later be collected by the fee collector through collectFee()). There is no check that msg.value == 0 in the WETH branch, no refund of excess ETH, and no revert on a mismatched payment.
​
Impact: direct loss of funds for the buyer, who pays in both assets for one purchase. With the deployed fee of 5 ether per Snow (script/DeploySnow.s.sol), a buyer who sends 1 ether by mistake and has approved WETH loses 1 ether plus 5 WETH for 1 Snow. A buyer who sends 6 ether for 1 Snow loses 6 ether plus 5 WETH.

Risk

Likelihood:

Impact:

Proof of Concept

// Explanation: the else branch is reached for every ETH value other than the exact price, so a mistaken ETH payment is charged on top of the WETH price instead of reverting.
// 1. Deploy as in script/DeploySnow.s.sol (buyFee 5 ether). Victor has
// 5 WETH approved and sends buySnow(1) with 1 ether by mistake.
// 2. msg.value (1 ether) != s_buyFee * 1 (5 ether), so the else branch
// runs: 5 WETH are pulled from Victor and 1 Snow is minted.
// 3. The 1 ether stays in the Snow contract. collectFee() can later
// send it to the fee collector - Victor never gets it back.
// 4. The same happens for an overpayment: buySnow(1) with 6 ether also
// takes 5 WETH and keeps the 6 ether.
// The project's tests only cover the exact ETH payment and the
// zero-value WETH payment (test/TestSnow.t.sol, testCanBuySnowWithEth
// and testCanBuySnow), so a mismatched payment is untested.

Recommended Mitigation

Explanation: only accept one payment method per call, and reject any other value instead of silently taking both.
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__InvalidPayment();
}
​
Declare a new error S__InvalidPayment (or reuse an existing error) for the mismatched-payment case.
Updates

Lead Judging Commences

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