ERC-4910 NFT with built-in royalties (Royalty Bearing NFTs)

Final · NFTs & multi-token · created 2022-03-14

Extends the NFT standard (ERC-721) so that the definition, distribution and payment of royalties are all handled onchain. It builds a layered royalty structure linking original works and derivative works, so revenue reaches creators without a central authority.

Why it is needed

Royalty management is a long-standing problem marked by complicated contracts, opaque operations, and fraud and deception. The problem is worse for layered royalties, where derivative works come from an original, such as a print made from a painting or a song that samples another song. The original creator should receive a share each time a derivative work is sold and resold, but without a mechanism to guarantee this, intermediaries can manipulate or skip payments entirely. Even in existing NFT trades, buyers and sellers can avoid royalties by exchanging money outside the contract. This standard is designed so that the NFT contract itself takes part directly in sales and payments, which means royalties go to the people legally entitled to receive them.

What to implement

In depth

Royalty account and sub-account structure

Each NFT is linked to one Royalty Account (RA) through its tokenId, and an RA must have at least one sub-account (R1). The RA identifier raAccountId must be unique (R2) and must be linked to the tokenId (R3). A sub-account can be an individual account or another RA, and only individual accounts can hold a withdrawable balance. If a sub-account is an RA, its share must be redistributed to that RA's sub-accounts, so its final balance must be 0. There must be a direct link between parent and child NFTs (R4), and the contract must also maintain the list of supported token types (R5), the last verified balance per token (R6), sale listings (R7, R8) and registered payments (R9, R10).

Minting rules and contract ownership structure

When an NFT is minted, an RA and a sub-account are created together, and the NFT, its owner, and the ancestor's RA (if there is an ancestor) must be linked to them (R18). Unusually, the owner of a new NFT must be the NFT contract itself (R19), and the actual owner is designated through isApproved and acts like the owner (R20). This design lets the contract serve as the escrow for payments and royalties. The mint input carries parent, canBeParent, maxChildren, royaltySplitForItsChildren, and uri (R21), and rules apply such as: the number of tokens issued must not be 0, msg.sender must hold MINTER_ROLE or CREATOR_Role, royaltySplitForItsChildren must be 0 for an NFT that cannot have children, and the parent's generation level must be below the maximum allowed number of generations (R23).

Two Sale Methods and Payment Handling

Sales are divided into direct sales (trxnType = 0) and exchange-brokered sales (trxnType = 1). Direct sales must be listed and delisted through the contract with listNFT and removeNFTListing (R24). The buyer always pays the NFT contract, not the seller. For ERC-20, the buyer calls executePayment after approve (R31). For a protocol token such as ETH, the buyer encodes seller, a tokenId array, receiver, and trxnType into msg.data and sends it to the payable fallback function (R39 to R41). receive() cannot accept msg.data, so it is not permitted. A direct sale verifies the payment, transfers immediately with the internal _safeTransferFrom, and removes the listing (R37, R38). A brokered sale registers the payment in registeredPayment and verifies it later in a separate safeTransferFrom (R36). Before completion, the buyer can reverse the payment with reversePayment (R44, R45).

Transfer Function Restrictions and _data Validation

To prevent royalty bypass, only one transfer function is kept. transferFrom and safeTransferFrom without data must be disabled (R46). The one remaining safeTransferFrom uses the ERC-721 interface as is, but encodes the information for transferring multiple tokens in _data. _data must contain at least the seller, buyer, and receiver addresses, the tokenId array, the payment token type, the payment amount, the registered payment identifier, and block.chainid (R48), and it must pass rules such as seller == from, the receiver is not the seller, and each token matches the token in the registered payment (R49). The transfer happens only after all royalties have been distributed, and it works by changing the approved address with _approve(to, tokenId[i]), so the technical owner remains the contract (R53). After the approval is updated, the registered payment is removed (R54).

Royalty Distribution by Layer Decomposition

distributePayment splits the recursive problem of a royalty tree of depth N into N per-layer problems and processes them from the bottom layer up to the root ancestor. The payment amount is divided equally among all NFTs included in the transfer. For each sub-account, if isIndividual is true, it adds the amount after applying the royalty ratio to that account's royaltybalance and emits an event containing assetId, accountId, tokenType, and royaltybalance. If false, it holds the amount in a temporary variable, moves on to the next RA along ancestor, and repeats the same rules, ending when it reaches an RA that has no ancestor and whose sub-accounts are all individual (R51). Ratio calculation represents 100% as an integer such as 10000, multiplies, then divides, so overflow and underflow need attention.

Royalty Withdrawal and Settlement During Transfer

There are two withdrawal functions, a public version and an internal version. The public royaltyPayOut requires msg.sender == RAsubaccount, that the token exists and has not been burned, that the sub-account's isIndividual == true, and that amount is at most royaltybalance (R56), and it has nonReentrant protection because funds leave the contract. The internal _royaltyPayOut sends the payment and deducts the balance only if it succeeds (R58, R59). During a transfer, the entire sub-account balance of the from address (the approved actual owner) is first paid out to from, and only after royaltybalance reaches 0 is the sub-account owner updated to to (R52).

Constraints on Account Updates and Deletion

In updateRoyaltyAccount, the asset identifier, ancestor, payment token type, sub-account balance, and the distribution ratio inherited from the parent cannot be changed. The sum of all distribution ratios must always be 1, msg.sender must be the sub-account owner of the RA being modified, and one can only reduce one's own ratio. The amount reduced must be added to existing accounts' ratios or to a new sub-account with a zero balance (R15). deleteRoyaltyAccount is a very gas-expensive operation, and it is allowed only when the tokenId has been burned, all ancestor and descendant tokens linked by lineage have been burned, and all sub-account balances are 0 (R17). If the rules are not followed, part of the tree can collapse and funds can be locked.

Things to watch out for

Original ERC · ← ERCs