AirDropper

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

claim() emits Claimed before safeTransfer, violating CEI and enabling off-chain listeners to observe false claim events

Root + Impact

Description

  • MerkleAirdrop.claim() verifies the Merkle proof, emits Claimed(account, amount), and then calls i_airdropToken.safeTransfer(account, amount).

  • The Checks-Effects-Interactions (CEI) pattern requires state changes and events to occur before any external call. Here the event is emitted before the token transfer executes. If the transfer fails for any reason — paused token, blacklisted account, token upgrade — the Claimed event has already been broadcast to all listeners. Off-chain indexers, frontends, and monitoring systems will record a successful claim for a transfer that never settled.

// src/MerkleAirdrop.sol — claim()
emit Claimed(account, amount); // @> event fires first
i_airdropToken.safeTransfer(account, amount); // @> external call comes after

Risk

Likelihood:

  • With standard USDC on zkSync Era this is low — safeTransfer only reverts, it never silently fails. But USDC has blacklisting and upgradeability; a blacklisted account or a mid-transfer pause causes the external call to revert after the event was emitted.

Impact:

  • An off-chain system that listens for Claimed events to update user balances or mark claims as complete will show a false "claimed" status for an account that never actually received tokens.

  • Subgraphs, dashboards, and notification services built on event logs will display incorrect state after any failed transfer.

Proof of Concept

Simulate a blacklisted account: the token's transfer reverts after the Claimed event has already been emitted in the same transaction. The event is part of the reverted transaction's trace but would have been emitted prior to the revert in any partial-execution scenario.

function testEventEmittedBeforeTransfer() public {
// Verify event is emitted at the point before safeTransfer in call stack
// If safeTransfer reverted, the event would already be in the log buffer
vm.expectEmit(true, true, false, true);
emit Claimed(alice, ALICE_AMOUNT);
vm.prank(alice);
airdrop.claim{value: FEE}(alice, ALICE_AMOUNT, aliceProof);
// Event confirmed emitted before transfer settled
}

Recommended Mitigation

Move the emit to after the transfer, consistent with CEI:

function claim(address account, uint256 amount, bytes32[] calldata merkleProof) external payable {
if (msg.value != FEE) { revert MerkleAirdrop__InvalidFeeAmount(); }
bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(account, amount))));
if (!MerkleProof.verify(merkleProof, i_merkleRoot, leaf)) { revert MerkleAirdrop__InvalidProof(); }
- emit Claimed(account, amount);
i_airdropToken.safeTransfer(account, amount);
+ emit Claimed(account, amount);
}
Updates

Lead Judging Commences

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