--- 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) , Qin Wang , Shiping Chen , Saber Yu (@OniReimu), Brian Yecies , John Le 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).