Snowman Merkle Airdrop

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

earnSnow() Mints 1 Wei Instead of 1 Whole Token Due to Decimals Mismatch

Root + Impact

Description

The earnSnow() function is intended to reward users with 1 whole Snow token per week. However, it calls _mint(msg.sender, 1), which mints only 1 wei (10⁻¹⁸ of a token) because the contract uses the standard 18-decimal representation.

The buySnow() function correctly uses full precision (e.g., amount * 1e18 or a uint256 already in wei), confirming that the codebase operates in 18-decimal units. The earnSnow() path is inconsistent with this convention, indicating a missing * 1e18 multiplier rather than an intentional design choice.

// Root cause: mints 1 wei instead of 1e18 (1 whole token)
function earnSnow() external {
require(block.timestamp >= s_earnTimer[msg.sender] + 1 weeks, "too early");
s_earnTimer[msg.sender] = block.timestamp;
_mint(msg.sender, 1); // ← mints 1 wei, not 1 Snow
}
// Contrast: buySnow uses full precision
function buySnow() external payable {
uint256 amount = msg.value * 1e18; // correctly scaled
_mint(msg.sender, amount);
}

Risk

Likelihood:

  • Every single call to earnSnow() triggers this code path — there is no conditional or edge case required.

  • The discrepancy with buySnow()'s use of 1e18 scaling makes the omission clearly unintentional.

Impact:

  • Users receive 1 wei (≈ $0.00) instead of 1 whole Snow token from the free weekly claim, rendering the earnSnow() incentive economically meaningless.

  • If the token's value proposition or marketing promises "1 Snow per week free," this is a direct violation of stated user expectations and could constitute a misleading claim.

Proof of Concept

Calling exploit() demonstrates that earnSnow() credits exactly 1 wei to the caller's balance. The expected reward is 1e18 (one whole token at 18 decimals). The 18-order-of-magnitude gap confirms the missing multiplier.

+ contract PoC {
+ Snow private snow;
+
+ constructor(address target) {
+ snow = Snow(target);
+ }
+
+ function exploit() external {
+ uint256 balanceBefore = snow.balanceOf(address(this));
+
+ snow.earnSnow();
+
+ uint256 balanceAfter = snow.balanceOf(address(this));
+
+ // Expected: 1e18 (1 whole token)
+ // Actual: 1 (1 wei)
+ require(balanceAfter - balanceBefore == 1, "minted 1 wei, not 1 token");
+ }
+ }

Recommended Mitigation

Adding the 1e18 multiplier aligns earnSnow() with the 18-decimal convention used throughout the rest of the contract (as seen in buySnow()). This ensures users receive 1 whole Snow token as intended.

function earnSnow() external {
require(block.timestamp >= s_earnTimer[msg.sender] + 1 weeks, "too early");
s_earnTimer[msg.sender] = block.timestamp;
- _mint(msg.sender, 1);
+ _mint(msg.sender, 1e18);
}
Updates

Lead Judging Commences

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