AirDropper

AI First Flight #5
Beginner FriendlyDeFiFoundry
EXP
View results
Submission Details
Impact: high
Likelihood: high
Invalid

Ghost Event — `MerkleRootUpdated` Declared but Never Emitted, Signals Missing Functionality

[HIGH-2] Ghost Event — MerkleRootUpdated Declared but Never Emitted, Signals Missing Functionality

File: src/MerkleAirdrop.sol (line 21)

Summary

The contract declares a MerkleRootUpdated(bytes32 newMerkleRoot) event but there is no function that emits it, and i_merkleRoot is immutable — it can never change. This orphaned event declaration reveals a design inconsistency: either the developer intended to allow Merkle root updates (which would be a critical admin privilege vulnerability), or it is dead code left from a template. In either scenario, it represents a logic error.

Vulnerability Details

// Line 21
event MerkleRootUpdated(bytes32 newMerkleRoot);

i_merkleRoot is declared immutable:

bytes32 private immutable i_merkleRoot;

Immutable variables in Solidity can only be set in the constructor and cannot be changed afterward. There is no setMerkleRoot() function anywhere. The event will never be emitted.

Why this matters from a security perspective:

  1. Suggests planned but unimplemented functionality. If there was a setMerkleRoot() function that was accidentally removed/forgotten, the contract is missing an intended admin capability — potentially preventing recovery from the CRITICAL-3 decimal mismatch.

  2. If a setMerkleRoot() function were added (e.g., in an upgrade), it would be a critical vulnerability: the owner could replace the Merkle root mid-airdrop, invalidate existing proofs, and create new ones pointing to attacker-controlled addresses, effectively stealing all remaining tokens.

  3. ABI/tooling confusion: Off-chain indexers and monitoring tools that track MerkleRootUpdated events will never trigger, creating false confidence that the root is stable.

PoC

# Static check: grep for any emission of MerkleRootUpdated
grep -r "MerkleRootUpdated" ./src/
# Output: only the declaration — never emitted
// If an admin function were naively added later:
function updateMerkleRoot(bytes32 newRoot) external onlyOwner {
i_merkleRoot = newRoot; // COMPILE ERROR — can't reassign immutable
emit MerkleRootUpdated(newRoot);
}
// This proves the event was either vestigial or intended for a mutable root design

Impact

  • Dead code creates confusion and audit surface.

  • Signals incomplete implementation.

  • If root mutability was the intent, the current immutable design blocks emergency recovery from CRITICAL-3.

  • Severity: Medium (code quality / potential design flaw)

Tools Used

  • Manual analysis

  • grep / AST analysis

Recommendations

- event MerkleRootUpdated(bytes32 newMerkleRoot);

Remove the unused event entirely. If root updates are needed (e.g., for emergency recovery), implement properly with time-locks and governance:

// Option: Make mutable with owner control + timelock
bytes32 private s_merkleRoot;
uint256 private s_rootUpdateTime;
uint256 private constant ROOT_UPDATE_DELAY = 2 days;
function proposeNewMerkleRoot(bytes32 newRoot) external onlyOwner {
s_pendingRoot = newRoot;
s_rootUpdateTime = block.timestamp + ROOT_UPDATE_DELAY;
emit MerkleRootUpdateProposed(newRoot, s_rootUpdateTime);
}
Updates

Lead Judging Commences

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