Snowman Merkle Airdrop

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

`Snow::earnSnow` can be front-run, allowing attackers to steal rewards from legitimate users.

Snow::earnSnow can be front-run, allowing attackers to steal rewards from legitimate users.

Description

The Snow::earnSnow function is a public function with no access control that allows anyone to call it once 1 week has passed since the last snow minting. This creates a front-running vulnerability where malicious actors can monitor the mempool and front-run legitimate users' transactions to claim the reward for themselves.

function earnSnow() external canFarmSnow {
if (s_earnTimer != 0 && block.timestamp < (s_earnTimer + 1 weeks)) {
revert S__Timer();
}
@> No Limitation
_mint(msg.sender, 1);
s_earnTimer = block.timestamp;
}

Risk

Likelihood:

The function mints 1 snow token to msg.sender without verifying that the caller is entitled to the reward. Any user can call this function and claim the reward once the 1-week timer expires.

Impact:

  • Attackers can front-run legitimate users who attempt to call earnSnow() and steal the 1 snow token reward

  • MEV bots can monitor the mempool for earnSnow transactions and submit higher gas price transactions to claim rewards first

  • Legitimate users lose their earned rewards, leading to unfair token distribution

  • This creates a gas war scenario where only those willing to pay the highest gas fees can claim rewards

  • The intended reward mechanism is completely broken as rewards go to front-runners rather than protocol participants

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Test, console2} from "forge-std/Test.sol";
import {Snow} from "../src/Snow.sol";
import {MockWETH} from "../src/mock/MockWETH.sol";
contract TestSnowFrontRunning is Test {
Snow snow;
MockWETH weth;
address alice;
address attacker;
address collector;
function setUp() public {
alice = makeAddr("alice");
attacker = makeAddr("attacker");
collector = makeAddr("collector");
weth = new MockWETH();
snow = new Snow(address(weth), 1 ether, collector);
// Alice buys snow to start the timer
vm.deal(alice, 10 ether);
vm.prank(alice);
snow.buySnow{value: 1 ether}(1);
}
function testFrontRunningEarnSnow() public {
// Initial state: Alice has 1 snow from buying
assertEq(snow.balanceOf(alice), 1);
assertEq(snow.balanceOf(attacker), 0);
// Fast forward 1 week
vm.warp(block.timestamp + 1 weeks);
// Alice tries to call earnSnow (this would be in the mempool)
// But attacker sees this transaction and front-runs it with higher gas
// Attacker front-runs Alice's transaction
vm.prank(attacker);
snow.earnSnow();
// Attacker successfully stole the reward
assertEq(snow.balanceOf(attacker), 1);
// Alice's transaction would now revert because timer was reset
vm.prank(alice);
vm.expectRevert(Snow.S__Timer.selector);
snow.earnSnow();
// Alice gets nothing, attacker stole the reward
assertEq(snow.balanceOf(alice), 1); // Still only has initial purchase
console2.log("Attacker successfully front-ran and stole the reward!");
}
function testContinuousFrontRunning() public {
// Demonstrates that attacker can repeatedly front-run
vm.deal(alice, 10 ether);
// Week 1
vm.warp(block.timestamp + 1 weeks);
vm.prank(attacker);
snow.earnSnow();
assertEq(snow.balanceOf(attacker), 1);
// Week 2
vm.warp(block.timestamp + 1 weeks);
vm.prank(attacker);
snow.earnSnow();
assertEq(snow.balanceOf(attacker), 2);
// Week 3
vm.warp(block.timestamp + 1 weeks);
vm.prank(attacker);
snow.earnSnow();
assertEq(snow.balanceOf(attacker), 3);
// Attacker can claim all rewards indefinitely
assertEq(snow.balanceOf(alice), 1); // Alice only has initial purchase
console2.log("Attacker claimed rewards for 3 consecutive weeks!");
}
}

Recommended Mitigation

There are several approaches to fix this issue:

  1. Implement a whitelist or access control: Only allow specific addresses (e.g., protocol participants, stakers) to call earnSnow().

  2. Distribute rewards proportionally: Instead of a first-come-first-served mechanism, distribute rewards to eligible users proportionally based on their snow holdings or participation.

  3. Use a commit-reveal scheme: Require users to commit to claiming rewards in advance, preventing front-running.

  4. Track eligible claimers: Maintain a list of addresses eligible to claim rewards based on their participation, and only allow those addresses to claim.

Example fix using access control:

mapping(address => bool) private s_eligibleClaimers;
function earnSnow() external canFarmSnow {
if (!s_eligibleClaimers[msg.sender]) {
revert S__NotAllowed();
}
if (s_earnTimer != 0 && block.timestamp < (s_earnTimer + 1 weeks)) {
revert S__Timer();
}
_mint(msg.sender, 1);
s_eligibleClaimers[msg.sender] = false; // Can only claim once
s_earnTimer = block.timestamp;
emit SnowEarned(msg.sender, 1);
}
function addEligibleClaimer(address claimer) external onlyOwner {
s_eligibleClaimers[claimer] = true;
}
Updates

Lead Judging Commences

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