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

Malicious winner can claim multiple times

Summary

The MerkleAirdrop::claim function allows 4 lucky addresses to claim 25 USDC. But this functions doesn't check if a given address had already claimed. Therefore a malicious address can claim multiple times 25 USDC until the balance of the protocol is drained.

Vulnerability Details

The MerkleAirdrop::claim function lacks a mechanism to track whether a user has already claimed his/her USDC. This permits malicious user from the list with lucky addresses to repeatedly invoke the claim function with a valid proof and to withdraw more tokens than entitled.

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();
}
emit Claimed(account, amount);
i_airdropToken.safeTransfer(account, amount);
}

Moreover if the other lucky addresses from the list have not claimed yet, the malicious user can claim the whole balance of the protocol and the other addresses will not be able to receive their USDC due to unsufficient balance of the protocol.

Impact

Let's consider the following test scenario. The collectorOne calls 4 times the claim function. The collectorOne pays 4 times the fee and receives 4 times from the claim function the amountToCollect (25 USDC). The balance of the protocol is 4 * 25 USDC. Therefore, after the fourth call of the claim function, there will be no more USDC in the protocol.
The testUsersCanClaimMoreThanOnce function shows the described test scenario. You can execute this test through the foundry command: `foundry test --match-test "testUsersCanClaimMoreThanOnce" -vvvvv".

function testUsersCanClaimMoreThanOnce() public {
uint256 startingBalance = token.balanceOf(collectorOne);
for (uint256 i = 0; i < 4; i++) {
vm.deal(collectorOne, airdrop.getFee());
vm.startPrank(collectorOne);
airdrop.claim{ value: airdrop.getFee() }(collectorOne, amountToCollect, proof);
vm.stopPrank();
}
uint256 endingBalance = token.balanceOf(collectorOne);
assertEq(endingBalance - startingBalance, 4 * amountToCollect);
console.log(endingBalance);
}

Tools Used

Manual Review, Foundry

Recommendations

Add a mapping that tracks if the user is already claimed:

+ mapping(address => bool) private claimed;
function claim(address account, uint256 amount, bytes32[] calldata merkleProof) external payable {
if (msg.value != FEE) {
revert MerkleAirdrop__InvalidFeeAmount();
}
+ require(!claimed[account], "You are already claimed");
bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(account, amount))));
if (!MerkleProof.verify(merkleProof, i_merkleRoot, leaf)) {
revert MerkleAirdrop__InvalidProof();
}
+ claimed[account] = true;
emit Claimed(account, amount);
i_airdropToken.safeTransfer(account, amount);
}
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.