AirDropper

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

Missing Signature Verification — Anyone Can Claim on Behalf of Any Eligible Address

[CRITICAL-2] Missing Signature Verification — Anyone Can Claim on Behalf of Any Eligible Address

File: src/MerkleAirdrop.sol, function claim() (line 31)

Summary

The claim function accepts an arbitrary account parameter without requiring that msg.sender == account or that account has signed the transaction. This allows any third party to claim tokens on behalf of eligible users, stealing their airdrop into the account address while the third party pays the fee.

Vulnerability Details

function claim(address account, uint256 amount, bytes32[] calldata merkleProof) external payable {

The Merkle leaf is constructed from (account, amount):

bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(account, amount))));

Since Merkle proofs are publicly visible on-chain (once any transaction is submitted), anyone who observes a valid (account, amount, proof) tuple from the mempool or past transactions can immediately replay it. Because account receives the tokens regardless of msg.sender, the attack doesn't steal tokens in flight — it doesn't need to — it simply claims tokens for a user who hasn't claimed yet, potentially at an unwanted time (e.g. before a tax event, or when the user intended to remain unclaimed for protocol-specific reasons).

More critically, combined with CRITICAL-1 (no re-claim guard), a sophisticated attacker could also front-run the legitimate user's own claim attempt and claim on their behalf before them, then claim again themselves if not patched.

Even without CRITICAL-1, forcing claims upon unconsenting users violates intent, could have tax implications, or could be weaponized if protocol rewards depend on claim timing.

PoC

// Bob (not eligible) sees Alice's pending claim tx in mempool
// Bob submits the same call with higher gas, front-running Alice
airdrop.claim{value: FEE}(alice, amountAlice, aliceProof);
// Tokens are sent to alice — but without her consent, at Bob's chosen time
// Bob paid FEE to force a claim Alice may not have wanted

Impact

  • Claim timing is fully controllable by any external actor.

  • Combined with CRITICAL-1, a griefing/drain scenario is amplified.

  • Users cannot choose when to receive airdrop tokens.

  • Medium–High depending on protocol context and token unlock/tax rules.

Tools Used

  • Manual analysis

  • Call graph tracing

Recommendations

Implement EIP-712 signature verification so the eligible address must sign their own claim:

+ using ECDSA for bytes32;
+
+ struct AirdropClaim {
+ address account;
+ uint256 amount;
+ }
+
+ bytes32 private constant MESSAGE_TYPEHASH = keccak256("AirdropClaim(address account,uint256 amount)");
+
function claim(
address account,
uint256 amount,
bytes32[] calldata merkleProof,
+ uint8 v, bytes32 r, bytes32 s
) external payable {
if (msg.value != FEE) revert MerkleAirdrop__InvalidFeeAmount();
+ if (s_hasClaimed[account]) revert MerkleAirdrop__AlreadyClaimed();
+
+ // Verify EIP-712 signature from account
+ bytes32 digest = _hashTypedDataV4(
+ keccak256(abi.encode(MESSAGE_TYPEHASH, account, amount))
+ );
+ address signer = ECDSA.recover(digest, v, r, s);
+ if (signer != account) revert MerkleAirdrop__InvalidSignature();
+
bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(account, amount))));
if (!MerkleProof.verify(merkleProof, i_merkleRoot, leaf)) revert MerkleAirdrop__InvalidProof();
+ s_hasClaimed[account] = true;
emit Claimed(account, amount);
i_airdropToken.safeTransfer(account, amount);
}

Inheriting from EIP712 (OpenZeppelin) is recommended.

Updates

Lead Judging Commences

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