Snowman Merkle Airdrop

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

[H-2] Unfunded Approval Allows Free Token Minting in Snow.sol

Root + Impact


  • Root: The buySnow function mints tokens via _mint without verifying sufficient WETH balance before safeTransferFrom, relying on ERC20 revert;

  • Impact: Attackers can mint tokens for free or underpay, undermining the contract’s economic model.

Description

  • The buySnow function intends to charge s_buyFee * amount WETH per mint but proceeds with _mint after safeTransferFrom without ensuring the sender’s balance covers the fee.

  • This allows minting with insufficient funds if safeTransferFrom doesn’t revert, as demonstrated by a test minting 1 token with only half the required WETH.

// Root cause in the codebase with @> marks to highlight the relevant section// Root cause in the codebase with @> marks to highlight the relevant section
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);
}
s_earnTimer = block.timestamp;
emit SnowBought(msg.sender, amount);
}

Risk

Likelihood:

  • When an attacker calls buySnow with insufficient WETH but a set allowance.

  • During any transaction exploiting a lenient MockWETH or manipulated ERC20.

Impact:

  • Enables free or underpaid token minting, depleting contract revenue.

  • Risks exhausting the total supply if unchecked.

Proof of Concept

  • Unfunded Mint Test: A user mints 1 token with half the required WETH, confirming the flaw.

  • You may copy paste this into the snow test suite:

function testBuySnowUnfundedVulnerability() public {
vm.startPrank(jerry);
uint256 buyFee = snow.s_buyFee(); // 5e14
weth.mint(jerry, buyFee / 2); // Half the fee (2.5e14)
weth.approve(address(snow), buyFee); // Approve full fee
vm.expectEmit();
emit SnowBought(jerry, 1); // Expect event
snow.buySnow(1); // Mints despite insufficient balance
vm.stopPrank();
uint256 balanceJerry = snow.balanceOf(jerry);
console2.log("Jerry's tokens after unfunded mint:", balanceJerry);
console2.log("Jerry's full Weth balance:", weth.balanceOf(jerry));
}
  • Result: Jerry's tokens after unfunded mint: 1, Jerry's full Weth balance: 2.5e14, proving 1 token minted without full payment.

Recommended Mitigation

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);
- }
- s_earnTimer = block.timestamp;
- emit SnowBought(msg.sender, amount);
+ if (msg.value != (s_buyFee * amount) && i_weth.balanceOf(msg.sender) < (s_buyFee * amount)) {
+ revert S__InsufficientFunds();
+ }
+ s_earnTimer = block.timestamp;
+ emit SnowBought(msg.sender, amount);
+ 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);
+ }
}
  • Add a balance check before minting to prevent underfunded transactions.

Updates

Lead Judging Commences

yeahchibyke Lead Judge
3 months ago
yeahchibyke Lead Judge 3 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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