Snowman Merkle Airdrop

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

Incorrect Fee Calculation in buySnow Funcation

Root + Impact

The root cause is a multiplication by PRECISION (1e18) when storing the fee in the constructor, which then gets multiplied again by the purchase amount in buySnow. This results in a required payment that is astronomically large and completely impractical, effectively breaking the purchase functionality.

Description

  • Normally, a user should be able to buy tokens by paying a reasonable fee. For example, if the fee is set to 1 unit (say 1 dollar worth of ETH), buying 1 token should cost around that fee. However, because the contract multiplies the fee by PRECISION during deployment and then again by the token amount during purchase, the actual amount required becomes enormous. For a fee of 1, buying 1 full token (18 decimals) would require 1e36 wei, which is far more than all the Ether that exists. Even buying just 1 wei of the token costs 1 Ether, making the token impossible to buy in any reasonable quantity.

constructor(address _weth, uint256 _buyFee, address _collector) ... {
...
@> s_buyFee = _buyFee * PRECISION; // Fee is inflated by 1e18
...
}
function buySnow(uint256 amount) external payable canFarmSnow {
if (msg.value == (s_buyFee * amount)) { // Fee multiplied again by amount
_mint(msg.sender, amount);
} else {
@> i_weth.safeTransferFrom(msg.sender, address(this), (s_buyFee * amount));
_mint(msg.sender, amount);
}
...
}

Risk

Likelihood:

  • Users cannot buy tokens in any practical amount; the purchase function is effectively unusable.

  • The contract’s intended functionality (buying Snow tokens) is broken, and any value sent with a purchase will be either stuck or require enormous amounts to work.

Impact:

  • Impact 1

  • Impact 2

Proof of Concept:

The following Foundry test demonstrates the issue. With _buyFee = 1, buying 1 wei of Snow costs 1 Ether, and buying 1 full token would require 1e36 wei (more than total Ether supply).

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "forge-std/Test.sol";
import "../src/snow.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract WETHMock is IERC20 {
string public name = "WETH";
string public symbol = "WETH";
uint8 public decimals = 18;
mapping(address => uint256) public override balanceOf;
mapping(address => mapping(address => uint256)) public override allowance;
function deposit() external payable {
balanceOf[msg.sender] += msg.value;
}
function withdraw(uint256 amount) external {
require(balanceOf[msg.sender] >= amount, "insufficient balance");
balanceOf[msg.sender] -= amount;
payable(msg.sender).transfer(amount);
}
function transfer(address recipient, uint256 amount) external override returns (bool) {
balanceOf[msg.sender] -= amount;
balanceOf[recipient] += amount;
return true;
}
function approve(address spender, uint256 amount) external override returns (bool) {
allowance[msg.sender][spender] = amount;
return true;
}
function transferFrom(address sender, address recipient, uint256 amount) external override returns (bool) {
allowance[sender][msg.sender] -= amount;
balanceOf[sender] -= amount;
balanceOf[recipient] += amount;
return true;
}
function totalSupply() external view override returns (uint256) { return 0; }
}
contract SnowTest is Test {
Snow snow;
WETHMock weth;
address collector = address(0x123);
uint256 buyFee = 1;
function setUp() public {
weth = new WETHMock();
snow = new Snow(address(weth), buyFee, collector);
}
function test_IncorrectFeeCalculation() public {
uint256 s_buyFee = snow.s_buyFee();
assertEq(s_buyFee, buyFee * 10**18);
uint256 amount = 10**18; // 1 token (18 decimals)
uint256 required = s_buyFee * amount; // = 1e36 wei
assertEq(required, 10**36);
// Total Ether in existence is about 120 million ETH = 1.2e26 wei
uint256 totalEthInWorld = 120_000_000 ether; // ~1.2e26 wei
assertGt(required, totalEthInWorld, "Required amount is far greater than total Ether supply");
// Buying just 1 wei of token costs 1 ether
uint256 smallAmount = 1;
uint256 requiredSmall = s_buyFee * smallAmount; // = 1e18 wei
assertEq(requiredSmall, 1 ether);
// Attempt to buy 1 wei with 1 ether (should succeed)
vm.deal(address(this), 1 ether);
vm.prank(address(this));
snow.buySnow{value: 1 ether}(smallAmount);
assertEq(snow.balanceOf(address(this)), smallAmount);
// To buy 1 full token, we need 1e36 wei (theoretically possible in test with huge balance)
vm.deal(address(this), required);
vm.prank(address(this));
snow.buySnow{value: required}(amount);
assertEq(snow.balanceOf(address(this)), amount + smallAmount);
}
function test_UnrealisticPrice() public {
uint256 pricePerToken = (snow.s_buyFee() * 1) * 10**18; // Price for 1 token in wei
assertEq(pricePerToken, 10**36);
// This price is astronomically high, making the token impractical to buy
}
}

Recommended Mitigation

The fee should not be multiplied by PRECISION in the constructor. Instead, the _buyFee should represent the actual cost per full token (in wei), and the buySnow function should multiply that base fee by the amount (in token units). Alternatively, if a fixed fee per token is desired, store it as is and do not multiply by PRECISION.

constructor(address _weth, uint256 _buyFee, address _collector) ... {
...
- s_buyFee = _buyFee * PRECISION;
+ s_buyFee = _buyFee; // _buyFee should be the fee per full token in wei
...
}
Updates

Lead Judging Commences

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