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