AirDropper

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

Possible front running on claim() function

Root + Impact

Description

  • The MerkleAirdrop contract is designed to allow eligible users to claim their allocated tokens by calling the claim() function with their address, amount, and Merkle proof, with the expectation that users initiate their own claims.

  • The claim() function accepts an account parameter that specifies the recipient address, allowing any third party to submit a claim transaction on behalf of any eligible user without their permission or signature, enabling front-running, griefing, and MEV extraction attacks.

// @> The 'account' parameter allows anyone to claim for any eligible address
// @> No validation that msg.sender == account
// @> No signature verification from the account holder
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);
// @> Tokens transferred to 'account' regardless of who called the function
i_airdropToken.safeTransfer(account, amount);
}

Risk

Likelihood:

  • MEV bots continuously monitor the mempool for profitable transactions and will detect pending claim transactions with public Merkle proofs

  • Attackers can trivially extract proof data from any pending claim transaction in the mempool and front-run it with higher gas prices

  • Users' Merkle proofs become publicly known as soon as they submit their first claim transaction, making them vulnerable to front-running

  • The economic incentive exists for bots to claim on behalf of users and charge relay fees through private mempools

Impact:

  • Users waste gas fees when their claim transactions revert after being front-run by bots or attackers

  • Griefing attacks where malicious actors force users to receive tokens at inopportune times, potentially creating tax implications or unwanted token exposure

  • MEV extraction where bot operators claim airdrops on behalf of users through private mempools and charge hidden fees

  • Phishing opportunities where attackers claim tokens for users first, then send fraudulent messages claiming the first airdrop was fake

Proof of Concept

function testAnyoneCanClaimForOthers() public {
// Setup: An attacker who is not eligible for the airdrop
address attacker = makeAddr("attacker");
vm.deal(attacker, airdrop.getFee());
uint256 collectorStartBalance = token.balanceOf(collectorOne);
// Attacker sees collectorOne's transaction in mempool with the proof
// Attacker front-runs by submitting same claim with higher gas price
vm.prank(attacker);
airdrop.claim{value: airdrop.getFee()}(collectorOne, amountToCollect, proof);
// CollectorOne received tokens but didn't initiate the claim
assertEq(token.balanceOf(collectorOne), collectorStartBalance + amountToCollect);
// Now when collectorOne's original transaction executes, it will revert
// (assuming double-claim prevention is added)
vm.deal(collectorOne, airdrop.getFee());
vm.prank(collectorOne);
// This would revert with MerkleAirdrop__AlreadyClaimed
// User wasted gas on the failed transaction
}
function testMEVBotGriefing() public {
// Scenario: MEV bot claims for all users before they can claim themselves
address mevBot = makeAddr("mevBot");
vm.deal(mevBot, airdrop.getFee() * 4);
address[] memory users = new address[](4);
users[0] = 0x20F41376c713072937eb02Be70ee1eD0D639966C;
users[1] = 0x277D26a45Add5775F21256159F089769892CEa5B;
users[2] = 0x0c8Ca207e27a1a8224D1b602bf856479b03319e7;
users[3] = 0xf6dBa02C01AF48Cf926579F77C9f874Ca640D91D;
vm.startPrank(mevBot);
// MEV bot claims for all users (with their proofs extracted from mempool)
for (uint i = 0; i < users.length; i++) {
// Bot would use each user's proof here
// airdrop.claim{value: airdrop.getFee()}(users[i], amountToCollect, proofs[i]);
}
vm.stopPrank();
// All users received tokens without their consent
// Users may have wanted to claim through a specific contract or at a different time
// Bot operator could charge fees through private relay services
}

Recommended Mitigation

Require msg.sender to be the recipient (Recommended for simplicity)

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

Lead Judging Commences

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