// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; interface IERC20 { function transferFrom(address from, address to, uint256 value) external returns (bool); } library SafeTransfer { function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal { (bool success, bytes memory data) = address(token).call( abi.encodeWithSelector(token.transferFrom.selector, from, to, value) ); require(success && (data.length == 0 || abi.decode(data, (bool))), "transfer failed"); } } contract Ownable { address public owner; event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); constructor(address initialOwner) { require(initialOwner != address(0), "owner is zero"); owner = initialOwner; emit OwnershipTransferred(address(0), initialOwner); } modifier onlyOwner() { require(msg.sender == owner, "not owner"); _; } function transferOwnership(address newOwner) external onlyOwner { require(newOwner != address(0), "owner is zero"); emit OwnershipTransferred(owner, newOwner); owner = newOwner; } } contract ReentrancyGuard { uint256 private constant NOT_ENTERED = 1; uint256 private constant ENTERED = 2; uint256 private status = NOT_ENTERED; modifier nonReentrant() { require(status != ENTERED, "reentrant call"); status = ENTERED; _; status = NOT_ENTERED; } } contract BulkAirdropWithFee is Ownable, ReentrancyGuard { using SafeTransfer for IERC20; IERC20 public serviceToken; address public feeRecipient; uint256 public pricePerRecipient; uint256 public maxRecipientsPerTx = 300; event Airdropped( address indexed sender, address indexed token, uint256 recipientCount, uint256 totalAmount, uint256 serviceFeePaid ); event PricingUpdated( address indexed token, uint256 pricePerRecipient, address indexed recipient ); event MaxRecipientsUpdated(uint256 maxRecipientsPerTx); constructor( address initialOwner, address initialServiceToken, uint256 initialPricePerRecipient, address initialFeeRecipient ) Ownable(initialOwner) { require(initialServiceToken != address(0), "service token is zero"); require(initialFeeRecipient != address(0), "fee recipient is zero"); require(initialPricePerRecipient > 0, "price is zero"); serviceToken = IERC20(initialServiceToken); pricePerRecipient = initialPricePerRecipient; feeRecipient = initialFeeRecipient; } function setPricing( address newServiceToken, uint256 newPricePerRecipient, address newFeeRecipient ) external onlyOwner { require(newServiceToken != address(0), "service token is zero"); require(newFeeRecipient != address(0), "fee recipient is zero"); require(newPricePerRecipient > 0, "price is zero"); serviceToken = IERC20(newServiceToken); pricePerRecipient = newPricePerRecipient; feeRecipient = newFeeRecipient; emit PricingUpdated(newServiceToken, newPricePerRecipient, newFeeRecipient); } function setMaxRecipientsPerTx(uint256 newMaxRecipientsPerTx) external onlyOwner { require(newMaxRecipientsPerTx > 0, "max is zero"); require(newMaxRecipientsPerTx <= 500, "max too high"); maxRecipientsPerTx = newMaxRecipientsPerTx; emit MaxRecipientsUpdated(newMaxRecipientsPerTx); } function quoteFee(uint256 recipientCount) public view returns (uint256) { return pricePerRecipient * recipientCount; } function airdropERC20( address token, address[] calldata recipients, uint256[] calldata amounts ) external nonReentrant { require(token != address(0), "token is zero"); require(recipients.length == amounts.length, "length mismatch"); require(recipients.length > 0, "empty recipients"); require(recipients.length <= maxRecipientsPerTx, "too many recipients"); uint256 serviceFeePaid = quoteFee(recipients.length); serviceToken.safeTransferFrom(msg.sender, feeRecipient, serviceFeePaid); IERC20 airdropToken = IERC20(token); uint256 totalAmount; for (uint256 i = 0; i < recipients.length; i++) { require(recipients[i] != address(0), "recipient is zero"); require(amounts[i] > 0, "amount is zero"); totalAmount += amounts[i]; airdropToken.safeTransferFrom(msg.sender, recipients[i], amounts[i]); } emit Airdropped(msg.sender, token, recipients.length, totalAmount, serviceFeePaid); } }