AirDropper

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

MerkleAirdrop declares a MerkleRootUpdated event that is never emitted because i_merkleRoot is immutable and has no setter

Root + Impact

Normal behavior

Events are declared to signal meaningful state changes to off-chain listeners. A MerkleRootUpdated event implies the merkle root can be changed after deployment — an important feature for long-running airdrop contracts.

Description

The issue

i_merkleRoot is declared immutable and assigned only in the constructor. There is no setMerkleRoot or equivalent function. The MerkleRootUpdated event is declared but never emitted anywhere in the contract and cannot ever be emitted because the state it describes — a root update — is architecturally impossible.

// @> Declared but never used — i_merkleRoot is immutable, cannot change
event MerkleRootUpdated(bytes32 newMerkleRoot);
constructor(bytes32 merkleRoot, IERC20 airdropToken) Ownable(msg.sender) {
// @> Root set once, never again
i_merkleRoot = merkleRoot;
i_airdropToken = airdropToken;
}
// @> No setMerkleRoot function exists anywhere in the contract

Risk

Likelihood:

  • This is present in every deployment of the contract as written — it is structural dead code

Impact:

  • Off-chain indexers, subgraphs, or front-ends that listen for MerkleRootUpdated events will never receive one, potentially causing silent integration failures or incorrect "no updates" assumptions

  • The event's presence falsely implies that the root can be updated — auditors and integrators must spend time confirming that no update path exists, adding unnecessary audit surface

  • Dead code increases bytecode size marginally and reduces overall code clarity

Proof of Concept

// Demonstrates that MerkleRootUpdated is unreachable
function test_MerkleRootUpdatedNeverEmits() public {
// Record all logs emitted during a claim
vm.deal(collectorOne, airdrop.getFee());
vm.recordLogs();
vm.prank(collectorOne);
airdrop.claim{ value: airdrop.getFee() }(collectorOne, amountToCollect, proof);
Vm.Log[] memory logs = vm.getRecordedLogs();
// MerkleRootUpdated event topic
bytes32 merkleRootUpdatedTopic = keccak256("MerkleRootUpdated(bytes32)");
for (uint256 i = 0; i < logs.length; i++) {
// This assertion should never fail — the event is never emitted
assertTrue(logs[i].topics[0] != merkleRootUpdatedTopic,
"MerkleRootUpdated was unexpectedly emitted");
}
// Confirm root is still the original — no update mechanism exists
assertEq(airdrop.getMerkleRoot(), merkleRoot);
}

Recommended Mitigation

Either remove the dead event declaration, or implement the update functionality it implies (with appropriate access control):

Option A — remove the dead event:

event Claimed(address account, uint256 amount);
- event MerkleRootUpdated(bytes32 newMerkleRoot);

Option B — implement the feature the event implies (if updatability is desired):

+ error MerkleAirdrop__RootAlreadySet();
- bytes32 private immutable i_merkleRoot;
+ bytes32 private s_merkleRoot;
+ function setMerkleRoot(bytes32 newMerkleRoot) external onlyOwner {
+ s_merkleRoot = newMerkleRoot;
+ emit MerkleRootUpdated(newMerkleRoot);
+ }

Option A is recommended for this codebase since the airdrop is designed to be a one-shot deployment.

Updates

Lead Judging Commences

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