AirDropper

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

[M-1] Missing authorization check allows anyone to claim tokens on behalf of other users

Missing msg.sender == account Check Allows Unauthorized Claims for Arbitrary Users

Description

  • The contract is designed to distribute tokens to specific addresses that are part of the Merkle tree, with the expectation that users will claim their own tokens when they choose to do so.

  • The function accepts an account parameter but never validates that msg.sender == account, allowing anyone who obtains a valid Merkle proof to claim tokens for any other address without permission.

// src/MerkleAirdrop.sol
function claim(address account, uint256 amount, bytes32[] calldata merkleProof) external payable {
@> // Missing: No check that msg.sender == 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); // Anyone can trigger this for any account
}

Risk

Likelihood:

  • The account parameter being separate from msg.sender creates an obvious attack vector that any observer can exploit

  • All necessary claim data (proofs, amounts) becomes publicly visible on-chain through transaction data and emitted events, making it trivial to claim on behalf of others

Impact:

  • Users lose control over when they claim their tokens, which can have significant financial and tax consequences depending on jurisdiction and token price fluctuations at claim time

  • The msg.sender pays the fee while another address receives the tokens, enabling griefing attacks where malicious actors force unwanted claims on victims

Proof of Concept

The following test demonstrates how any address can claim tokens on behalf of another user without their permission:

function test_AnyoneCanClaimForOthers() public {
// Alice is entitled to 100 tokens
address alice = makeAddr("alice");
uint256 aliceAmount = 100e18;
// Setup Alice's valid proof in the Merkle tree
bytes32[] memory leaves = new bytes32[](1);
leaves[0] = keccak256(bytes.concat(keccak256(abi.encode(alice, aliceAmount))));
bytes32[] memory aliceProof = merkle.getProof(leaves, 0);
// Bob (attacker) finds Alice's proof from on-chain data
address bob = makeAddr("bob");
vm.deal(bob, 1 ether);
// Bob claims for Alice without her permission
vm.prank(bob);
airdrop.claim{value: FEE}(alice, aliceAmount, aliceProof);
// Alice receives tokens even though she didn't initiate the claim
assertEq(token.balanceOf(alice), aliceAmount);
// Bob paid the fee but Alice got the tokens - griefing attack
assertEq(bob.balance, 1 ether - FEE);
}

Explanation: Bob successfully claims tokens for Alice without her consent. The contract never validates that msg.sender == account, allowing anyone to force claims on behalf of others. This removes user autonomy over claim timing and enables griefing attacks.

Recommended Mitigation

Add an authorization check to ensure only the intended recipient can claim their tokens:

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

Explanation: Adding msg.sender != account check ensures only the rightful owner can execute their claim. This preserves user autonomy and prevents griefing attacks. The check is placed early to fail fast and save gas on unauthorized attempts.

Updates

Lead Judging Commences

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