AirDropper

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

`claim` does not require `msg.sender == account`, enabling third parties to trigger payouts and amplifying replay abuse

Description

  • The intended flow is that each eligible address claims its allocation. The function accepts an arbitrary account parameter and never checks that msg.sender is that account.

  • Any address can pay the ETH fee and call claim(victim, amount, proof), sending tokens to victim. Combined with missing claim tracking, any party with a copied proof can repeatedly trigger transfers to the leaf address.

function claim(address account, uint256 amount, bytes32[] calldata merkleProof) external payable {
// @> msg.sender is never compared to account
if (msg.value != FEE) {
revert MerkleAirdrop__InvalidFeeAmount();
}
// ...
i_airdropToken.safeTransfer(account, amount);
}

(Aderyn H-1 flags this as “ETH transferred without address checks” — tokens go to account, but authorization is absent.)

Risk

Likelihood:

  • A claim is included in a public block; the proof is trivially reusable by mempool searchers or bots.

  • A relayer model was not documented in the README; operators assume only the beneficiary can initiate claims.

Impact:

  • Attackers drain the pool by replaying proofs (see finding above) without needing to control the beneficiary key.

  • Frontrunners can execute the first claim for a victim (victim still receives tokens, but attacker controls timing and pays negligible fees).

  • Accounting and compliance assumptions (“user must call claim”) break when claims are forced by third parties.

Proof of Concept

function test_PoC_anyoneCanClaimForVictim() public {
address victim = 0x20F41376c713072937eb02Be70ee1eD0D639966C;
address attacker = makeAddr("attacker");
bytes32[] memory proof = /* valid proof for victim */;
vm.deal(attacker, airdrop.getFee());
vm.prank(attacker);
airdrop.claim{ value: airdrop.getFee() }(victim, 25 * 1e6, proof);
assertEq(token.balanceOf(victim), 25 * 1e6);
assertEq(token.balanceOf(attacker), 0);
}

Recommended Mitigation

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

Alternatively, document and implement an explicit allowedRelayer or EIP-712 meta-transaction path instead of open relaying.

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!