Snowman Merkle Airdrop

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

Decimal scaling error in buySnow requires users to pay 1e36 WETH to purchase a single full Snow token

Root + Impact

Description

  • Users should be able to purchase Snow tokens using WETH or native ETH. The cost should scale proportionally to the amount of tokens they wish to buy based on the s_buyFee (e.g., if the fee is 1 WETH per token, buying 1 full token should cost 1 WETH).

  • The contract incorrectly multiplies a standard ERC-20 amount (which inherently has 18 decimals) by s_buyFee (which is also scaled to 18 decimals in the constructor). This double-multiplication results in an astronomical required payment of 1e36 wei to purchase a single full token, making it mathematically and practically impossible for users to buy tokens.

constructor(address _weth, uint256 _buyFee, address _collector) ERC20("Snow", "S") Ownable(msg.sender) {
// ...
s_buyFee = _buyFee * PRECISION; // @> s_buyFee is scaled to 10**18 (e.g., 1e18)
// ...
}
function buySnow(uint256 amount) external payable canFarmSnow {
// @> BUG: If amount is 1 full token (1e18), the required value is 1e18 * 1e18 = 1e36.
if (msg.value == (s_buyFee * amount)) {
_mint(msg.sender, amount);
} else {
// @> BUG: Attempts to pull 1e36 WETH from the user
i_weth.safeTransferFrom(msg.sender, address(this), (s_buyFee * amount));
_mint(msg.sender, amount);
}
}

Risk

Likelihood:

  • Occurs 100% of the time a user attempts to purchase a standard, full unit of the Snow token (1e18 wei).

Impact:

  • Complete failure of the purchasing mechanic.

  • Users are either completely blocked from participating due to insufficient funds (revert), or if they attempt to buy fractions of a token, they are egregiously overcharged (paying 1 full ETH for 1 single wei of a token).

Proof of Concept

Narrative Setup: The following Foundry test proves the scaling error. It simulates a user attempting to buy exactly one full Snow token (1e18). We mint the user a massive amount of WETH (1,000,000 WETH) to prove that even a whale cannot afford a single token. The test expects the WETH transferFrom to revert because the required cost evaluates to 1e36 (One quintillion WETH).

Execution Steps:

  1. We simulate a wealthy user (Alice) with 1,000,000 WETH.

  2. Alice approves the Snow contract to spend her WETH.

  3. Alice attempts to buy 1 full Snow token (1 ether / 1e18 wei).

  4. The transaction reverts due to an arithmetic/balance error because the contract tries to pull 1e36 wei.

How to run this test: Place the following code inside the protocol's existing TestSnow.t.sol file and execute: forge test --match-test test_POC_Precision -vvv

function test_POC_PrecisionErrorOverchargesUsers() public {
// 1. Setup Alice with a massive amount of WETH (1 Million WETH)
address wealthyAlice = makeAddr("wealthyAlice");
uint256 oneFullToken = 1 ether; // 1e18 wei
uint256 massiveWethBalance = 1_000_000 ether;
deal(address(weth), wealthyAlice, massiveWethBalance);
// 2. Alice tries to buy 1 full Snow token.
vm.startPrank(wealthyAlice);
weth.approve(address(snow), type(uint256).max);
// 3. The transaction reverts because 1e18 * 1e18 = 1e36 required WETH.
// Even with 1 million WETH, she cannot afford 1 single token.
vm.expectRevert();
snow.buySnow(oneFullToken);
vm.stopPrank();
console2.log("Alice's WETH Balance:", weth.balanceOf(wealthyAlice));
console2.log("Required WETH for 1 Token:", FEE * oneFullToken);
console2.log("BUG: The required cost is 1e36, which is impossible to pay.");
}

Recommended Mitigation

Architectural Fix: To fix the double-multiplication, the contract must divide the total calculated cost by the PRECISION constant (which is 1e18). This normalizes the decimal places and ensures that buying 1e18 tokens correctly costs the s_buyFee amount.

This fix maintains the precision required for fractional token purchases without astronomically overcharging the user.

function buySnow(uint256 amount) external payable canFarmSnow {
- if (msg.value == (s_buyFee * amount)) {
+ uint256 totalCost = (s_buyFee * amount) / PRECISION;
+ if (msg.value == totalCost) {
_mint(msg.sender, amount);
} else {
- i_weth.safeTransferFrom(msg.sender, address(this), (s_buyFee * amount));
+ i_weth.safeTransferFrom(msg.sender, address(this), totalCost);
_mint(msg.sender, amount);
}
s_earnTimer = block.timestamp;
}
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!