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

An allowed address can call the `MerkleAirdrop::claim` function more than once, receiving all the USDT, while other users receive none.

Summary

MerkleAirdrop::claim can be called by eligible users multiple times which leads to the non-uniform distribution of USDT.

Vulnerability Details

As the function logic states that there are four eligible users that can claim maximum of 25 USDT each. But there is no access check that they can call function only one time. hence one user can claim all available USDT by multiple MerkleAirdrop::claim calls.

function testUsersCanClaim() public {
// starting balance of collectorOne is )
uint256 startingBalance = token.balanceOf(collectorOne);
// collectorOne gets the 4e9 ETH
vm.deal(collectorOne, airdrop.getFee() * 4);
vm.startPrank(collectorOne);
// calls the claim function four times by depositing 1e9 ETH as fee everytime
for (uint i = 0; i < 4; i++) {
airdrop.claim{value: airdrop.getFee()}(
collectorOne,
amountToCollect,
proof
);
}
vm.stopPrank();
// ending balance is 100000000 = 2500000000 *4
uint256 endingBalance = token.balanceOf(collectorOne);
assertEq(endingBalance - startingBalance, amountToCollect * 4);
}

Impact

one user gets all the available USDT and rest gets zero. leads to the violation of function logic.

Tools Used

manual review , foundry

Recommendations

To fix this we can store if every collector had once claim the USDT by mapping(address => bool)

contract MerkleAirdrop is Ownable {
using SafeERC20 for IERC20;
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 isClaimed;
event Claimed(address account, uint256 amount);
event MerkleRootUpdated(bytes32 newMerkleRoot);
function claim(
address account,
uint256 amount,
bytes32[] calldata merkleProof
) external payable {
if (msg.value != FEE) {
revert MerkleAirdrop__InvalidFeeAmount();
}
+ if (isClaimed[account]) {
+ revert MerkleAirdrop__AlreadyClaimed();
+ }
bytes32 leaf = keccak256(
bytes.concat(keccak256(abi.encode(account, amount)))
);
if (!MerkleProof.verify(merkleProof, i_merkleRoot, leaf)) {
revert MerkleAirdrop__InvalidProof();
}
emit Claimed(account, amount);
+ isClaimed[account] = true;
i_airdropToken.safeTransfer(account, amount);
}
function claimFees() external onlyOwner {
(bool succ, ) = payable(owner()).call{value: address(this).balance}("");
if (!succ) {
revert MerkleAirdrop__TransferFailed();
}
}
}
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.