Summary
Any caller can claim airdrop for any valid address that is part of Merkle tree.
Vulnerability Details
Function claim
has address account
parameter which is directly used in leaf creation. This means any caller can claim airdrop for any valid address that is part of Merkle tree. Caller will need to pay fee. Amount will be transferred to account
parameter which may not be desired behavior and can be malicious.
@> function claim(address account, uint256 amount, bytes32[] calldata merkleProof) external payable {
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);
}
Impact
Malicious caller could claim for valid address that is currently not expecting to receive any USDC.
zkSync Era comes with native account abstraction support for private-key controlled EOAs. So there is possibility that receiver has contract logic which could mishandle received USDC, so there is possibility that USDC will be lost permanently.
Proof of Concept
Random caller has enough Ether to cover fee.
Random caller calls claim
function with collectorOne
address and amount to claim.
collectorOne
receives intended amount.
Place the following test into MerkleAirdropTest.t.sol
.
function testClaimForOtherAddress() public {
uint256 startingBalance = token.balanceOf(collectorOne);
address randomCaller = address(452);
vm.deal(randomCaller, airdrop.getFee());
vm.startPrank(randomCaller);
airdrop.claim{value: airdrop.getFee()}(collectorOne, amountToCollect, proof);
vm.stopPrank();
uint256 endingBalance = token.balanceOf(collectorOne);
assertEq(endingBalance - startingBalance, amountToCollect);
}
Tools Used
Manual review
Recommendations
Suggestion is to not allow account
parameter in claim
function, but to use msg.sender
instead so caller can only claim for themselves.
- function claim(address account, uint256 amount, bytes32[] calldata merkleProof) external payable {
+ function claim(uint256 amount, bytes32[] calldata merkleProof) external payable {
if (msg.value != FEE) {
revert MerkleAirdrop__InvalidFeeAmount();
}
- bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(account, amount))));
+ bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(msg.sender, amount))));
if (!MerkleProof.verify(merkleProof, i_merkleRoot, leaf)) {
revert MerkleAirdrop__InvalidProof();
}
- emit Claimed(account, amount);
+ emit Claimed(msg.sender, amount);
- i_airdropToken.safeTransfer(account, amount);
+ i_airdropToken.safeTransfer(msg.sender, amount);
}