AirdropDistributor

0x79b73f360f84a5edc239df65df6eb181abe6036f

Verification
Verified
v0.8.24+commit.e11b9ed9
Type
Contract
3,583 bytes
ABI entries
32
10 read · 4 write
License
none

Contract information

Address
0x79b73f360f84a5edc239df65df6eb181abe6036f
Chain
Robinhood Chain (4663)
Compiler
v0.8.24+commit.e11b9ed9
Optimization
Enabled
Creation tx
0x6a6b276a44…92b5b1adc4

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (10)

MAX_BATCH_SIZE()uint256
batchCount()uint256
campaignHash()bytes32
distributedAmount()uint256
distributedRecipients()uint256
owner()address
paused()bool
processed(address)bool
processedStatuses(address[])bool[]
token()address

Events (4)

BatchAirdroppedOwnershipTransferredPauseChangedTokenRecovered

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "tokenAddress",
        "type": "address"
      },
      {
        "internalType": "bytes32",
        "name": "manifestHash",
        "type": "bytes32"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      }
    ],
    "name": "AlreadyProcessed",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "supplied",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "maximum",
        "type": "uint256"
      }
    ],
    "name": "BatchTooLarge",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "EmptyBatch",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "available",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "required",
        "type": "uint256"
      }
    ],
    "name": "InsufficientTokenBalance",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidAddress",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidAmount",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "LengthMismatch",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NativeCurrencyNotAccepted",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotOwner",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "Paused",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ReentrantCall",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "TokenTransferFailed",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "batchNumber",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "recipientCount",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "totalAmount",
        "type": "uint256"
      }
    ],
    "name": "BatchAirdropped",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "previousOwner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "OwnershipTransferred",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "bool",
        "name": "paused",
        "type": "bool"
      }
    ],
    "name": "PauseChanged",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "TokenRecovered",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "MAX_BATCH_SIZE",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address[]",
        "name": "recipients",
        "type": "address[]"
      },
      {
        "internalType": "uint256[]",
        "name": "amounts",
        "type": "uint256[]"
      }
    ],
    "name": "airdrop",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "batchCount",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "campaignHash",
    "outputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "distributedAmount",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "distributedRecipients",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "paused",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "processed",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address[]",
        "name": "recipients",
        "type": "address[]"
      }
    ],
    "name": "processedStatuses",
    "outputs": [
      {
        "internalType": "bool[]",
        "name": "statuses",
        "type": "bool[]"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "tokenAddress",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "recoverToken",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bool",
        "name": "shouldPause",
        "type": "bool"
      }
    ],
    "name": "setPaused",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "token",
    "outputs": [
      {
        "internalType": "contract IERC20AirdropAsset",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "transferOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "stateMutability": "payable",
    "type": "receive"
  }
]

Source code

// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

interface IERC20AirdropAsset {
    function balanceOf(address account) external view returns (uint256);
    function transfer(address recipient, uint256 amount) external returns (bool);
}

/// @title Airdrop Distributor
/// @notice A transparent, non-upgradeable batch distributor for variable ERC-20 allocations.
/// @dev The campaign hash commits to the off-chain snapshot and every recipient allocation.
contract AirdropDistributor {
    error AlreadyProcessed(address recipient);
    error BatchTooLarge(uint256 supplied, uint256 maximum);
    error EmptyBatch();
    error InsufficientTokenBalance(uint256 available, uint256 required);
    error InvalidAddress();
    error InvalidAmount();
    error LengthMismatch();
    error NativeCurrencyNotAccepted();
    error NotOwner();
    error Paused();
    error ReentrantCall();
    error TokenTransferFailed(address token, address recipient, uint256 amount);

    event BatchAirdropped(uint256 indexed batchNumber, uint256 recipientCount, uint256 totalAmount);
    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
    event PauseChanged(bool paused);
    event TokenRecovered(address indexed token, address indexed recipient, uint256 amount);

    uint256 public constant MAX_BATCH_SIZE = 150;

    IERC20AirdropAsset public immutable token;
    bytes32 public immutable campaignHash;
    address public owner;
    bool public paused;
    uint256 public batchCount;
    uint256 public distributedRecipients;
    uint256 public distributedAmount;
    mapping(address => bool) public processed;

    uint256 private _entered = 1;

    modifier onlyOwner() {
        if (msg.sender != owner) revert NotOwner();
        _;
    }

    modifier nonReentrant() {
        if (_entered != 1) revert ReentrantCall();
        _entered = 2;
        _;
        _entered = 1;
    }

    constructor(address tokenAddress, bytes32 manifestHash) {
        if (tokenAddress == address(0) || tokenAddress.code.length == 0) revert InvalidAddress();
        token = IERC20AirdropAsset(tokenAddress);
        campaignHash = manifestHash;
        owner = msg.sender;
        emit OwnershipTransferred(address(0), msg.sender);
    }

    /// @notice Sends each recipient the corresponding amount committed in the campaign plan.
    function airdrop(address[] calldata recipients, uint256[] calldata amounts) external onlyOwner nonReentrant {
        if (paused) revert Paused();
        uint256 length = recipients.length;
        if (length == 0) revert EmptyBatch();
        if (length != amounts.length) revert LengthMismatch();
        if (length > MAX_BATCH_SIZE) revert BatchTooLarge(length, MAX_BATCH_SIZE);

        uint256 required;
        for (uint256 i; i < length; ) {
            if (recipients[i] == address(0) || recipients[i] == address(this)) revert InvalidAddress();
            if (amounts[i] == 0) revert InvalidAmount();
            required += amounts[i];
            unchecked {
                ++i;
            }
        }

        uint256 available = token.balanceOf(address(this));
        if (available < required) revert InsufficientTokenBalance(available, required);

        for (uint256 i; i < length; ) {
            address recipient = recipients[i];
            if (processed[recipient]) revert AlreadyProcessed(recipient);
            processed[recipient] = true;
            _safeTransfer(address(token), recipient, amounts[i]);
            unchecked {
                ++i;
            }
        }

        unchecked {
            ++batchCount;
            distributedRecipients += length;
            distributedAmount += required;
        }

        emit BatchAirdropped(batchCount, length, required);
    }

    function processedStatuses(address[] calldata recipients) external view returns (bool[] memory statuses) {
        uint256 length = recipients.length;
        statuses = new bool[](length);
        for (uint256 i; i < length; ) {
            statuses[i] = processed[recipients[i]];
            unchecked {
                ++i;
            }
        }
    }

    function setPaused(bool shouldPause) external onlyOwner {
        paused = shouldPause;
        emit PauseChanged(shouldPause);
    }

    function transferOwnership(address newOwner) external onlyOwner {
        if (newOwner == address(0)) revert InvalidAddress();
        address previousOwner = owner;
        owner = newOwner;
        emit OwnershipTransferred(previousOwner, newOwner);
    }

    function recoverToken(address tokenAddress, address recipient, uint256 amount) external onlyOwner nonReentrant {
        if (tokenAddress == address(0) || recipient == address(0)) revert InvalidAddress();
        if (amount == 0) revert InvalidAmount();
        _safeTransfer(tokenAddress, recipient, amount);
        emit TokenRecovered(tokenAddress, recipient, amount);
    }

    function _safeTransfer(address tokenAddress, address recipient, uint256 amount) private {
        (bool success, bytes memory returnData) = tokenAddress.call(
            abi.encodeWithSelector(IERC20AirdropAsset.transfer.selector, recipient, amount)
        );
        if (!success || (returnData.length != 0 && !abi.decode(returnData, (bool)))) {
            revert TokenTransferFailed(tokenAddress, recipient, amount);
        }
    }

    receive() external payable {
        revert NativeCurrencyNotAccepted();
    }
}
Chain explorer3527msChain node74ms