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

Lack of check for already claimed accounts in `MerkleAirdrop::claim` allows anyone to drain the contract of USDC token funds

Vulnerability Details

Description:
The claim function in the smart contract does not implement a mechanism to prevent accounts that have already claimed tokens from claiming again. This allows accounts to repeatedly claim tokens, potentially draining the contract of its token funds.

Impact:
If one person claims all the tokens, other winners will not be able to claim any. This not only disadvantages the other winners but also undermines the fairness and integrity of the token distribution process.

Proof of Concept:
An attacker can repeatedly call the claim function with the same account address and amount, exploiting the lack of a check for already claimed accounts. This can be demonstrated by observing the contract's token balance decrease over time as the same account repeatedly claims tokens.

Add the following test to MerkleAirdropTest.t.sol and run forge test --zksync --mt testNoCheckForAlreadyClaimedAccounts.

Proof of Code
function testNoCheckForAlreadyClaimedAccounts() public {
// deal attacker enough ether for multiple claims
vm.deal((collectorOne), airdrop.getFee() * 4);
// call claim until the airdrop contract is drained of its token funds
vm.startPrank(collectorOne);
while (token.balanceOf(address(airdrop)) > 0) {
airdrop.claim{ value: airdrop.getFee() }(collectorOne, amountToCollect, proof);
}
vm.stopPrank();
// check if token balance of the airdrop contract is equal to 0
assertEq(token.balanceOf(address(airdrop)), 0);
}

Recommended Mitigation:
Implement a check within the claim function to ensure that an account cannot claim tokens more than once. This can be achieved by maintaining a mapping of claimed accounts and checking against this mapping before allowing a token transfer.

Updated Code
+ error AlreadyClaimed();
+ mapping(address => bool) public 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();
}
+ if (claimed[account]) {
+ revert AlreadyClaimed();
+ }
+ claimed[account] = true;
emit Claimed(account, amount);
i_airdropToken.safeTransfer(account, amount);
}

Tools Used: Manual Review and Foundry for POC

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.