HoodIndexDistributor
0xb9dff91e7ca167925c6b9b96ea97394df0d317c6
Verification
Verified
v0.8.24+commit.e11b9ed9
Type
Contract
5,677 bytes
ABI entries
45
9 read · 11 write
License
mit
Contract information
- Address
- 0xb9dff91e7ca167925c6b9b96ea97394df0d317c6
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.24+commit.e11b9ed9
- Optimization
- Enabled
- Creator
- 0x21EEAc5199…BD3fdd7A18
- Creation tx
- 0x35ba8e7445…325bfb301e
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (9)
MAX_BATCH_SIZE() → uint256
allowedTokens(address) → bool
approvedUntil(bytes32) → uint64
computeBatchId(address, address[], uint256[], bytes32, uint64) → bytes32
executedBatches(bytes32) → bool
operators(address) → bool
owner() → address
paused() → bool
pendingOwner() → address
Events (10)
BatchApprovedBatchDistributedBatchRevokedNativeRescuedOperatorSetOwnershipTransferStartedOwnershipTransferredPausedTokenAllowedTokenRescued
ABI
[
{
"inputs": [
{
"internalType": "address",
"name": "initialOwner",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "AlreadyExecuted",
"type": "error"
},
{
"inputs": [],
"name": "ApprovalExpired",
"type": "error"
},
{
"inputs": [],
"name": "BatchNotApproved",
"type": "error"
},
{
"inputs": [],
"name": "InexactTransfer",
"type": "error"
},
{
"inputs": [],
"name": "InvalidAddress",
"type": "error"
},
{
"inputs": [],
"name": "InvalidBatch",
"type": "error"
},
{
"inputs": [],
"name": "InvalidExpiry",
"type": "error"
},
{
"inputs": [],
"name": "NotAuthorized",
"type": "error"
},
{
"inputs": [],
"name": "NotOwner",
"type": "error"
},
{
"inputs": [],
"name": "ReentrantCall",
"type": "error"
},
{
"inputs": [],
"name": "TokenNotAllowed",
"type": "error"
},
{
"inputs": [],
"name": "TokenTransferFailed",
"type": "error"
},
{
"inputs": [],
"name": "TransfersPaused",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "bytes32",
"name": "batchId",
"type": "bytes32"
},
{
"indexed": false,
"internalType": "uint64",
"name": "validUntil",
"type": "uint64"
}
],
"name": "BatchApproved",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "bytes32",
"name": "batchId",
"type": "bytes32"
},
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "recipientCount",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "totalAmount",
"type": "uint256"
}
],
"name": "BatchDistributed",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "bytes32",
"name": "batchId",
"type": "bytes32"
}
],
"name": "BatchRevoked",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "recipient",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "NativeRescued",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "operator",
"type": "address"
},
{
"indexed": false,
"internalType": "bool",
"name": "allowed",
"type": "bool"
}
],
"name": "OperatorSet",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "pendingOwner",
"type": "address"
}
],
"name": "OwnershipTransferStarted",
"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": "Paused",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": false,
"internalType": "bool",
"name": "allowed",
"type": "bool"
}
],
"name": "TokenAllowed",
"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": "TokenRescued",
"type": "event"
},
{
"inputs": [],
"name": "MAX_BATCH_SIZE",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "acceptOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "allowedTokens",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "batchId",
"type": "bytes32"
},
{
"internalType": "uint64",
"name": "validUntil",
"type": "uint64"
}
],
"name": "approveBatch",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32[]",
"name": "batchIds",
"type": "bytes32[]"
},
{
"internalType": "uint64",
"name": "validUntil",
"type": "uint64"
}
],
"name": "approveBatches",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"name": "approvedUntil",
"outputs": [
{
"internalType": "uint64",
"name": "",
"type": "uint64"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "address[]",
"name": "recipients",
"type": "address[]"
},
{
"internalType": "uint256[]",
"name": "amounts",
"type": "uint256[]"
},
{
"internalType": "bytes32",
"name": "salt",
"type": "bytes32"
},
{
"internalType": "uint64",
"name": "validUntil",
"type": "uint64"
}
],
"name": "batchTransfer",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "address[]",
"name": "recipients",
"type": "address[]"
},
{
"internalType": "uint256[]",
"name": "amounts",
"type": "uint256[]"
},
{
"internalType": "bytes32",
"name": "salt",
"type": "bytes32"
},
{
"internalType": "uint64",
"name": "validUntil",
"type": "uint64"
}
],
"name": "computeBatchId",
"outputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"name": "executedBatches",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "operators",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"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": [],
"name": "pendingOwner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address payable",
"name": "recipient",
"type": "address"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "rescueNative",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "address",
"name": "recipient",
"type": "address"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "rescueToken",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "batchId",
"type": "bytes32"
}
],
"name": "revokeBatch",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "operator",
"type": "address"
},
{
"internalType": "bool",
"name": "allowed",
"type": "bool"
}
],
"name": "setOperator",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bool",
"name": "value",
"type": "bool"
}
],
"name": "setPaused",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "bool",
"name": "allowed",
"type": "bool"
}
],
"name": "setTokenAllowed",
"outputs": [],
"stateMutability": "nonpayable",
"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 IERC20 {
function balanceOf(address account) external view returns (uint256);
function transfer(address recipient, uint256 amount) external returns (bool);
}
/**
* @notice Minimal, token-agnostic batch distributor for Hood Index epochs.
* @dev The backend computes payouts off-chain and submits gas-safe chunks.
* The owner approves an exact, expiring batch commitment before an
* operator can execute it.
*/
contract HoodIndexDistributor {
uint256 public constant MAX_BATCH_SIZE = 200;
address public owner;
address public pendingOwner;
bool public paused;
uint256 private entered;
mapping(address => bool) public operators;
mapping(address => bool) public allowedTokens;
mapping(bytes32 => uint64) public approvedUntil;
mapping(bytes32 => bool) public executedBatches;
event BatchDistributed(
bytes32 indexed batchId, address indexed token, uint256 recipientCount, uint256 totalAmount
);
event BatchApproved(bytes32 indexed batchId, uint64 validUntil);
event BatchRevoked(bytes32 indexed batchId);
event OperatorSet(address indexed operator, bool allowed);
event TokenAllowed(address indexed token, bool allowed);
event OwnershipTransferStarted(address indexed owner, address indexed pendingOwner);
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
event Paused(bool paused);
event TokenRescued(address indexed token, address indexed recipient, uint256 amount);
event NativeRescued(address indexed recipient, uint256 amount);
error AlreadyExecuted();
error ApprovalExpired();
error BatchNotApproved();
error InexactTransfer();
error InvalidAddress();
error InvalidBatch();
error InvalidExpiry();
error NotAuthorized();
error NotOwner();
error ReentrantCall();
error TokenTransferFailed();
error TokenNotAllowed();
error TransfersPaused();
constructor(address initialOwner) {
if (initialOwner == address(0)) revert InvalidAddress();
owner = initialOwner;
paused = true;
emit OwnershipTransferred(address(0), initialOwner);
emit Paused(true);
}
modifier onlyOwner() {
if (msg.sender != owner) revert NotOwner();
_;
}
modifier onlyOperator() {
if (msg.sender != owner && !operators[msg.sender]) revert NotAuthorized();
_;
}
modifier nonReentrant() {
if (entered != 0) revert ReentrantCall();
entered = 1;
_;
entered = 0;
}
function setOperator(address operator, bool allowed) external onlyOwner {
if (operator == address(0)) revert InvalidAddress();
operators[operator] = allowed;
emit OperatorSet(operator, allowed);
}
function setTokenAllowed(address token, bool allowed) external onlyOwner {
if (token == address(0) || (allowed && token.code.length == 0)) {
revert InvalidAddress();
}
allowedTokens[token] = allowed;
emit TokenAllowed(token, allowed);
}
function setPaused(bool value) external onlyOwner {
paused = value;
emit Paused(value);
}
function transferOwnership(address newOwner) external onlyOwner {
if (newOwner == address(0)) revert InvalidAddress();
pendingOwner = newOwner;
emit OwnershipTransferStarted(owner, newOwner);
}
function acceptOwnership() external {
if (msg.sender != pendingOwner) revert NotOwner();
address previousOwner = owner;
owner = msg.sender;
pendingOwner = address(0);
emit OwnershipTransferred(previousOwner, msg.sender);
}
function computeBatchId(
address token,
address[] calldata recipients,
uint256[] calldata amounts,
bytes32 salt,
uint64 validUntil
) public view returns (bytes32) {
return keccak256(
abi.encode(block.chainid, address(this), token, salt, validUntil, recipients, amounts)
);
}
function approveBatch(bytes32 batchId, uint64 validUntil) external onlyOwner {
_approveBatch(batchId, validUntil);
}
function approveBatches(bytes32[] calldata batchIds, uint64 validUntil) external onlyOwner {
if (batchIds.length == 0 || batchIds.length > MAX_BATCH_SIZE) {
revert InvalidBatch();
}
for (uint256 index = 0; index < batchIds.length; ++index) {
_approveBatch(batchIds[index], validUntil);
}
}
function revokeBatch(bytes32 batchId) external onlyOwner {
if (batchId == bytes32(0)) revert InvalidBatch();
delete approvedUntil[batchId];
emit BatchRevoked(batchId);
}
function batchTransfer(
address token,
address[] calldata recipients,
uint256[] calldata amounts,
bytes32 salt,
uint64 validUntil
) external onlyOperator nonReentrant {
if (paused) revert TransfersPaused();
if (token == address(0)) revert InvalidAddress();
if (salt == bytes32(0)) revert InvalidBatch();
if (!allowedTokens[token]) revert TokenNotAllowed();
if (
recipients.length == 0 || recipients.length > MAX_BATCH_SIZE
|| recipients.length != amounts.length
) revert InvalidBatch();
bytes32 batchId = computeBatchId(token, recipients, amounts, salt, validUntil);
if (executedBatches[batchId]) revert AlreadyExecuted();
uint64 approvalExpiry = approvedUntil[batchId];
if (approvalExpiry == 0 || approvalExpiry != validUntil) revert BatchNotApproved();
if (block.timestamp > approvalExpiry) revert ApprovalExpired();
delete approvedUntil[batchId];
executedBatches[batchId] = true;
uint256 totalAmount;
for (uint256 index = 0; index < recipients.length; ++index) {
if (recipients[index] == address(0) || amounts[index] == 0) {
revert InvalidBatch();
}
totalAmount += amounts[index];
_exactTransfer(token, recipients[index], amounts[index]);
}
emit BatchDistributed(batchId, token, recipients.length, totalAmount);
}
function rescueToken(address token, address recipient, uint256 amount)
external
onlyOwner
nonReentrant
{
if (token == address(0) || recipient == address(0)) revert InvalidAddress();
_safeTransfer(token, recipient, amount);
emit TokenRescued(token, recipient, amount);
}
function rescueNative(address payable recipient, uint256 amount)
external
onlyOwner
nonReentrant
{
if (recipient == address(0)) revert InvalidAddress();
(bool sent,) = recipient.call{value: amount}("");
if (!sent) revert TokenTransferFailed();
emit NativeRescued(recipient, amount);
}
function _approveBatch(bytes32 batchId, uint64 validUntil) private {
if (batchId == bytes32(0)) revert InvalidBatch();
if (validUntil <= block.timestamp) revert InvalidExpiry();
if (executedBatches[batchId]) revert AlreadyExecuted();
approvedUntil[batchId] = validUntil;
emit BatchApproved(batchId, validUntil);
}
function _exactTransfer(address token, address recipient, uint256 amount) private {
uint256 senderBefore = _safeBalanceOf(token, address(this));
uint256 recipientBefore = _safeBalanceOf(token, recipient);
_safeTransfer(token, recipient, amount);
uint256 senderAfter = _safeBalanceOf(token, address(this));
uint256 recipientAfter = _safeBalanceOf(token, recipient);
if (
senderBefore < senderAfter || senderBefore - senderAfter != amount
|| recipientAfter < recipientBefore || recipientAfter - recipientBefore != amount
) revert InexactTransfer();
}
function _safeBalanceOf(address token, address account) private view returns (uint256) {
if (token.code.length == 0) revert InvalidAddress();
(bool success, bytes memory result) =
token.staticcall(abi.encodeCall(IERC20.balanceOf, (account)));
if (!success || result.length < 32) revert TokenTransferFailed();
return abi.decode(result, (uint256));
}
function _safeTransfer(address token, address recipient, uint256 amount) private {
if (token.code.length == 0) revert InvalidAddress();
(bool success, bytes memory result) =
token.call(abi.encodeCall(IERC20.transfer, (recipient, amount)));
if (!success || (result.length != 0 && !abi.decode(result, (bool)))) {
revert TokenTransferFailed();
}
}
receive() external payable {}
}
Chain explorer3603msChain node79ms