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

Any of Four Airdrop Recievers Can Empty the Contract

[H-2] Any of Four Airdrop Recievers Can Empty the Contract

Description: The contract doesnt use a mapping to store if a user has claimed the airdrop as a result any of the four users can claim until the contract is empty.

Impact: This will cause other users to loose airdroped money!

Proof of Concept: Add this test to existing test suit.

function testAnyOfWinnersCanGetTheWholeMoney() public {
uint256 startingBalance = token.balanceOf(collectorOne);
vm.deal(collectorOne, 5 * airdrop.getFee());
vm.startPrank(collectorOne);
//call the claim 4 times
while (token.balanceOf(address(airdrop)) > 0) {
airdrop.claim{value: airdrop.getFee()}(
collectorOne,
amountToCollect,
proof
);
}
vm.stopPrank();
uint256 endingBalance = token.balanceOf(collectorOne);
assertEq(endingBalance - startingBalance, 4 * amountToCollect);
}

Recommended Mitigation: One simple way to fix this is to add a claimed mapping and update it when someone claims their airdrop.

contract MerkleAirdrop is Ownable {
.
.
.
+ error MerkleAirdrop__AlreadyClaimed();
+ mapping(address => bool) private claimed;
.
.
.
function claim(
address account,
uint256 amount,
bytes32[] calldata merkleProof
) external payable {
if (msg.value != FEE) {
revert MerkleAirdrop__InvalidFeeAmount();
}
+ if (claimed[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);
+ claimed[account] = true;
i_airdropToken.safeTransfer(account, amount);
}
.
.
.
}

Or you can use the method used by uniswap and add an Index to merkle hashing alghorithm and then store the index in a mapping:

Uniswap Example Contract:
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity =0.8.17;
import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {MerkleProof} from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
import {IMerkleDistributor} from "./interfaces/IMerkleDistributor.sol";
error AlreadyClaimed();
error InvalidProof();
contract MerkleDistributor is IMerkleDistributor {
using SafeERC20 for IERC20;
address public immutable override token;
bytes32 public immutable override merkleRoot;
// This is a packed array of booleans.
mapping(uint256 => uint256) private claimedBitMap;
constructor(address token_, bytes32 merkleRoot_) {
token = token_;
merkleRoot = merkleRoot_;
}
function isClaimed(uint256 index) public view override returns (bool) {
uint256 claimedWordIndex = index / 256;
uint256 claimedBitIndex = index % 256;
uint256 claimedWord = claimedBitMap[claimedWordIndex];
uint256 mask = (1 << claimedBitIndex);
return claimedWord & mask == mask;
}
function _setClaimed(uint256 index) private {
uint256 claimedWordIndex = index / 256;
uint256 claimedBitIndex = index % 256;
claimedBitMap[claimedWordIndex] = claimedBitMap[claimedWordIndex] | (1 << claimedBitIndex);
}
function claim(uint256 index, address account, uint256 amount, bytes32[] calldata merkleProof)
public
virtual
override
{
if (isClaimed(index)) revert AlreadyClaimed();
// Verify the merkle proof.
bytes32 node = keccak256(abi.encodePacked(index, account, amount));
if (!MerkleProof.verify(merkleProof, merkleRoot, node)) revert InvalidProof();
// Mark it claimed and send the token.
_setClaimed(index);
IERC20(token).safeTransfer(account, amount);
emit Claimed(index, account, amount);
}
}
Updates

Lead Judging Commences

inallhonesty Lead Judge about 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.