HoodsSplitter
0xf42590e35941be123b42ffd9a50bdac33c37dada
Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
5,142 bytes
ABI entries
19
8 read · 6 write
License
none
Contract information
- Address
- 0xf42590e35941be123b42ffd9a50bdac33c37dada
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.26+commit.8a97fa7a
- Optimization
- Enabled
- Creator
- 0x221e0B4809…340b0C1100
- Creation tx
- 0x3e7f549320…11d5ed31ba
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (8)
BPS() → uint256
MAX_PAYEES() → uint256
creatorNFT() → address
deployer() → address
escrow(address) → uint256
hook() → address
pending(address) → uint256
splitOf(address) → address, address[], uint16[]
Events (4)
CreditedEscrowedForHolderReleasedSplitConfigured
ABI
[
{
"inputs": [
{
"internalType": "address",
"name": "creatorNFT_",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "Credited",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "holder",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "EscrowedForHolder",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "holder",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
},
{
"indexed": false,
"internalType": "bool",
"name": "viaConfig",
"type": "bool"
}
],
"name": "Released",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "author",
"type": "address"
},
{
"indexed": false,
"internalType": "address[]",
"name": "payees",
"type": "address[]"
},
{
"indexed": false,
"internalType": "uint16[]",
"name": "shares",
"type": "uint16[]"
}
],
"name": "SplitConfigured",
"type": "event"
},
{
"inputs": [],
"name": "BPS",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "MAX_PAYEES",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "creatorNFT",
"outputs": [
{
"internalType": "contract HoodsCreatorNFT",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "credit",
"outputs": [],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [],
"name": "deployer",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "escrow",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "hook",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "pending",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "release",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "hook_",
"type": "address"
}
],
"name": "setHook",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "address[]",
"name": "payees",
"type": "address[]"
},
{
"internalType": "uint16[]",
"name": "shares",
"type": "uint16[]"
}
],
"name": "setSplit",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
}
],
"name": "settleTo",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "splitOf",
"outputs": [
{
"internalType": "address",
"name": "author",
"type": "address"
},
{
"internalType": "address[]",
"name": "payees",
"type": "address[]"
},
{
"internalType": "uint16[]",
"name": "shares",
"type": "uint16[]"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "withdrawEscrow",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]Source code
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
import { HoodsCreatorNFT } from "./HoodsCreatorNFT.sol";
/// @title HoodsSplitter — creator fee distribution in native ETH (spec §2.4)
/// @notice The Hook forwards each token's creator share (ETH) here per swap. Payouts always resolve
/// the CURRENT HoodsCreatorNFT holder (invariant §8.6 — no path pays a stale address). NOTE: the
/// NFT itself is reassignable by the platform guardian (`ctoTransfer`, community takeovers) — the
/// splitter needs no special handling for that; it simply keeps paying whoever holds the NFT:
/// • The holder may configure up to MAX_PAYEES wallets with bps shares.
/// • A config is honored only while its author still holds the NFT; otherwise the whole release
/// goes to the current holder (stale configs are ignored, never paid).
/// Pull-based: ETH accrues per token; anyone can trigger `release`.
contract HoodsSplitter {
uint256 public constant MAX_PAYEES = 5;
uint256 public constant BPS = 10_000;
HoodsCreatorNFT public immutable creatorNFT;
address public immutable deployer; // only address allowed to wire `hook` (anti front-run, M-1)
address public hook; // set-once (breaks the hook↔splitter deploy cycle)
struct Split {
address author; // NFT holder who set this config; honored only while they still hold it
address[] payees;
uint16[] shares; // bps, sums to 10_000
}
mapping(address => uint256) public pending; // token => accrued ETH
mapping(address => uint256) public escrow; // holder => ETH from a failed CTO settle (pull to withdraw)
mapping(address => Split) private _splits; // token => payout config
event Credited(address indexed token, uint256 amount);
event Released(address indexed token, address indexed holder, uint256 amount, bool viaConfig);
event SplitConfigured(address indexed token, address indexed author, address[] payees, uint16[] shares);
event EscrowedForHolder(address indexed holder, uint256 amount);
constructor(address creatorNFT_) {
creatorNFT = HoodsCreatorNFT(creatorNFT_);
deployer = msg.sender;
}
/// One-shot binding to the hook (the only address allowed to credit). Deployer-only so it cannot be
/// front-run into a griefing address before the real hook binds (which would brick all trading, M-1).
function setHook(address hook_) external {
require(msg.sender == deployer, "not deployer");
require(hook == address(0) && hook_ != address(0), "hook set");
hook = hook_;
}
/// Hook-only: record ETH (sent with this call) for `token`'s creator stream.
function credit(address token) external payable {
require(msg.sender == hook, "not hook");
if (msg.value == 0) return;
pending[token] += msg.value;
emit Credited(token, msg.value);
}
/// Current NFT holder configures payout wallets. Re-set after any NFT transfer to re-enable.
function setSplit(address token, address[] calldata payees, uint16[] calldata shares) external {
require(msg.sender == creatorNFT.ownerOf(uint256(uint160(token))), "not creator");
require(payees.length == shares.length && payees.length > 0 && payees.length <= MAX_PAYEES, "bad split");
uint256 total;
for (uint256 i = 0; i < shares.length; i++) {
require(payees[i] != address(0), "zero payee");
total += shares[i];
}
require(total == BPS, "shares != 100%");
_splits[token] = Split({ author: msg.sender, payees: payees, shares: shares });
emit SplitConfigured(token, msg.sender, payees, shares);
}
/// Permissionless: pay out `token`'s accrued creator ETH.
function release(address token) external {
uint256 amount = pending[token];
require(amount > 0, "nothing pending");
pending[token] = 0;
address holder = creatorNFT.ownerOf(uint256(uint160(token)));
Split storage s = _splits[token];
if (s.author == holder && s.payees.length > 0) {
uint256 paid;
for (uint256 i = 0; i < s.payees.length; i++) {
uint256 cut = i == s.payees.length - 1 ? amount - paid : (amount * s.shares[i]) / BPS;
paid += cut;
if (cut > 0) _send(s.payees[i], cut);
}
emit Released(token, holder, amount, true);
} else {
_send(holder, amount);
emit Released(token, holder, amount, false);
}
}
/// CreatorNFT-only: on a CTO handover, pay `token`'s accrued fees to the OUTGOING holder `to` so a
/// takeover can never capture already-earned creator fees (CON-2). Explicit recipient (ignores the
/// old holder's split config) and best-effort: if `to` can't receive, the amount is escrowed for
/// them to pull — never redirected to the incoming holder, never able to brick the handover.
function settleTo(address token, address to) external {
require(msg.sender == address(creatorNFT), "not nft");
uint256 amount = pending[token];
if (amount == 0) return;
pending[token] = 0; // effect before interaction (reentrancy-safe: release() then sees nothing)
(bool ok,) = to.call{ value: amount }("");
if (ok) {
emit Released(token, to, amount, false);
} else {
escrow[to] += amount;
emit EscrowedForHolder(to, amount);
}
}
/// Pull escrowed fees (from a failed CTO settle). Anyone can withdraw only their own balance.
function withdrawEscrow() external {
uint256 amount = escrow[msg.sender];
require(amount > 0, "nothing");
escrow[msg.sender] = 0;
(bool ok,) = msg.sender.call{ value: amount }("");
require(ok, "withdraw failed");
}
/// Best-effort payout used by release(): a recipient that can't receive ETH (contract holder or a
/// reverting payee) must not freeze the whole payout (audit H-2). On failure, escrow for a pull —
/// never revert. release() zeroes `pending` before calling this, so it's reentrancy-safe.
function _send(address to, uint256 amount) private {
(bool ok,) = to.call{ value: amount }("");
if (!ok) {
escrow[to] += amount;
emit EscrowedForHolder(to, amount);
}
}
function splitOf(address token) external view returns (address author, address[] memory payees, uint16[] memory shares) {
Split storage s = _splits[token];
return (s.author, s.payees, s.shares);
}
}
Chain explorer5130msChain node88ms