Snowman Merkle Airdrop

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

Constructor Fee Double-Scaling in `Snow.sol` Makes `buySnow()` Permanently Uncallable

Description

  • The Snow constructor is intended to accept a fee parameter and store it for use in buySnow(). The buySnow() function computes the required payment as s_buyFee * amount. The constructor also reverts on _buyFee == 0 (line 65), so the only value that would avoid the inflation is explicitly blocked.

  • The constructor multiplies _buyFee by PRECISION (1e18) before storing it in s_buyFee. Then buySnow() applies the already-scaled fee directly to the token count. This inflates the required payment by a factor of 1e18, making buySnow() permanently unusable for any realistic fee value.

// Snow.sol — line 73 (constructor)
s_buyFee = _buyFee * PRECISION; // @> stores fee already scaled by 1e18
// Snow.sol — line 80 (buySnow)
if (msg.value == (s_buyFee * amount)) { // @> uses pre-scaled value, no division

Risk

Likelihood:

  • Every valid deployment triggers this bug — the constructor reverts on _buyFee == 0, so there is no valid constructor input that avoids the inflation

  • Both payment paths are affected: the ETH path checks msg.value == (s_buyFee * amount), and the WETH fallback calls safeTransferFrom with the same inflated value

Impact:

  • With _buyFee = 1 (intending 1-wei fee), the contract requires 1 ETH per token instead of 1 wei

  • With _buyFee = 1e16 (0.01 ETH intended), the stored value becomes 1e34, requiring 1e16 ETH per token — roughly 83 million times the entire ETH supply

  • buySnow() is the primary Snow token acquisition function — the double-scaling makes it permanently uncallable for any standard deployment

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Test, console} from "forge-std/Test.sol";
import {Snow} from "../src/Snow.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract MockWETH9 {
mapping(address => uint256) public balanceOf;
function transferFrom(address from, address to, uint256 amount) external returns (bool) {
balanceOf[from] -= amount;
balanceOf[to] += amount;
return true;
}
function transfer(address to, uint256 amount) external returns (bool) {
balanceOf[address(this)] -= amount;
balanceOf[to] += amount;
return true;
}
function mint(address to, uint256 amount) external {
balanceOf[to] += amount;
}
}
contract PocCR009Final is Test {
Snow snow;
MockWETH9 weth;
uint256 constant PRECISION = 1e18;
function setUp() public {
weth = new MockWETH9();
snow = new Snow(address(weth), 1, address(this));
}
function test_StoredFeeInflated() public view {
uint256 stored = snow.s_buyFee();
assertEq(stored, 1e18, "stored fee 1e18x too large");
}
function test_BuyRequires1ETHInsteadOf1Wei() public {
address buyer = makeAddr("buyer");
vm.deal(buyer, 2 ether);
vm.prank(buyer);
snow.buySnow{value: 1 ether}(1);
assertEq(snow.balanceOf(buyer), 1);
vm.prank(buyer);
vm.expectRevert();
snow.buySnow{value: 1 wei}(1);
}
function test_RealisticFeeExceedsTotalSupply() public {
MockWETH9 w2 = new MockWETH9();
Snow snow2 = new Snow(address(w2), 1e16, address(this));
uint256 stored = snow2.s_buyFee();
assertEq(stored, 1e34);
assertTrue(stored > 120_000_000 ether, "fee exceeds total ETH supply");
}
function test_WETHPathEquallyBroken() public {
address buyer = makeAddr("wethBuyer");
weth.mint(buyer, 1e18);
vm.prank(buyer);
snow.buySnow(1);
assertEq(snow.balanceOf(buyer), 1);
}
function test_ZeroFeeConstructorReverts() public {
MockWETH9 w3 = new MockWETH9();
vm.expectRevert(Snow.S__ZeroValue.selector);
new Snow(address(w3), 0, address(this));
}
function test_InflationQuantification() public view {
uint256 actualCostPerToken = snow.s_buyFee();
assertEq(actualCostPerToken / 1, 1e18);
}
}

Recommended Mitigation

// Fix option 1: store raw fee
- s_buyFee = _buyFee * PRECISION;
+ s_buyFee = _buyFee;

Or, if the intent is to accept integer fee values and convert them to 18-decimal form, add a compensating division in buySnow():

// Fix option 2: divide out the extra PRECISION
- if (msg.value == (s_buyFee * amount)) {
+ if (msg.value == (s_buyFee * amount / PRECISION)) {

Updates

Lead Judging Commences

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