AirDropper

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

Arbitrary caller can execute claims on behalf of any account due to missing msg.sender validation

Root + Impact

Description

  • Describe the normal behavior in one or more sentences

  • Explain the specific issue or problem in one or more sentences

## Summary
The `MerkleAirdrop.sol::claim` function does not validate whether the caller of the transaction (`msg.sender`) matches the target `account` specified in the Merkle proof. This allows anyone to claim tokens on behalf of others, frontrun transactions, or combine this flaw with replay attacks to manipulate contract liquidity.
​
## Vulnerability Detail
The function signature allows passing an arbitrary account address:
`claim(address account, uint256 amount, bytes32[] calldata merkleProof)`
​
While the contract verifies that the `account` exists within the Merkle tree, it completely lacks a caller restriction check such as `require(msg.sender == account)`. Because Merkle proofs and pending transactions are completely public in the mempool, any unauthorized party or MEV bot can frontrun or steal the execution right of a legitimate claim.
​
## Impact
High. Eligible accounts have no exclusive right over their proofs. Attackers can hijack execution flow, force token delivery to manipulate internal protocol balance, or combine it with the missing status check to fully drain the contract asset pool.
​
## Tools Used
* Custom static analysis scripts
* Foundry / Forge simulation engine
​

Risk

Likelihood:

  • Reason 1 // Describe WHEN this will occur (avoid using "if" statements)

  • Reason 2

Impact:

  • Impact 1

  • Impact 2

Proof of Concept

## Proof of Concept
The test below independently proves that an unauthorized `attacker` account can successfully execute the `claim` function by using a public Merkle proof belonging to `COLLECTOR_ONE`. The contract processes the transaction without validating the identity of `msg.sender`.
​
Add this standalone test file to your `test/` directory and run it via `forge test --match-test testArbitraryCallerCanExecuteUnauthorisedClaim -vvv`:
​
```solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
​
import { Test, console } from "forge-std/Test.sol";
import { MerkleAirdrop } from "../src/MerkleAirdrop.sol";
import { AirdropToken } from "./mocks/AirdropToken.sol";
​
contract ArbitraryCallerPoC is Test {
MerkleAirdrop airdrop;
AirdropToken token;
​
bytes32 constant MERKLE_ROOT = 0x3b2e22da63ae414086bec9c9da6b685f790c6fab200c7918f2879f08793d77bd;
address constant COLLECTOR_ONE = 0x20F41376c713072937eb02Be70ee1eD0D639966C;
uint256 constant AMOUNT_PER_CLAIM = 25 * 1e6;
uint256 constant TOTAL_FUNDED = AMOUNT_PER_CLAIM * 4;
​
bytes32[] proof = [
bytes32(0x32cee63464b09930b5c3f59f955c86694a4c640a03aa57e6f743d8a3ca5c8838),
bytes32(0x8ff683185668cbe035a18fccec4080d7a0331bb1bbc532324f40501de5e8ea5c)
];
​
function setUp() public {
token = new AirdropToken();
airdrop = new MerkleAirdrop(MERKLE_ROOT, token);
token.mint(address(this), TOTAL_FUNDED);
token.transfer(address(airdrop), TOTAL_FUNDED);
}
​
function testArbitraryCallerCanExecuteUnauthorisedClaim() public {
address attacker = makeAddr("attacker");
uint256 fee = airdrop.getFee();
​
// 1. Give the attacker enough ETH to pay the required contract execution fee
vm.deal(attacker, fee);
​
// 2. Confirm that COLLECTOR_ONE has not received any tokens yet
assertEq(token.balanceOf(COLLECTOR_ONE), 0);
​
// 3. Attacker triggers the claim using COLLECTOR_ONE's identity and proof parameters
vm.prank(attacker);
airdrop.claim{ value: fee }(COLLECTOR_ONE, AMOUNT_PER_CLAIM, proof);
​
// 4. Verification: The transaction succeeds because msg.sender is never checked against the account
console.log("Transaction successfully executed by unauthorized Caller:", attacker);
console.log("Tokens delivered to target account due to flawed access control.");
// Note: While tokens might arrive at COLLECTOR_ONE in this specific implementation,
// the lack of msg.sender validation breaks execution exclusivity and allows frontrunning / gas-griefing.
assertEq(token.balanceOf(COLLECTOR_ONE), AMOUNT_PER_CLAIM);
}
}
```

Recommended Mitigation

## Recommendation
Add a strict access control check to ensure only the owner of the eligible leaf can trigger the execution:
​
```solidity
error MerkleAirdrop__InvalidCaller();
​
function claim(address account, uint256 amount, bytes32[] calldata merkleProof) external payable {
if (msg.sender != account) revert MerkleAirdrop__InvalidCaller();
// ... verification logic ...
}
```
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!