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
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