AirDropper

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

Missing msg.sender Validation in claim() — Front-Running Griefing and Fee Theft

Missing msg.sender Validation in claim() — Front-Running Griefing and Fee Theft

Description

Vulnerable Contract: src/MerkleAirdrop.sol (line 30, function claim())

The MerkleAirdrop.claim() function accepts an account parameter that specifies the recipient of the airdrop tokens. The function does not verify that msg.sender == account, allowing any caller who possesses a valid Merkle proof to trigger a claim for any eligible address.

While tokens are correctly sent to account (not the caller), this enables front-running attacks where an attacker monitors the mempool, extracts claim parameters, and submits a competing transaction with higher gas.

function claim(address account, uint256 amount, bytes32[] calldata merkleProof) external payable {
@> // No check: require(msg.sender == account)
@> // Any caller can submit a claim for any `account`
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);
}

Risk

Likelihood: Medium

  • Requires mempool monitoring or prior knowledge of valid Merkle proofs. Proofs eventually become public via Claimed events.

Impact: Low

  • Tokens always reach the intended recipient. No direct fund loss occurs. The attack griefs legitimate claimers by wasting their gas on reverted transactions (if hasClaimed guard from H01 is added).

Severity: Low

Proof of Concept

An address not included in the Merkle tree successfully calls claim() with collectorOne's address and valid proof. The tokens go to collectorOne, not the attacker, confirming that the access control check is missing.

function test_SEED03_claimOnBehalf() public {
uint256 fee = airdrop.getFee();
vm.deal(attacker, fee); // attacker is NOT in the Merkle tree
vm.prank(attacker);
airdrop.claim{value: fee}(collectorOne, amountToCollect, proof);
// Tokens go to collectorOne, not attacker
assertEq(token.balanceOf(collectorOne), amountToCollect);
assertEq(token.balanceOf(attacker), 0);
}
// Result: [PASS] (gas: 85764) — untrusted caller submitted claim

Recommended Mitigation

Validate that the caller is the intended recipient, or implement EIP-712 signature verification for gasless relay support.

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