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

User claims are not invalidated in `MerkleAirdrop::claim` function which leads to claiming multiple times with same parameters.

Summary

Each user has single amount allocated to them in Merkle tree, but user can claim same amount multiple times which is not intended.

Vulnerability Details

User has right to claim amount but MerkleAirdrop::claim function does not check if user has already claimed. Which means user can claim same amount multiple times with same parameters.

// @audit - there is no check if user has already claimed
@> 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);
}

Impact

Contract can be completely drained of funds by calling claim function multiple times with the same parameters.

Proof of Concept

  1. collectorOne has enough Ether to cover 3 claim transactions.

  2. collectorOne calls claim function 3 times with the same parameters.

  3. collectionOne acquires amount that is 3 times bigger than amount supposed to be collected.

Place the following test into MerkleAirdropTest.t.sol.

function testClaimMultipleTimes() public {
uint256 startingBalance = token.balanceOf(collectorOne);
vm.deal(collectorOne, 3 * airdrop.getFee());
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);
vm.stopPrank();
uint256 endingBalance = token.balanceOf(collectorOne);
assertEq(endingBalance - startingBalance, 3 * amountToCollect);
}

Tools Used

Manual review

Recommendations

error MerkleAirdrop__InvalidFeeAmount();
error MerkleAirdrop__InvalidProof();
error MerkleAirdrop__TransferFailed();
+ error MerkleAirdrop__AlreadyClaimed();
uint256 private constant FEE = 1e9;
IERC20 private immutable i_airdropToken;
bytes32 private immutable i_merkleRoot;
+ mapping(address user => bool claimed) public hasClaimed;
.
.
function claim(address account, uint256 amount, bytes32[] calldata merkleProof) external payable {
+ if (hasClaimed[account]) {
+ revert MerkleAirdrop__AlreadyClaimed();
+ }
+ hasClaimed[account] = true;
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);
}
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.