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.
@> 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
collectorOne
has enough Ether to cover 3 claim transactions.
collectorOne
calls claim
function 3 times with the same parameters.
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);
}