---
eip: 8034
title: Referable NFT Royalties
description: A standalone royalty distribution for Referable NFTs, supporting multiple recipients, reference-based royalty distribution.
author: Ruiqiang Li (@richard-620) <richard.620.research@gmail.com>, Qin Wang <qin.wang@data61.csiro.au>, Shiping Chen <shiping.chen@data61.csiro.au>, Saber Yu (@OniReimu), Brian Yecies <byecies@uow.edu.au>, John Le <johnle@uow.edu.au>
discussions-to: https://ethereum-magicians.org/t/erc-8034-referable-nft-royalties/25643
status: Final
type: Standards Track
category: ERC
created: 2025-10-02
requires: 165, 712, 5521
---

## Abstract

This ERC proposes Royalty Distribution, a standalone royalty distribution for Referable Non-Fungible Tokens (rNFTs). It enables royalty distribution to multiple recipients at the primary level and referenced NFTs in the directed acyclic graph (DAG), with a single depth limit to control propagation. The standard is independent of [ERC-2981](./eip-2981.md). and token-standard-agnostic, but expects [ERC-5521](./eip-5521.md) rNFTs, which in practice build on [ERC-721](./eip-721.md) ownership semantics. It includes a function to query fixed royalty amounts (in basis points) for transparency. Royalties are voluntary, transparent, and configurable on-chain, supporting collaborative ecosystems and fair compensation.

## Motivation

[ERC-5521](./eip-5521.md) introduces Referable NFTs (rNFTs), which form a DAG through "referring" and "referred" relationships. Existing royalty standards like [ERC-2981](./eip-2981.md) do not account for this structure or support multiple recipients per level. This EIP addresses the need for a royalty mechanism that:

- Supports multiple recipients per royalty level (e.g., creators and collaborators).
- Distributes royalties to referenced NFTs in the DAG.
- Limits royalty propagation with a single reference depth.
- Provides a function to query fixed royalty amounts without a sale price.
- Provides a function to query fixed royalty amounts with a sale price.
- Operates independently of [ERC-721](./eip-721.md) or [ERC-2981](./eip-2981.md).
- Ensures transparency for marketplaces and users.
- Is discoverable via [ERC-165](./eip-165.md) supportsInterface.
- Supports optional [EIP-712](./eip-712.md) signature-based configuration to streamline marketplace or owner-driven updates.

## Specification

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.

### Interface

The `IRNFTRoyalty` interface defines the royalty distribution for rNFTs and MUST inherit `ERC165` so that supporting contracts can advertise compliance via [ERC-165](./eip-165.md):

```solidity
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.0;

interface IRNFTRoyalty is ERC165 {
    struct RoyaltyInfo {
        address recipient; // Address to receive royalty
        uint256 royaltyAmount; // Royalty amount (in wei for sale-based queries, basis points for fixed queries)
    }

    struct ReferenceRoyalty {
        RoyaltyInfo[] royaltyInfos; // Array of recipients and their royalty amounts
        uint256 referenceDepth; // Maximum depth in the reference DAG for royalty distribution
    }

    event ReferenceRoyaltiesPaid(
        address indexed rNFTContract,
        uint256 indexed tokenId,
        address indexed buyer,
        address marketplace,
        ReferenceRoyalty royalties
    );

    function getReferenceRoyaltyInfo(
        address rNFTContract,
        uint256 tokenId,
        uint256 salePrice
    ) external view returns (ReferenceRoyalty memory royalties);

    function getReferenceRoyaltyInfo(
        address rNFTContract,
        uint256 tokenId
    ) external view returns (ReferenceRoyalty memory royalties);

    function setReferenceRoyalty(
        address rNFTContract,
        uint256 tokenId,
        address[] memory recipients,
        uint256[] memory royaltyFractions,
        uint256 referenceDepth
    ) external;
    
    function setReferenceRoyalty(
        address rNFTContract,
        uint256 tokenId,
        address[] memory recipients,
        uint256[] memory royaltyFractions,
        uint256 referenceDepth,
        address signer,
        uint256 deadline,
        bytes calldata signature
    ) external;

    function supportsReferenceRoyalties() external view returns (bool);
    
    function royaltyNonce(address signer, address rNFTContract, uint256 tokenId) external view returns (uint256);
}
```
### ERC-165 requirement

  - Implementations MUST return true for `supportsInterface(type(IRNFTRoyalty).interfaceId)`.
  - Additional interfaces (e.g., `AccessControl`) SHOULD be forwarded via `super.supportsInterface(interfaceId)` when using inheritance.

### Signature-Based Configuration

To support gas-efficient and flexible configuration, implementations MUST support the following semantics for the signature overload:

- Authorization: The recovered EIP-712 signer MUST satisfy one of:
  1. Has `CONFIGURATOR_ROLE`, or
  2. Is `IERC721(rNFTContract).ownerOf(tokenId)` at verification time.
- Anti-replay: The message MUST include a nonce; the contract MUST track, verify, and increment a nonce to prevent replay.
- Typed Data: Use EIP-712 domain and struct as below (reference implementation provided).
- Deadline MUST be compared against `block.timestamp`; signatures with `block.timestamp > deadline` MUST be rejected.

RECOMMENDED EIP-712 Domain

- name = "RNFTRoyalty", version = "2", chainId, verifyingContract = `address(this)`

RECOMMENDED Typed Struct

```
SetReferenceRoyalty(
  address rNFTContract,
  uint256 tokenId,
  bytes32 recipientsHash,        // keccak256(abi.encode(recipients))
  bytes32 royaltyFractionsHash,  // keccak256(abi.encode(royaltyFractions))
  uint256 referenceDepth,
  address signer,
  uint256 deadline,
  uint256 nonce
)
```

### Key Components

#### Structs

- `RoyaltyInfo`:
  - recipient: The address to receive the royalty payment.
  - `royaltyAmount`: The royalty amount, in wei for `getReferenceRoyaltyInfo` with `salePrice`, or basis points (e.g., 100 = 1%) for `getReferenceRoyaltyInfo` without `salePrice`.
- `ReferenceRoyalty`:
  - `royaltyInfos`: An array of `RoyaltyInfo` for multiple recipients at the primary level and referenced NFTs.
  - `referenceDepth`: A single value limiting royalty distribution to referenced NFTs in the DAG.

#### Functions

- `getReferenceRoyaltyInfo(address rNFTContract, uint256 tokenId, uint256 salePrice)`:
  - Returns a `ReferenceRoyalty` struct with royalty amounts in wei, calculated from the `salePrice`.
  - Includes primary-level royalties and referenced NFT royalties up to `referenceDepth`.
  - MUST return zero amounts if no royalties are configured or if `salePrice` is zero.
- `getReferenceRoyaltyInfo(address rNFTContract, uint256 tokenId)`:
  - Returns a `ReferenceRoyalty` struct with fixed royalty amounts in basis points (e.g., 100 = 1%).
  - Includes primary-level royalties and referenced NFT royalties up to `referenceDepth`.
  - MUST return the configured royalty fractions without sale price calculations.
- `setReferenceRoyalty(address rNFTContract, uint256 tokenId, address[] recipients, uint256[] royaltyFractions, uint256 referenceDepth)`:
  - Configures royalties for the specified rNFT.
  - `recipients` and `royaltyFractions` (in basis points) define primary-level royalties.
  - `referenceDepth` limits royalty distribution to referenced NFTs.
  - MUST be restricted to authorized parties (e.g., rNFT contract owner).
  - MUST enforce a total primary-level royalty cap of ≤ 1000 basis points (10%).
- `setReferenceRoyalty(address rNFTContract, uint256 tokenId, address[] recipients, uint256[] royaltyFractions, uint256 referenceDepth, address signer, uint256 deadline, bytes signature)`:
  - Signature-based configuration per EIP-712.
  - MUST verify signer authorization, nonce, and enforce deadline to reject expired signatures.
  - The signer parameter specifies which address is expected to have signed the message, enabling relayer execution.
- `supportsReferenceRoyalties()`:
  - Returns true if the contract implements this standard. Discovery MUST rely on ERC-165.

- `royaltyNonce(address signer, address rNFTContract, uint256 tokenId) external view returns (uint256)`:
	-	Returns the current nonce used for EIP-712 signatures.
  

#### Events

- `ReferenceRoyaltiesPaid`: Emitted when royalties are paid, logging the rNFT contract, token ID, buyer, marketplace, and `ReferenceRoyalty` details (with `royaltyAmount` in wei).

### Royalty Distribution Model

- Primary Royalties: The rNFT’s `royaltyInfos` array specifies multiple recipients and their fractions (e.g., 5% total, split as 3% and 2%).
- Reference Royalties: At each hop, a total forwarded share equal to `REFERRED_ROYALTY_FRACTION` (e.g., 200 bps / 2%) is carved out and distributed across all referenced NFTs at that depth proportional to their configured weights (fallback: evenly if all weights are zero).
- Total Royalty Cap (Primary Level): The 10% (1000 bps) cap applies to the primary-level configured `royaltyFractions`. Propagated/reference-level flows are governed separately by `REFERRED_ROYALTY_FRACTION` and `referenceDepth`.
- Depth Limit: Implementations MUST cap `referenceDepth`; this reference implementation enforces <= 3 (RECOMMENDED).
- Fixed Royalties: The `getReferenceRoyaltyInfo` function without `salePrice` returns royalty fractions in basis points, enabling transparent inspection.

### Example

For an rNFT (contract 0xABC, token ID 1) with `referenceDepth` = 2:

- Configuration:
  - Primary royalties: 5% (3% to creator, 2% to collaborator).
  - Depth 1: Two referenced NFTs; a total of 2% is forwarded at depth 1 and split equally (1% each) under equal weights.
  - Depth 2: No royalties (capped by `referenceDepth`).
- `getReferenceRoyaltyInfo(0xABC, 1)`:
  
  - Returns:
  
    `{ royaltyInfos: [ {recipient: creator, royaltyAmount: 300}, {recipient: collaborator, royaltyAmount: 200}, {recipient: tokenA_owner, royaltyAmount: 100}, {recipient: tokenB_owner, royaltyAmount: 100} ], referenceDepth: 2 }`.
- Sale for 100 ETH:
  - `getReferenceRoyaltyInfo(0xABC, 1, 100 ether)` returns:
  
    `{ royaltyInfos: [ {recipient: creator, royaltyAmount: 3 ether}, {recipient: collaborator, royaltyAmount: 2 ether}, {recipient: tokenA_owner, royaltyAmount: 1 ether}, {recipient: tokenB_owner, royaltyAmount: 1 ether} ], referenceDepth: 2 }`.
  
    

## Rationale

- Fixed Royalty Query: The new `getReferenceRoyaltyInfo` function without salePrice allows users to inspect fixed royalty fractions (in basis points), improving transparency.
- Multiple Recipients: The `RoyaltyInfo` array supports collaborative projects.
- Single Depth Limit: Simplifies configuration and reduces gas costs.
- Standalone Design: Ensures compatibility with any ERC-5521 contract.
- Voluntary Royalties: Aligns with marketplace practices.
- Transparency: On-chain storage and fixed-amount queries enable verifiable royalties.
- ERC-165 Discoverability: Marketplaces and wallets can reliably detect support via supportsInterface, avoiding ad-hoc feature flags.
- EIP-712 Signatures: Off-chain approvals enable safe, gas-efficient configurations.

## Backwards Compatibility

This standard is independent of ERC-2981 and targets ERC-5521 rNFTs, which in practice build on ERC-721 ownership semantics. Marketplaces can integrate by:

- Checking ERC-165: `supportsInterface(type(IRNFTRoyalty).interfaceId)`.
- Calling `getReferenceRoyaltyInfo` (with or without sale price).
- Optionally leveraging the signature-based configuration for off-chain workflows.

## Reference Implementation

```
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; 
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/utils/introspection/IERC165.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

import "./IRNFTRoyalty.sol";

interface IERC_5521 is IERC165 {
    function setNode(uint256 tokenId, address[] memory addresses, uint256[][] memory tokenIds) external;
    function referringOf(address _address, uint256 tokenId) external view returns (address[] memory, uint256[][] memory);
    function referredOf(address _address, uint256 tokenId) external view returns (address[] memory, uint256[][] memory);
    function supportsInterface(bytes4 interfaceId) external view returns (bool);
}

contract RNFTRoyalty is IRNFTRoyalty, AccessControl, EIP712, ReentrancyGuard {
    using ECDSA for bytes32;

    bytes32 public constant CONFIGURATOR_ROLE = keccak256("CONFIGURATOR_ROLE");
    uint256 private constant MAX_ROYALTY_FRACTION = 1000; // 10%
    uint256 private constant REFERRED_ROYALTY_FRACTION = 200; // 2%
    uint256 private constant MAX_CHAIN_STEPS = 32;
    uint256 private constant MAX_RECIPIENTS = 64;

    // storage
    mapping(address => mapping(uint256 => ReferenceRoyalty)) private _royalties;
    event ReferenceRoyaltyConfigured(
        address indexed rNFTContract,
        uint256 indexed tokenId,
        address indexed setter,
        address[] recipients,
        uint256[] royaltyFractions,
        uint256 referenceDepth,
        bool viaSignature
    );

    // EIP-712 typed data & nonce
    bytes32 private constant _SET_TYPEHASH =
        keccak256("SetReferenceRoyalty(address rNFTContract,uint256 tokenId,bytes32 recipientsHash,bytes32 royaltyFractionsHash,uint256 referenceDepth,address signer,uint256 deadline,uint256 nonce)");
    // (signer => rNFT => tokenId => nonce)
    mapping(address => mapping(address => mapping(uint256 => uint256))) private _sigNonces;

    constructor() EIP712("RNFTRoyalty", "2") {
        _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
        _grantRole(CONFIGURATOR_ROLE, msg.sender);
    }

    // ===== IRNFTRoyalty =====

    // expose nonce for off-chain signing
    function royaltyNonce(address signer, address rNFTContract, uint256 tokenId)
        external
        view
        returns (uint256)
    {
        return _sigNonces[signer][rNFTContract][tokenId];
    }

    function setReferenceRoyalty(
        address rNFTContract,
        uint256 tokenId,
        address[] calldata recipients,
        uint256[] calldata royaltyFractions,
        uint256 referenceDepth
    ) external onlyRole(CONFIGURATOR_ROLE) {
        _configureRoyalty(rNFTContract, tokenId, recipients, royaltyFractions, referenceDepth);
        emit ReferenceRoyaltyConfigured(
            rNFTContract,
            tokenId,
            msg.sender,
            recipients,
            royaltyFractions,
            referenceDepth,
            false
        );
    }

    /// @notice Configure reference royalty via EIP-712 signature (supports relayers).
    /// @dev
    /// - Uses explicit `signer` for nonce lookup and authorization; caller can be a relayer.
    /// - Includes `deadline` in the signed struct; reverts with "Signature expired" if now > deadline.
    /// - Non-reentrant to defend against malicious `rNFT.ownerOf` implementations.
    /// - Authorization: `signer` must have `CONFIGURATOR_ROLE` or be current `ownerOf(tokenId)`.
    /// - Nonce scope: per-signer-per-token; increments on success to prevent replay.
    function setReferenceRoyalty(
        address rNFTContract,
        uint256 tokenId,
        address[] calldata recipients,
        uint256[] calldata royaltyFractions,
        uint256 referenceDepth,
        address signer,
        uint256 deadline,
        bytes calldata signature
    ) external nonReentrant {
        _checkParams(rNFTContract, recipients, royaltyFractions, referenceDepth);

        bytes32 recipientsHash = keccak256(abi.encode(recipients));
        bytes32 fractionsHash  = keccak256(abi.encode(royaltyFractions));
        // Compute expected signer digest and use per-signer-per-token nonce (explicit signer for relaying)
        require(signer != address(0), "Invalid signer");
        uint256 nonce = _sigNonces[signer][rNFTContract][tokenId];

        require(block.timestamp <= deadline, "Signature expired");

        bytes32 structHash = keccak256(
            abi.encode(
                _SET_TYPEHASH,
                rNFTContract,
                tokenId,
                recipientsHash,
                fractionsHash,
                referenceDepth,
                signer,
                deadline,
                nonce
            )
        );

        bytes32 digest = _hashTypedDataV4(structHash);
        address recovered = ECDSA.recover(digest, signature);
        require(recovered == signer && signer != address(0), "Invalid signature");

        // Authorization: CONFIGURATOR_ROLE or current owner
        bool authorized = hasRole(CONFIGURATOR_ROLE, signer);
        if (!authorized) {
            address owner = _safeOwnerOf(IERC721(rNFTContract), tokenId);
            require(signer == owner, "Signer not authorized");
        }

        // effects: bump nonce to prevent replay
        _sigNonces[signer][rNFTContract][tokenId] = nonce + 1;

        // configure royalties
        _configureRoyalty(rNFTContract, tokenId, recipients, royaltyFractions, referenceDepth);
        emit ReferenceRoyaltyConfigured(
            rNFTContract,
            tokenId,
            signer,
            recipients,
            royaltyFractions,
            referenceDepth,
            true
        );
    }

    /// @notice Compute reference royalty distribution for a concrete sale price (values in wei).
    /// @param rNFTContract RNFT contract implementing IERC_5521
    /// @param tokenId Token id
    /// @param salePrice Sale price in wei
    function getReferenceRoyaltyInfo(
        address rNFTContract,
        uint256 tokenId,
        uint256 salePrice
    ) external view returns (ReferenceRoyalty memory royalties) {
        royalties = _royalties[rNFTContract][tokenId];
        if (salePrice == 0) {
            uint256 len = royalties.royaltyInfos.length;
            if (len == 0) return royalties;
            RoyaltyInfo[] memory zeroed = new RoyaltyInfo[](len);
            for (uint256 i = 0; i < len; i++) {
                zeroed[i] = RoyaltyInfo(royalties.royaltyInfos[i].recipient, 0);
            }
            royalties.royaltyInfos = zeroed;
            return royalties;
        }
        RoyaltyInfo[] memory chainRoyalties = _calculateChainRoyalties(rNFTContract, tokenId, salePrice);
        royalties.royaltyInfos = chainRoyalties;
        return royalties;
    }

    /// @notice Compute reference royalty distribution in basis points (bps), i.e. relative amounts.
    /// @param rNFTContract RNFT contract implementing IERC_5521
    /// @param tokenId Token id
    function getReferenceRoyaltyInfo(
        address rNFTContract,
        uint256 tokenId
    ) external view returns (ReferenceRoyalty memory royalties) {
        royalties = _royalties[rNFTContract][tokenId];
        if (royalties.royaltyInfos.length == 0) return royalties;
        RoyaltyInfo[] memory bpsRoyalties = _calculateChainRoyalties(rNFTContract, tokenId, 0);
        royalties.royaltyInfos = bpsRoyalties;
        return royalties;
    }

    function supportsReferenceRoyalties() external pure returns (bool) {
        return true;
    }

    // ===== ERC-165 =====
    function supportsInterface(bytes4 interfaceId)
        public
        view
        override(AccessControl, IERC165)
        returns (bool)
    {
        return
            interfaceId == type(IRNFTRoyalty).interfaceId ||
            super.supportsInterface(interfaceId);
    }

    // ===== Internal =====
    function _checkParams(
        address rNFTContract,
        address[] calldata recipients,
        uint256[] calldata royaltyFractions,
        uint256 referenceDepth
    ) internal pure {
        require(rNFTContract != address(0), "Invalid contract");
        require(recipients.length == royaltyFractions.length, "Length mismatch");
        require(recipients.length <= MAX_RECIPIENTS, "Too many recipients");
        require(referenceDepth <= 3, "Depth too high");
        for (uint256 i = 0; i < recipients.length; ++i) {
            require(recipients[i] != address(0), "Zero recipient");
        }
    }

    function _configureRoyalty(
        address rNFTContract,
        uint256 tokenId,
        address[] calldata recipients,
        uint256[] calldata royaltyFractions,
        uint256 referenceDepth
    ) internal {
        uint256 totalFraction = 0;
        for (uint256 i = 0; i < royaltyFractions.length; i++) {
            totalFraction += royaltyFractions[i];
        }
        require(totalFraction <= MAX_ROYALTY_FRACTION, "Royalty cap exceeded");

        ReferenceRoyalty memory config;
        config.referenceDepth = referenceDepth;
        config.royaltyInfos = new RoyaltyInfo[](recipients.length);

        for (uint256 i = 0; i < recipients.length; i++) {
            config.royaltyInfos[i] = RoyaltyInfo(recipients[i], royaltyFractions[i]);
        }

        _royalties[rNFTContract][tokenId] = config;
    }

    function _safeOwnerOf(IERC721 rNFT, uint256 tokenId) internal view returns (address) {
        address owner = rNFT.ownerOf(tokenId);
        require(owner != address(0), "No owner");
        return owner;
    }

    function _calculateChainRoyalties(
        address rNFTContract,
        uint256 tokenId,
        uint256 salePrice
    ) internal view returns (RoyaltyInfo[] memory) {
        ReferenceRoyalty memory currentRoyalty = _royalties[rNFTContract][tokenId];
        if (currentRoyalty.royaltyInfos.length == 0) {
            return new RoyaltyInfo[](0);
        }

        RoyaltyInfo[] memory staged = new RoyaltyInfo[](MAX_CHAIN_STEPS * 32 + 32);
        uint256 count = 0;

        uint256 totalShare = _sumShares(currentRoyalty);
        (uint256 netPrimary, uint256 remainder) = _splitRoyalty(totalShare, salePrice, currentRoyalty.referenceDepth > 0);
        count = _appendDistribution(staged, count, currentRoyalty, netPrimary);

        if (remainder == 0) {
            return _shrink(staged, count);
        }

        // Layered aggregation (BFS) to support multi-parent merges
        uint256 maxItems = MAX_CHAIN_STEPS * 32 + 32;
        address[] memory curContracts = new address[](maxItems);
        uint256[] memory curIds = new uint256[](maxItems);
        uint256[] memory curAmts = new uint256[](maxItems);
        uint256[] memory curDepths = new uint256[](maxItems);
        uint256 curCount = 0;
        if (remainder > 0 && currentRoyalty.referenceDepth > 0) {
            curContracts[0] = rNFTContract;
            curIds[0] = tokenId;
            curAmts[0] = remainder;
            curDepths[0] = currentRoyalty.referenceDepth;
            curCount = 1;
        }

        address[] memory processedContracts = new address[](maxItems);
        uint256[] memory processed = new uint256[](maxItems);
        uint256 processedCount = 0;

        while (curCount > 0) {
            address[] memory nextContracts = new address[](maxItems);
            uint256[] memory nextIds = new uint256[](maxItems);
            uint256[] memory nextAmts = new uint256[](maxItems);
            uint256[] memory nextDepths = new uint256[](maxItems);
            uint256 nextCount = 0;

            for (uint256 iL = 0; iL < curCount; iL++) {
                address curContract = curContracts[iL];
                uint256 curId = curIds[iL];
                uint256 amt = curAmts[iL];
                uint256 depth = curDepths[iL];
                if (amt == 0) continue;

                IERC_5521 curRNFT = IERC_5521(curContract);
                IERC721 curRNFT721 = IERC721(curContract);

                bool seen = false;
                for (uint256 p = 0; p < processedCount; p++) {
                    if (processed[p] == curId && processedContracts[p] == curContract) { seen = true; break; }
                }
                if (seen) {
                    address cycOwner = _safeOwnerOf(curRNFT721, curId);
                    staged[count++] = RoyaltyInfo(cycOwner, amt);
                    continue;
                }

                uint256 maxChildren = 32;
                address[] memory childContracts = new address[](maxChildren);
                uint256[] memory childIds = new uint256[](maxChildren);
                uint256 children = _collectReferring(curRNFT, curContract, curId, childContracts, childIds);
                if (depth == 0 || children == 0) {
                    address fallbackOwner = _safeOwnerOf(curRNFT721, curId);
                    staged[count++] = RoyaltyInfo(fallbackOwner, amt);
                    processedContracts[processedCount] = curContract;
                    processed[processedCount++] = curId;
                    continue;
                }

                uint256 keepBase = (amt * (10_000 - REFERRED_ROYALTY_FRACTION)) / 10_000;
                uint256 passBase = amt - keepBase;

                uint256[] memory childWeights = new uint256[](maxChildren);
                ReferenceRoyalty[] memory childConfigs = new ReferenceRoyalty[](maxChildren);
                uint256 sumWeights = 0;

                for (uint256 j = 0; j < children; j++) {
                    address childContract = childContracts[j];
                    uint256 cid = childIds[j];
                    ReferenceRoyalty memory cfg = _royalties[childContract][cid];
                    childConfigs[j] = cfg;
                    if (cfg.royaltyInfos.length > 0) {
                        uint256 w = _sumShares(cfg);
                        childWeights[j] = w;
                        sumWeights += w;
                    }
                }

                if (sumWeights == 0) {
                    uint256 each = amt / children;
                    uint256 rem = amt - (each * children);
                    for (uint256 j = 0; j < children; j++) {
                        address ow = _safeOwnerOf(IERC721(childContracts[j]), childIds[j]);
                        uint256 share = each + (j == children - 1 ? rem : 0);
                        staged[count++] = RoyaltyInfo(ow, share);
                    }
                    processedContracts[processedCount] = curContract;
                    processed[processedCount++] = curId;
                    continue;
                }

                uint256 passDistributed = 0;
                uint256 lastWeightedIdx = 0;
                uint256[] memory keepShares = new uint256[](children);
                uint256[] memory passShares = new uint256[](children);
                for (uint256 j = 0; j < children; j++) {
                    if (childWeights[j] == 0) continue;
                    lastWeightedIdx = j;
                    uint256 kShare = (keepBase * childWeights[j]) / sumWeights;
                    uint256 pShare = (passBase * childWeights[j]) / sumWeights;
                    keepShares[j] = kShare;
                    passShares[j] = pShare;
                    passDistributed += pShare;
                }
                // Remainders: pass and keep
                uint256 passRemainder = passBase - passDistributed;
                if (passRemainder > 0) {
                    passShares[lastWeightedIdx] += passRemainder;
                }
                uint256 keepDistributed = 0;
                for (uint256 j2 = 0; j2 < children; j2++) {
                    keepDistributed += keepShares[j2];
                }
                uint256 keepRemainder = keepBase - keepDistributed;
                if (keepRemainder > 0) {
                    keepShares[lastWeightedIdx] += keepRemainder;
                }

                for (uint256 j = 0; j < children; j++) {
                    if (childWeights[j] == 0) continue;
                    uint256 kShare = keepShares[j];
                    uint256 pShare = passShares[j];
                    ReferenceRoyalty memory cfgj = childConfigs[j];
                    uint256 cid2 = childIds[j];
                    address childContract = childContracts[j];
                    uint256 nextDepth = depth > 0 ? depth - 1 : 0;
                    if (nextDepth == 0) {
                        // Depth exhausted: distribute both keep and pass to child's recipients
                        count = _appendDistribution(staged, count, cfgj, kShare + pShare);
                    } else {
                        if (kShare > 0) {
                            count = _appendDistribution(staged, count, cfgj, kShare);
                        }
                        if (pShare > 0) {
                            bool merged = false;
                            for (uint256 nx = 0; nx < nextCount; nx++) {
                                if (nextIds[nx] == cid2 && nextContracts[nx] == childContract) {
                                    nextAmts[nx] += pShare;
                                    if (nextDepth > nextDepths[nx]) {
                                        nextDepths[nx] = nextDepth;
                                    }
                                    merged = true;
                                    break;
                                }
                            }
                            if (!merged) {
                                nextContracts[nextCount] = childContract;
                                nextIds[nextCount] = cid2;
                                nextAmts[nextCount] = pShare;
                                nextDepths[nextCount] = nextDepth;
                                nextCount++;
                            }
                        }
                    }
                }

                processedContracts[processedCount] = curContract;
                processed[processedCount++] = curId;
            }

            for (uint256 k = 0; k < nextCount; k++) {
                curContracts[k] = nextContracts[k];
                curIds[k] = nextIds[k];
                curAmts[k] = nextAmts[k];
                curDepths[k] = nextDepths[k];
            }
            curCount = nextCount;
        }

        return _shrink(staged, count);
    }

    function _splitRoyalty(uint256 totalRate, uint256 salePrice, bool canPropagate)
        internal
        pure
        returns (uint256 netPrimary, uint256 forwardedAmount)
    {
        if (totalRate == 0) {
            return (0, 0);
        }

        if (!canPropagate) {
            if (salePrice == 0) {
                return (totalRate, 0);
            }

            return ((salePrice * totalRate) / 10_000, 0);
        }

        if (salePrice == 0) {
            if (totalRate <= REFERRED_ROYALTY_FRACTION) {
                return (0, totalRate);
            }

            return (totalRate - REFERRED_ROYALTY_FRACTION, REFERRED_ROYALTY_FRACTION);
        }

        uint256 gross = (salePrice * totalRate) / 10_000;
        uint256 forwarded = (salePrice * REFERRED_ROYALTY_FRACTION) / 10_000;
        if (forwarded > gross) {
            forwarded = gross;
        }

        return (gross > forwarded ? gross - forwarded : 0, forwarded);
    }

    function _sumShares(ReferenceRoyalty memory config) internal pure returns (uint256 total) {
        for (uint256 i = 0; i < config.royaltyInfos.length; i++) {
            total += config.royaltyInfos[i].royaltyAmount;
        }
    }

    function _collectReferring(
        IERC_5521 rNFT,
        address rNFTContract,
        uint256 tokenId,
        address[] memory childContracts,
        uint256[] memory childIds
    ) internal view returns (uint256 childCount) {
        (address[] memory refContracts, uint256[][] memory refTokenIds) =
            rNFT.referringOf(rNFTContract, tokenId);

        uint256 maxChildren = childIds.length;
        uint256 listLen = refContracts.length;
        if (refTokenIds.length < listLen) {
            listLen = refTokenIds.length;
        }

        for (uint256 i = 0; i < listLen && childCount < maxChildren; i++) {
            uint256[] memory ids = refTokenIds[i];
            for (uint256 j = 0; j < ids.length && childCount < maxChildren; j++) {
                childContracts[childCount] = refContracts[i];
                childIds[childCount] = ids[j];
                childCount++;
            }
        }
    }

    function _appendDistribution(
        RoyaltyInfo[] memory staged,
        uint256 count,
        ReferenceRoyalty memory config,
        uint256 amount
    ) internal pure returns (uint256) {
        uint256 len = config.royaltyInfos.length;
        if (len == 0) {
            return count;
        }

        require(count + len <= staged.length, "royalty overflow");

        if (amount == 0) {
            for (uint256 i = 0; i < len; i++) {
                staged[count++] = RoyaltyInfo(config.royaltyInfos[i].recipient, 0);
            }
            return count;
        }

        uint256 totalShare = _sumShares(config);

        if (totalShare == 0) {
            staged[count++] = RoyaltyInfo(config.royaltyInfos[0].recipient, amount);
            for (uint256 i = 1; i < len; i++) {
                staged[count++] = RoyaltyInfo(config.royaltyInfos[i].recipient, 0);
            }
            return count;
        }

        uint256 remaining = amount;
        for (uint256 i = 0; i < len; i++) {
            uint256 share = config.royaltyInfos[i].royaltyAmount;
            if (share == 0) {
                staged[count++] = RoyaltyInfo(config.royaltyInfos[i].recipient, 0);
                continue;
            }

            uint256 portion = (amount * share) / totalShare;
            if (portion > remaining) {
                portion = remaining;
            }
            remaining -= portion;

            staged[count++] = RoyaltyInfo(config.royaltyInfos[i].recipient, portion);
        }

        if (remaining > 0) {
            staged[count - 1].royaltyAmount += remaining;
        }

        return count;
    }

    function _shrink(RoyaltyInfo[] memory staged, uint256 count)
        internal
        pure
        returns (RoyaltyInfo[] memory out)
    {
        out = new RoyaltyInfo[](count);
        for (uint256 i = 0; i < count; i++) {
            out[i] = staged[i];
        }
    }

    function recordRoyaltyPayment(
        address rNFTContract,
        uint256 tokenId,
        address buyer,
        ReferenceRoyalty memory royalties
    ) external {
        emit ReferenceRoyaltiesPaid(rNFTContract, tokenId, buyer, msg.sender, royalties);
    }
}
```



## Security Considerations

- Access Control: `setReferenceRoyalty` MUST be restricted to authorized roles (e.g., via AccessControl).
- Total Royalty Cap (Primary Level): The 10% (1000 bps) cap applies to the primary-level configured `royaltyFractions`. Propagated/reference-level flows are governed separately by `REFERRED_ROYALTY_FRACTION` and `referenceDepth`.
- Gas Limits: `referenceDepth` MUST be capped (e.g., ≤ 3) to avoid high gas costs.
- Input Validation: Ensure non-zero addresses and valid royalty fractions.
- Interface Signaling: Ensure supportsInterface forwards properly to parents.
- Signature Replay: Use a per-signer-per-(rNFTContract, tokenId) nonce, and increment after successful verification. Implementations MUST document the chosen scope.

## Copyright

Copyright and related rights waived via [CC0](../LICENSE.md).
