AirDropper

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

MerkleAirdrop::claimFees() emits no event on withdrawal, making fee collection undetectable without parsing raw transaction data

Description

  • The claimFees() function allows the owner to withdraw all accumulated ETH fees from the contract.

  • However, the function emits no event after a successful withdrawal. Every other state-changing action in the contract — token claims — emits a Claimed event. The absence of a corresponding event for fee withdrawals means that on-chain monitoring tools, indexers, and off-chain observers have no standard way to detect when fees are collected, how much was withdrawn, or how frequently the owner calls this function. The only way to detect a withdrawal is to parse raw call data or monitor ETH balance changes, both of which are significantly more complex and error-prone than event listening.

function claimFees() external onlyOwner {
// @> No event emitted before or after the withdrawal
// @> On-chain observers cannot detect this call via standard log filtering
(bool succ,) = payable(owner()).call{ value: address(this).balance }("");
if (!succ) {
revert MerkleAirdrop__TransferFailed();
}
}

Risk

Likelihood:

  • Every call to claimFees() produces no log entry — any monitoring system that relies on event logs to track contract activity silently misses every fee withdrawal for the lifetime of the contract

  • A protocol dashboard or subgraph built on top of this contract has no data source for fee withdrawal history, requiring a full transaction trace scan instead of a simple log query

Impact:

  • Fee withdrawal activity is invisible to standard on-chain transparency tools — users and auditors cannot verify how much ETH the owner has collected or reconstruct the withdrawal history without a full archive node trace

  • Absence of event logging is inconsistent with the rest of the contract's design, where Claimed is emitted for every token transfer — the missing event creates a blind spot specifically around the owner's privileged action

Proof of Concept

The missing event means that a fee withdrawal transaction leaves no trace in the contract's event log. Any listener subscribed to the contract's logs — such as a subgraph, a monitoring bot, or a front-end — receives no notification when the owner withdraws. The contrast with the claim() function makes the omission clear:

// claim() — state change is logged:
emit Claimed(account, amount); // ✅ observable on-chain
i_airdropToken.safeTransfer(account, amount);
// claimFees() — state change produces no log:
(bool succ,) = payable(owner()).call{ value: address(this).balance }(""); // ❌ no event
if (!succ) revert MerkleAirdrop__TransferFailed();
// To detect a claimFees() call, an observer must:
// 1. Monitor raw ETH balance changes on the contract address, OR
// 2. Parse transaction input data for the claimFees() selector (0x...)
// Both approaches are significantly more complex than:
// 3. Filter logs for FeesClaimed(address,uint256) — which does not exist

Recommended Mitigation

Declare a FeesClaimed event and emit it inside claimFees() before the ETH transfer. Capture the balance into a local variable first so the withdrawn amount is recorded in the event rather than relying on the caller to infer it from balance diffs. Replace payable(owner()) with payable(msg.sender) for consistency, since onlyOwner already guarantees the caller is the owner.

+ event FeesClaimed(address indexed owner, uint256 amount);
function claimFees() external onlyOwner {
+ uint256 amount = address(this).balance;
- (bool succ,) = payable(owner()).call{ value: address(this).balance }("");
+ (bool succ,) = payable(msg.sender).call{ value: amount }("");
if (!succ) {
revert MerkleAirdrop__TransferFailed();
}
+ emit FeesClaimed(msg.sender, amount);
}
Updates

Lead Judging Commences

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