AirDropper

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

claim() takes account as a caller-supplied parameter, allowing anyone to trigger a claim on behalf of any eligible address

Root + Impact

Description

  • MerkleAirdrop.claim() is designed so that an eligible address calls it to receive their USDC allocation. The function requires the caller to pay the 1 gwei fee and provide a valid Merkle proof for the claimed account.

  • account is a caller-supplied parameter — msg.sender is never validated against it. Any third party can supply any eligible address as account, pay the 1 gwei fee themselves, and trigger the token transfer to that address. The recipient still receives their tokens, but they did not consent to or initiate the transaction.

// src/MerkleAirdrop.sol
function claim(address account, uint256 amount, bytes32[] calldata merkleProof) external payable {
if (msg.value != FEE) { revert MerkleAirdrop__InvalidFeeAmount(); }
// @> msg.sender is never checked against account
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); // @> tokens sent to account, not msg.sender
}

Risk

Likelihood:

  • Merkle proofs are typically published or derivable from the Merkle tree data (which must be public for users to generate their own proofs). Any observer who knows a valid (account, amount, proof) tuple can trigger the claim.

Impact:

  • Users who want precise control over when they receive tokens — for tax timing, wallet migration, or protocol interaction sequencing — have their claims frontrun and executed without consent.

  • A griefing actor can claim on behalf of all 4 eligible addresses, depositing USDC into wallets the recipients may not actively monitor, disrupting their intended workflows.

Proof of Concept

A third party (satoshi) calls claim() with Alice's address and proof before Alice does. Alice receives her tokens but did not control the timing.

function testAnyoneCanClaimForAccount() public {
address satoshi = makeAddr("satoshi");
vm.deal(satoshi, FEE);
uint256 aliceBalanceBefore = token.balanceOf(alice);
// Satoshi claims on behalf of alice — satoshi pays the fee
vm.prank(satoshi);
airdrop.claim{value: FEE}(alice, ALICE_AMOUNT, aliceProof);
// Alice received tokens without calling claim herself
assertEq(token.balanceOf(alice), aliceBalanceBefore + ALICE_AMOUNT);
}

Recommended Mitigation

Require msg.sender == account so only the eligible address can trigger its own claim:

function claim(address account, uint256 amount, bytes32[] calldata merkleProof) external payable {
if (msg.value != FEE) { revert MerkleAirdrop__InvalidFeeAmount(); }
+ if (msg.sender != account) { revert MerkleAirdrop__CallerNotAccount(); }
...
}

Alternatively, remove the account parameter entirely and use msg.sender:

- function claim(address account, uint256 amount, bytes32[] calldata merkleProof) external payable {
+ function claim(uint256 amount, bytes32[] calldata merkleProof) external payable {
+ address account = msg.sender;
...
}
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!