AirDropper

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

Anyone can claim tokens on behalf of eligible addresses without their consent

Root + Impact

Description

  • The claim() function accepts an arbitrary account address parameter with no verification that msg.sender == account or that the account holder has signed the transaction.

  • Any third party can trigger a claim for any eligible address.

// account is caller-supplied, never validated against msg.sender
function claim(address account, uint256 amount, bytes32[] calldata merkleProof) external payable {

Risk

Likelihood:

  • The Merkle proof for each address is derivable from the public makeMerkle.js script and on-chain Merkle root

  • Any attacker can front-run all 4 legitimate claimants at contract deployment

Impact:

  • Attacker forces all 4 claims before legitimate users can act, paying only 4 × 1e9 wei (negligible)

  • Once fixed with H-1's hasClaimed mapping, legitimate users are permanently locked out of ever claiming

  • Users lose all agency over when and whether to receive their airdrop

Proof of Concept

function testAttackerClaimsForVictim() public {
address attacker = makeAddr("attacker");
vm.deal(attacker, airdrop.getFee());
vm.startPrank(attacker);
// attacker uses victim's address and public proof data
airdrop.claim{value: airdrop.getFee()}(collectorOne, amountToCollect, proof);
vm.stopPrank();
// tokens sent to collectorOne but collectorOne never authorized this
assertEq(token.balanceOf(collectorOne), amountToCollect);
// if hasClaimed mapping added, collectorOne can never claim again
}

Recommended Mitigation

+ error MerkleAirdrop__InvalidSignature();
function claim(
address account,
uint256 amount,
bytes32[] calldata merkleProof,
+ uint8 v, bytes32 r, bytes32 s
) external payable {
if (msg.value != FEE) { revert MerkleAirdrop__InvalidFeeAmount(); }
+ // Verify account signed this claim
+ bytes32 hash = keccak256(abi.encodePacked(account, amount));
+ bytes32 ethHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash));
+ if (ecrecover(ethHash, v, r, s) != account) revert MerkleAirdrop__InvalidSignature();
bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(account, amount))));
if (!MerkleProof.verify(merkleProof, i_merkleRoot, leaf)) { revert MerkleAirdrop__InvalidProof(); }
i_airdropToken.safeTransfer(account, amount);
}
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!