Snowman Merkle Airdrop

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

`Snow::earnSnow` and `Snow::collectFee` modify state but never emit their declared `SnowEarned` and `FeeCollected` events

Root + Impact

Description

SnowEarned and FeeCollected are declared as events in Snow.sol explicitly to log token earning and fee collection activity for off-chain indexers, subgraphs, and monitoring tools.

Neither event is ever emitted anywhere in the contract. earnSnow() mints tokens and updates s_earnTimer with no event log, and collectFee() transfers WETH and ETH with no event log. Off-chain tools cannot distinguish freely earned tokens from purchased tokens, and fee collection operations are invisible to monitoring infrastructure.

// Snow.sol
@> event SnowEarned(address indexed earner, uint256 indexed amount); // Declared — NEVER emitted
@> event FeeCollected(); // Declared — NEVER emitted
function earnSnow() external canFarmSnow {
...
_mint(msg.sender, 1);
s_earnTimer = block.timestamp;
@> // emit SnowEarned(msg.sender, 1); — missing
}
function collectFee() external onlyCollector {
...
require(collected, "Fee collection failed!!!");
@> // emit FeeCollected(); — missing
}

Risk

Likelihood:

  • Every call to earnSnow() and collectFee() produces no event log for the duration of the protocol's operation

Impact:

  • Off-chain indexers and subgraphs cannot track free Snow earning separately from purchases, making protocol analytics incomplete

  • Protocol monitoring tools receive no alert when fees are collected, making treasury management invisible to operators

Proof of Concept

The test calls earnSnow from Alice's address and records every log emitted during the transaction via vm.recordLogs(). It then iterates over the returned Vm.Log array searching for any entry whose first topic matches keccak256("SnowEarned(address,uint256)") — the selector of the declared event. The loop completes without finding a match, and assertFalse(found) confirms the event was never emitted despite the mint and timer update executing successfully. The same pattern can be applied to collectFee to confirm that FeeCollected is equally absent from its transaction logs.

To run: forge test --match-test test_EarnSnowEmitsNoEvent -vvvv

function test_EarnSnowEmitsNoEvent() public {
vm.recordLogs();
vm.prank(alice);
snow.earnSnow();
Vm.Log[] memory logs = vm.getRecordedLogs();
bytes32 snowEarnedSig = keccak256("SnowEarned(address,uint256)");
bool found = false;
for (uint256 i = 0; i < logs.length; i++) {
if (logs[i].topics[0] == snowEarnedSig) found = true;
}
assertFalse(found); // Confirmed: SnowEarned declared but never emitted
}

Recommended Mitigation

Both missing emits are trivial one-line additions, but they restore the protocol's full observability for off-chain tools. For earnSnow, add emit SnowEarned(msg.sender, 1) at the end of the function body, after the timer is updated, so the event is only emitted on a successful earn.

For collectFee, add emit FeeCollected() after the ETH transfer is confirmed successful, so the event marks the completion of the entire collection operation rather than just the WETH portion.

function earnSnow() external canFarmSnow {
if (s_earnTimer != 0 && block.timestamp < (s_earnTimer + 1 weeks)) {
revert S__Timer();
}
_mint(msg.sender, 1);
s_earnTimer = block.timestamp;
+ emit SnowEarned(msg.sender, 1);
}
function collectFee() external onlyCollector {
uint256 collection = i_weth.balanceOf(address(this));
i_weth.transfer(s_collector, collection);
(bool collected,) = payable(s_collector).call{value: address(this).balance}("");
require(collected, "Fee collection failed!!!");
+ emit FeeCollected();
}
Updates

Lead Judging Commences

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