Snowman Merkle Airdrop

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

[H-2] Negligible Snow Token Amount Minted by `earnSnow` Function Renders Feature Ineffective

[H-2] Negligible Snow Token Amount Minted by earnSnow Function Renders Feature Ineffective

Description

  • Normal Behavior: The earnSnow() function in the Snow.sol contract is intended to allow users to receive a predefined quantity of Snow tokens for free, once per week, during the FARMING_DURATION. This mechanism is designed to encourage user engagement and provide a no-cost entry point to acquiring Snow tokens.

  • Specific Issue: The earnSnow() function calls _mint(msg.sender, 1). Given that the Snow token uses a PRECISION of 10**18 (standard for 18 decimal ERC20 tokens), this mints only 1 wei of the Snow token (i.e., 1 / 10^18 of a full Snow token). This amount is infinitesimally small and practically valueless, making the "earn for free" feature ineffective and misleading to users. A similar issue exists in the buySnow() function where the amount parameter is also treated as wei instead of full tokens.

function earnSnow() external canFarmSnow {
if (s_earnTimer != 0 && block.timestamp < (s_earnTimer + 1 weeks)) {
revert S__Timer();
}
// @> VULNERABILITY: Mints only 1 wei (10^-18 of a full token)
_mint(msg.sender, 1);
s_earnTimer = block.timestamp;
// Emitting SnowEarned event with amount=1 (wei) is also misleading
emit SnowEarned(msg.sender, 1);
}
}

Risk

Likelihood: High

  • A user calls the earnSnow() function after the 1-week cooldown period and during the FARMING_DURATION.

  • A user calls the buySnow() function, intending to purchase a specific number of full Snow tokens.

Impact: Medium

  • Ineffective Token Distribution Mechanism: The earnSnow() feature, designed as a way to distribute tokens and engage users, fails to achieve its purpose due to the negligible amount minted.

  • Potential Loss of Funds (for buySnow): While the earnSnow issue is about not receiving value, the similar issue in buySnow means users pay the full fee for X tokens but receive X wei of tokens, which is a direct financial loss relative to their expectation. If s_buyFee is significant, this becomes a more severe loss.

  • Airdrop Eligibility Issues: If Snow token balances are critical for eligibility in the SnowmanAirdrop (e.g., needing at least 1 full Snow token), users relying on earnSnow() or buySnow() (with its current bug) will never accumulate enough tokens to participate, despite their actions.

Proof of Concept

The following Foundry test, testEarnSnowMintsNegligibleAmount from TestSnow.t.sol, demonstrates that calling earnSnow() results in the user's balance increasing by only 1 wei, not 1 full token.

// TestSnow.t.sol
contract TestSnow is Test {
Snow snow;
//add this
uint256 internal constant PRECISION = 10 ** 18; // For clarity in test
function testEarnSnowMintsNegligibleAmount() public {
vm.prank(ashley);
snow.earnSnow();
// Assert that Ashley received 1 wei (the negligible amount)
assertEq(snow.balanceOf(ashley), 1, "Ashley should have 1 wei of Snow token after first earnSnow call");
// Assert that Ashley did NOT receive 1 full token (1 * 10^18 wei)
assertNotEq(snow.balanceOf(ashley), 1 * PRECISION, "Ashley should NOT have 1 full Snow token (1e18 wei)");
console2.log("Ashley's balance after first earnSnow (in wei):", snow.balanceOf(ashley));
vm.warp(block.timestamp + 1 weeks + 1 seconds);
vm.prank(ashley);
snow.earnSnow();
assertEq(snow.balanceOf(ashley), 2, "Ashley should have 2 wei of Snow token after second earnSnow call");
assertNotEq(snow.balanceOf(ashley), 2 * PRECISION, "Ashley should NOT have 2 full Snow tokens (2e18 wei)");
console2.log("Ashley's balance after second earnSnow (in wei):", snow.balanceOf(ashley));
}
}

The test output will show Ashley's balance after first earnSnow (in wei): 1.

Recommended Mitigation

Modify the _mint calls within earnSnow() and buySnow() to use the PRECISION constant to ensure the correct number of full tokens (or their wei equivalent) is minted.

For earnSnow(), if the intention is to mint 1 full Snow token:

// Snow.sol
function earnSnow() external canFarmSnow {
if (s_earnTimer != 0 && block.timestamp < (s_earnTimer + 1 weeks)) {
revert S__Timer();
}
- _mint(msg.sender, 1);
+ _mint(msg.sender, 1 * PRECISION); // Mint 1 full Snow token
s_earnTimer = block.timestamp;
- emit SnowEarned(msg.sender, 1);
+ emit SnowEarned(msg.sender, 1 * PRECISION); // Emit amount in wei
}

For buySnow(), assuming amount parameter is the number of full tokens the user wants to buy:

function buySnow(uint256 amount) external payable canFarmSnow {
//...
- _mint(msg.sender, amount);
+ _mint(msg.sender, amount * PRECISION);
} else { // Paying with WETH
i_weth.safeTransferFrom(msg.sender, address(this), totalFeeInWei);
- _mint(msg.sender, amount);
+ _mint(msg.sender, amount * PRECISION);
}
s_earnTimer = block.timestamp;
- emit SnowBought(msg.sender, amount);
+ emit SnowBought(msg.sender, amount * PRECISION); // Emit amount in wei
}

Note on buySnow mitigation: The buySnow logic for handling ETH vs WETH and exact payment amounts needs careful consideration beyond just fixing the _mint call. The diff above simplifies the payment check for ETH; a more robust implementation would clearly differentiate payment paths and handle msg.value appropriately. The core fix for this specific vulnerability is amount * PRECISION. The s_buyFee should also be clearly defined as the price per full token.

Updates

Lead Judging Commences

yeahchibyke Lead Judge
3 months ago
yeahchibyke Lead Judge 2 months ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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