diff --git a/EIPS/eip-4910.md b/EIPS/eip-4910.md index 2deb8cd39b8a6d..bd02bf70f7a21d 100644 --- a/EIPS/eip-4910.md +++ b/EIPS/eip-4910.md @@ -4,8 +4,7 @@ title: Royalty Bearing NFTs description: Extension of ERC-721 to correctly define, process, and pay (hierarchical) onchain NFT royalties. author: Andreas Freund (@Therecanbeonlyone1969) discussions-to: https://ethereum-magicians.org/t/royalty-bearing-nfts/8453 -status: Last Call -last-call-deadline: 2023-03-31 +status: Final type: Standards Track category: ERC created: 2022-03-14 @@ -13,11 +12,13 @@ requires: 165, 721 --- ## Abstract + The proposal directly connects NFTs and royalties in a smart contract architecture extending the [ERC-721](./eip-721.md) standard, with the aim of precluding central authorities from manipulating or circumventing payments to those who are legally entitled to them. The proposal builds upon the OpenZeppelin Smart Contract Toolbox architecture, and extends it to include royalty account management (CRUD), royalty balance and payments management, simple trading capabilities -- Listing/De-Listing/Buying -- and capabilities to trace trading on exchanges. The royalty management capabilities allow for hierarchical royalty structures, referred to herein as royalty trees, to be established by logically connecting a "parent" NFT to its "children", and recursively enabling NFT "children" to have more children. ## Motivation + The management of royalties is an age-old problem characterized by complex contracts, opaque management, plenty of cheating and fraud. The above is especially true for a hierarchy of royalties, where one or more assets is derived from an original asset such as a print from an original painting, or a song is used in the creation of another song, or distribution rights and compensation are managed through a series of affiliates. @@ -36,11 +37,13 @@ In order to solve for the complicated inheritance problem, this proposal breaks This affords creators, and the distributors of art derived from the original, the opportunity to achieve passive income from the creative process, enhancing the value of an NFT, since it now not only has intrinsic value but also comes with an attached cash flow. ## 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. ### Outline This proposal introduces several new concepts as extensions to the ERC-721 standard that warrant explanation: + * **Royalty Account (RA)** * A Royalty Account is attached to each NFT through its `tokenId` and consists of several sub-accounts which can be accounts of individuals or other RAs. A Royalty Account is identified by an account identifier. * **Account Type** @@ -88,6 +91,7 @@ Authorized user addresses can list NFTs for sale for non-exchange mediated NFT p To avoid royalty circumvention, a buyer will always pay the NFT contract directly and not the seller. The seller is paid through the royalty distribution and can later request a payout. The payment process depends on whether the payment is received in ETH or an [ERC-20](./eip-20.md) token: + * ERC-20 Token 1. The Buyer must `approve` the NFT contract for the purchase price, `payment` for the selected payment token (ERC-20 contract address). 2. For an ERC-20 payment token, the Buyer must then call the `executePayment` in the NFT contract -- the ERC-20 is not directly involved. @@ -98,6 +102,7 @@ The payment process depends on whether the payment is received in ETH or an [ERC The input parameters must satisfy several requirements for the NFT to be transferred AFTER the royalties have been properly distributed. Furthermore, the ability to transfer more than one token at a time is also considered. The proposal defines: + * Input parameter validation * Payment Parameter Validation * Distributing Royalties @@ -114,12 +119,15 @@ This is the final part of the proposal. There are two versions of the payout function -- a `public` function and an `internal` function. The public function has the following interface: + ``` function royaltyPayOut (uint256 tokenId, address RAsubaccount, address payable payoutAccount, uint256 amount) public virtual nonReentrant returns (bool) ``` + where we only need the `tokenId`, the RA Sub Account address, `_RAsubaccount` which is the `owner`, and the amount to be paid out, `_amount`. Note that the function has `nonReentrant` modifier protection, because funds are being payed out. To finally send a Payout payment, the following steps need to be taken: + * find the RA Sub Account based on `RAaccount` and the `subaccountPos` and extract the balance * extract `tokenType` from the Sub Account * based on the token type, send the payout payment (not exceeding the available balance) @@ -202,6 +210,7 @@ Below are the definitions and interfaces for the Royalty Account RUD (Read-Updat There is only one get function required because a Royalty Account and its sub accounts can be retrieved through the `tokenId` in the `ancestry` field of the Royalty Account. **[R12]** *The `getRoyaltyAccount` function interface MUST adhere to the definition below:* + ``` /// @dev Function to fetch a Royalty Account for a given tokenId /// @param tokenId is the identifier of the NFT to which a Royalty Account is attached @@ -215,6 +224,7 @@ function getRoyaltyAccount (uint256 tokenId) public view virtual returns (addres **[R13]** *The following business rules MUST be enforced in the `getRoyaltyAccount` function:* + * *`tokenId` exists and is not burned* #### Update a Royalty Account @@ -223,6 +233,7 @@ In order to update a Royalty Account, the caller must have both the 'tokenId' an **[R14]** *The `updateRoyaltyAccount` function interface MUST adhere to the definition below:* + ``` /// @dev Function to update a Royalty Account and its Sub Accounts /// @param tokenId is the identifier of the NFT to which the Royalty Account to be updated is attached @@ -234,6 +245,7 @@ function updateRoyaltyAccount (uint256 _tokenId, `RoyaltyAccount memory _raAccou The update functionality of a Royalty Account, while straightforward, is also highly nuanced. To avoid complicated change control rules such as multi-signature rules, Royalty Account changes are kept simple. **[R15]** *The business rules for the update function are as follows:* + 1. *An NFTs asset identifier MUST NOT be changed.* 2. *An NFTs ancestor MUST NOT be updated.* 3. *An NFTs token type accepted for payment MUST NOT be updated.* @@ -246,19 +258,19 @@ The update functionality of a Royalty Account, while straightforward, is also hi 9.1 *the Sub Account belonging to the account identifier MUST NOT be removed* - 9.2 * A royalty split MUST only be decreased, and either the existing sub account's royalty split MUST be increased accordingly such that the sum of all royalty splits remains equal to 1 or its numerical equivalent, or one or more new Royalty Sub Accounts MUST be added according to rule 10.* + 9.2 *A royalty split MUST only be decreased, and either the existing sub account's royalty split MUST be increased accordingly such that the sum of all royalty splits remains equal to 1 or its numerical equivalent, or one or more new Royalty Sub Accounts MUST be added according to rule 10.* - 9.3 * a royalty balance MUST NOT be changed* + 9.3 *a royalty balance MUST NOT be changed* - 9.4 * an account identifier MUST NOT be NULL* + 9.4 *an account identifier MUST NOT be NULL* 10. *If `msg.sender` is equal to the account identifier of one of the Sub Account owners which is not the parent NFT, an additional Royalty Sub Accounts MAY be added* 10.1 *if the royalty split of the Royalty Sub Account belonging to `msg.sender` is reduced* - 10.1.1 *then the royalty balance in each new Royalty Sub Account MUST be zero* + * then the royalty balance in each new Royalty Sub Account MUST be zero - 10.1.2 *the sum of the new royalty splits data MUST be equal to the royalty split of the Royalty Sub Account of `msg.sender` before it was modified* + * and the sum of the new royalty splits data MUST be equal to the royalty split of the Royalty Sub Account of `msg.sender` before it was modified 10.2 *new account identifier MUST not be NULL* @@ -269,6 +281,7 @@ The update functionality of a Royalty Account, while straightforward, is also hi While sometimes deleting a Royalty Account is necessary, even convenient, it is a very costly function in terms of gas, and should not be used unless one is absolutely sure that the conditions enumerated below are met. **[R16]** *The `deleteRoyaltyAccount` function interface MUST adhere to the definition below:* + ``` /// @dev Function to delete a Royalty Account /// @param tokenId is the identifier of the NFT to which the Royalty Account to be updated is attached @@ -277,6 +290,7 @@ function deleteRoyaltyAccount (uint256 _tokenId) public virtual returns (bool) ``` **[R17]** *The business rules for this function are as follows:* + * *`_tokenId` MUST be burned, i.e., have owner `address(0)`.* * *all `tokenId` numbers genealogically related to `_tokenId` either as ancestors or offspring MUST also be burnt.* * *all balances in the Royalty Sub Accounts MUST be zero.* @@ -298,6 +312,7 @@ To this end the specification utilizes the ERC-721 `_safemint` function in a new This strange choice in the two requirements above is necessary, because the NFT contract functions as an escrow for payments and royalties, and, hence, needs to be able to track payments received from buyers and royalties due to recipients, and to associate them with a valid `tokenId`. **[R21]** *For compactness of the input, and since the token meta data might vary from token to token the MUST be a minimal data structure containing:* + ``` /// @param parent is the parent tokenId of the (child) token, and if set to 0 then there is no parent. /// @param canBeParent indicates if a tokenId can have children or not. @@ -307,6 +322,7 @@ This strange choice in the two requirements above is necessary, because the NFT ``` **[R22]** *The `mint` function interface MUST adhere to the definition below:* + ``` /// @dev Function creates one or more new NFTs with its relevant meta data necessary for royalties, and a Royalty Account with its associated met data for `to` address. The tokenId(s) will be automatically assigned (and available on the emitted {IERC-721-Transfer} event). /// @param to is the address to which the NFT(s) are minted @@ -317,22 +333,24 @@ function mint(address to, NFTToken[] memory nfttoken, address tokenType) public ``` **[R23]** *The following business rules for the `mint` function's input data MUST be fulfilled:* -- *The number of tokens to be minted MUST NOT be zero.* -- *`msg.sender` MUST have either the `MINTER_ROLE` or the `CREATOR_Role` identifying the creator of the first NFT.* -- *`to` address MUST NOT be the zero address.* -- *`to` address MUST NOT be a contract, unless it has been whitelisted -- see [Security Considerations](#security-considerations) for more details.* -- *`tokenType` MUST be a token type supported by the contract.* -- *`royaltySplitForItsChildren` MUST be less or equal to 100% or numerical equivalent thereof less any constraints such as platform fees* -- *If the new NFT(s) cannot have children, `royaltySplitForItsChildren` MUST be zero.* -- *If the new NFT(s) has a parent, the parent NFT `tokenId` MUST exist.* -- *The ancestry level of the parent MUST be less than the maximum number of allowed NFT generations, if specified.* -- *The number of allowed children for an NFT to be minted MUST be less than the maximum number of allowed children, if specified.* + +* *The number of tokens to be minted MUST NOT be zero.* +* *`msg.sender` MUST have either the `MINTER_ROLE` or the `CREATOR_Role` identifying the creator of the first NFT.* +* *`to` address MUST NOT be the zero address.* +* *`to` address MUST NOT be a contract, unless it has been whitelisted -- see [Security Considerations](#security-considerations) for more details.* +* *`tokenType` MUST be a token type supported by the contract.* +* *`royaltySplitForItsChildren` MUST be less or equal to 100% or numerical equivalent thereof less any constraints such as platform fees* +* *If the new NFT(s) cannot have children, `royaltySplitForItsChildren` MUST be zero.* +* *If the new NFT(s) has a parent, the parent NFT `tokenId` MUST exist.* +* *The ancestry level of the parent MUST be less than the maximum number of allowed NFT generations, if specified.* +* *The number of allowed children for an NFT to be minted MUST be less than the maximum number of allowed children, if specified.* ### Listing and De-Listing of NFTs for Direct Sales In the sales process, we need to minimally distinguish two types of transactions -- Exchange-mediated sales -- Direct sales + +* Exchange-mediated sales +* Direct sales The first type of transaction does not require that the smart contract is aware of a sales listing since the exchange contract will trigger payment and transfer transactions directly with the NFT contract as the owner. However, for the latter transaction type it is essential, since direct sales are required to be mediated at every step by the smart contract. @@ -343,6 +361,7 @@ Exchange-mediated sales will be discussed when this document discusses payments. In direct sales, authorized user addresses can list NFTs for sale, see the business rules below. **[R25]** *The `listNFT` function interface MUST adhere to the definition below:* + ``` /// @dev Function to list one or more NFTs for direct sales /// @param tokenIds is the array of tokenIds to be included in the listing @@ -351,30 +370,35 @@ In direct sales, authorized user addresses can list NFTs for sale, see the busin function listNFT (uint256[] calldata tokenIds, uint256 price, address tokenType) public virtual returns (bool) ``` + The Boolean return value is `true` for a successful function execution, and `false` for an unsuccessful function execution. **[R26]** *The business rules of the `listNFT` function are as follows:* -- there MUST NOT already be a listing for one or more NFTs in the `listedNFT` mapping of the proposed listing. -- `seller` MUST be equal to `getApproved(tokenId[i])` for all NFTs in the proposed listing. -- `tokenType` MUST be supported by the smart contract. -- `price` MUST be larger than `0`. + +* there MUST NOT already be a listing for one or more NFTs in the `listedNFT` mapping of the proposed listing. +* `seller` MUST be equal to `getApproved(tokenId[i])` for all NFTs in the proposed listing. +* `tokenType` MUST be supported by the smart contract. +* `price` MUST be larger than `0`. **[R27]** *If the conditions in [**[R26]**](#r26) are met, then the NFT sales list MUST be updated.* Authorized user addresses can also remove a direct sale listing of NFTs. **[R28]** *The `removeNFTListing` function interface MUST adhere to the definition below:* + ``` /// @dev Function to de-list one or more NFTs for direct sales /// @param listingId is the identifier of the NFT listing function removeNFTListing (uint256 listingId) public virtual returns (bool) ``` + The Boolean return value is `true` for a successful function execution, and `false` for an unsuccessful function execution. **[R29]** *The business rules of the `removeNFTListing` function below MUST be adhered to:* -- * the registered payment entry MUST be NULL* -- *`msg.sender = getApproved(tokenId)` for the NFT listing* + +* *the registered payment entry MUST be NULL* +* *`msg.sender = getApproved(tokenId)` for the NFT listing* **[R30]** *If the conditions in [**[R29]**](#r29) are met, then the NFT sales listing MUST be removed.* @@ -383,13 +407,15 @@ The Boolean return value is `true` for a successful function execution, and `fal As noted before, a buyer will always pay the NFT contract directly and not the seller. The seller is paid through the royalty distribution and can later request a payout to their wallet. **[R31]** *The payment process requires either one or two steps:* + 1. *For an ERC-20 token* - - *The buyer MUST `approve` the NFT contract for the purchase price, `payment`, for the selected payment token type.* - - *The buyer MUST call the `executePayment` function.* + * *The buyer MUST `approve` the NFT contract for the purchase price, `payment`, for the selected payment token type.* + * *The buyer MUST call the `executePayment` function.* 2. *For a protocol token* - - *The buyer MUST call a payment fallback function with `msg.data` not NULL.* + * *The buyer MUST call a payment fallback function with `msg.data` not NULL.* **[R32]** *For an ERC-20 token type, the required `executePayment` function interface MUST adhere to the definition below*: + ``` /// @dev Function to make a NFT direct sales or exchange-mediate sales payment /// @param receiver is the address of the receiver of the payment @@ -401,23 +427,26 @@ As noted before, a buyer will always pay the NFT contract directly and not the s function executePayment (address receiver, address seller, uint 256[] tokenIds, uint256 payment, string tokenType, int256 trxnType) public virtual nonReentrant returns (bool) ``` + The Boolean return value is `true` for a successful function execution, and `false` for an unsuccessful function execution. **[R33]** *Independent of `trxnType`, the business rules for the input data are as follows:* -- *All purchased NFTs in the `tokenIds` array MUST exist and MUST NOT be burned.* -- *`tokenType` MUST be a supported token.* -- *`trxnType` MUST be set to either `0` (direct sale) or `1` (exchange-mediate sale), or another supported type.* -- *`receiver` MAY be NULL but MUST NOT be the Zero Address.* -- *`seller` MUST be the address in the corresponding listing.* -- *`msg.sender` MUST not be a contract, unless it is whitelisted in the NFT contract.* + +* *All purchased NFTs in the `tokenIds` array MUST exist and MUST NOT be burned.* +* *`tokenType` MUST be a supported token.* +* *`trxnType` MUST be set to either `0` (direct sale) or `1` (exchange-mediate sale), or another supported type.* +* *`receiver` MAY be NULL but MUST NOT be the Zero Address.* +* *`seller` MUST be the address in the corresponding listing.* +* *`msg.sender` MUST not be a contract, unless it is whitelisted in the NFT contract.* In the following, this document will only discuss the differences between the two minimally required transaction types. **[R34]** *For `trxnType = 0`, the payment data MUST to be validated against the listing, based on the following rules:* -- *NFT(s) MUST be listed* -- *`payment` MUST be larger or equal to the listing price.* -- *The listed NFT(s) MUST match the NFT(s) in the payment data.* -- *The listed NFT(s) MUST be controlled by `seller`.* + +* *NFT(s) MUST be listed* +* *`payment` MUST be larger or equal to the listing price.* +* *The listed NFT(s) MUST match the NFT(s) in the payment data.* +* *The listed NFT(s) MUST be controlled by `seller`.* **[R35]** *If all checks in [**[R33]**](#r33), and in [**[R34]**](#r34) for `trxnType = 0`, are passed, the `executePayment` function MUST call the `transfer` function in the ERC-20 contract identified by `tokenType` with `recipient = address(this)` and `amount = payment`.* @@ -445,26 +474,30 @@ Note that since the information for which NFTs the payment was for must be passe **[R43]** *For `trxnType` equal to either '0' or '1', the requirements [**[R33]**](#r33) through [**[R38]**](#r38) MUST be satisfied for the fallback function to successfully execute, otherwise the fallback function MUST `revert`.* **[R44]** *In case of a transaction failure (for direct sales, `trxnType = 0`), or the buyer of the NFT listing changing their mind (for exchange-mediated sales, `trxnType = 1`), the submitted payment MUST be able to revert using the `reversePayment` function where the function interface is defined below:* + ``` /// @dev Definition of the function enabling the reversal of a payment before the sale is complete /// @param paymentId is the unique identifier for which a payment was made /// @param tokenType is the type of payment token used in the payment function reversePayment(uint256 paymentId, string memory tokenType) public virtual returns (bool) ``` + The Boolean return value is `true` for a successful function execution, and `false` for an unsuccessful function execution. Note, `reentrancy` protection through e.g. `nonReentrant` from the Open Zeppelin library is strongly advised since funds are being paid out. **[R45]** *The business rules for the `reversePayment` function are as follows:* -- *There MUST be registered payment for a given `paymentId` and `tokenType`.* -- *`msg.sender` MUST be the buyer address in the registered payment.* -- *The payment amount must be larger than `0`.* -- *The registered payment MUST be removed when the payment has been successfully reverted, otherwise the function must fail.* + +* *There MUST be registered payment for a given `paymentId` and `tokenType`.* +* *`msg.sender` MUST be the buyer address in the registered payment.* +* *The payment amount must be larger than `0`.* +* *The registered payment MUST be removed when the payment has been successfully reverted, otherwise the function must fail.* ### Modified NFT Transfer function This document adheres to the ERC-721 interface format for the `safeTransferFrom` function as given below: + ``` function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory _data) external virtual override ``` @@ -472,6 +505,7 @@ function safeTransferFrom(address from, address to, uint256 tokenId, bytes memor Note, that the input parameters must satisfy several requirements for the NFT(s) to be transferred AFTER royalties have been properly distributed. Note also, that the ability to transfer more than one token at a time is required. However, the standard interface only allows one token to be transferred at a time. In order to remain compliant with the ERC-721 standard, this document uses `tokenId` only for the first NFT to be transferred. All other transfer relevant data is encoded in `_data`. The high-level requirements are as follows: + * The payment parameters of the trade encoded in `_data` must be validated. * The seller and the sold NFT token(s) must exist, and the seller must be the owner of the token. * `msg.sender` must be the seller address or an approved address. @@ -485,6 +519,7 @@ Also, note that in order to avoid royalty circumvention attacks, there is only o This can be achieved through for example a `revert` statement in an `override` function. **[R47]** *The requirements on input parameters of the function are as follows*: + * *`from` MUST not be `address(0)`.* * *`from` MUST be the owner or `approved` for `tokenId` and the other tokens included in `_data`.* * *`from` MUST not be a smart contract unless whitelisted.* @@ -497,6 +532,7 @@ Note, that in the context of this document only the scenario where the calling c Turning to the `_data` object. **[R48]** *The `_data` object MUST minimally contain the following payment parameters:* + * *Seller Address as `address`.* * *Buyer Address as `address`.* * *Receiver Address as `address.* @@ -507,6 +543,7 @@ Turning to the `_data` object. * *blockchain ID, `block.chainid`, of the underlying blockchain.* **[R49]** *The following business rules MUST be met for the payment data in '_data':* + * *`seller == from`.* * *`tokenId[0] == tokenId`.* * *Each token in `_tokenId` has an associated Royalty Account.* @@ -529,6 +566,7 @@ The approach to distributing royalties is to break down the hierarchical structu Note, that the distribution function assumes that the payment made is for ALL tokens in the requested transfer. That means, that `payment` for the distribution function is equally divided between all NFTs included in the payment. **[R50]** *The `distributePayment` function interface MUST adhere to the definition below: + ``` /// @dev Function to distribute a payment as royalties to a chain of Royalty Accounts /// @param tokenId is a tokenId included in the sale and used to look up the associated Royalty Account @@ -536,6 +574,7 @@ Note, that the distribution function assumes that the payment made is for ALL to function distributePayment (uint256 tokenId, uint265 payment) internal virtual returns (bool) ``` + The Boolean return value is `true` for a successful function execution, and `false` for an unsuccessful function execution. As mentioned before, the internal `distributePayment` function is called within the modified `safeTransferFrom` function. @@ -543,6 +582,7 @@ As mentioned before, the internal `distributePayment` function is called within Note, that it is necessary to multiply two `uint256` numbers with each other -- the payment amount with the royalty split percentage expressed as a whole number e.g. `10000 = 100%`. And then divide the result by the whole number representing `100%` in order to arrive at the correct application of the royalty split percentage to the payment amount. This requires careful treatment of numbers in the implementation to prevent issues such as buffer over or under runs. **[R51]** *The processing logic of `distributePayment` function MUST be as follows:* + * *Load the Royalty Account (`RA`) and associated Royalty Sub Accounts using the passed `tokenId`.* * *For each Royalty Sub Account in `RA` apply the following rules:* * *If a Royalty Sub Account in `RA` has `isIndividual` set to `true` then* @@ -562,6 +602,7 @@ Note, that it is necessary to multiply two `uint256` numbers with each other -- In order to simplify the ownership transfer, first the approved address -- the non-contract NFT owner --, `from`, is paid out its share of the royalties. And then the Royalty Sub Account is updated with the new owner, `to`. This step repeats for each token to be transferred. **[R52]** *The business rules are as follows:* + * *the internal version of the`royaltyPayOut` function MUST pay out the entire royalty balance of the Royalty Sub Account owned by the `from` address to the `from` address.* * *the Royalty Sub Account MUST only be updated with the new owner only once the payout function has successfully completed and the `royaltybalance = 0`.* @@ -586,6 +627,7 @@ Only after the real ownership of the NFT, the approved address, has been updated There are two versions of the payout function -- a `public` and an `internal` function -- depending on whether there is a payout during a purchase, or a payout is requested by a Royalty Sub Account owner. **[R55]** *The public `royaltyPayOut` function interface MUST adhere to the definition below:* + ``` /// @dev Function to payout a royalty payment /// @param tokenId is the identifier of the NFT token @@ -595,11 +637,13 @@ There are two versions of the payout function -- a `public` and an `internal` fu function royaltyPayOut (uint256 tokenId, address RAsubaccount, address payable payoutAccount, uint256 amount) public virtual nonReentrant returns (bool) ``` + The Boolean return value is `true` for a successful function execution, and `false` for an unsuccessful function execution. Note, that the function has `reentrancy` protection through `nonReentrant` from the Open Zeppelin library since funds are being paid out. **[R56]** *The input parameters of the `royaltyPayOut` function MUST satisfy the following requirements:* + * *`msg.sender == RAsubaccount`.* * *`tokenId` must exist and must not be burned.* * *`tokenId` must be associated with a Royalty Account.* @@ -608,15 +652,18 @@ Note, that the function has `reentrancy` protection through `nonReentrant` from * *`amount <= royaltybalance` of the Royalty Sub Account, `RAsubaccount.*` **[R57]** *The internal `_royaltyPayOut` function interface MUST adhere to the definition below*: + ``` function _royaltyPayOut (uint256 tokenId, address RAsubaccount, address payable payoutAccount, uint256 amount) public virtual returns (bool) ``` **[R58]** *The internal `_royaltyPayOut` function MUST perform the following actions: + * *send the payment to the `payoutaccount`.* * *update the `royaltybalance` of the `RAsubaccount` of the Royalty Account upon successful transfer.* **[R59]** *The following steps MUST be taken to send out a royalty payment to its recipient:* + * *find the Royalty Sub Account.* * *extract `tokenType` from the Royalty Sub Account.* * *based on the token type send to the `payoutAccount` either* @@ -630,7 +677,7 @@ Royalties for NFTs is at its core a distribution licensing problem. A buyer obta In order to solve for the complicated inheritance problem, this proposal design breaks down the recursive problem of the hierarchy first into a tree of depth N. And the further breaks down the tree structure into N separate problems, one for each layer. This design allows one to traverse the tree from its lowest level upwards to its root most efficiently. This is achieved with the design for the `distributePayment` function and the NFT data structures allowing for the tree structure e.g. `ancestry`,`royaltyAccount`, `RAsubaccount`. -In order to avoid massive gas costs during the payout of royalties, possibly exceeding block gas limits for large royalty trees, the design needed to create a royalty accounting system to maintain royalty balances for recipients as done with the `royaltyAccount`, 'RAsubaccount' data structures and the associated CRUD operations, as well as require that royalty payouts are done by indvidual and by request, only, as is achieved with the `royaltyPayout` function design. +In order to avoid massive gas costs during the payout of royalties, possibly exceeding block gas limits for large royalty trees, the design needed to create a royalty accounting system to maintain royalty balances for recipients as done with the `royaltyAccount`, 'RAsubaccount' data structures and the associated CRUD operations, as well as require that royalty payouts are done by individual and by request, only, as is achieved with the `royaltyPayout` function design. Furthermore, the design had to ensure that in order to account for and payout royalties the smart contract must be in the "know" of all buying and selling of an NFT including the exchange of monies. This buying and selling can be either direct through the NFT contract or can be exchange-mediated as is most often the case today -- which is a centralizing factor! The chosen design for purchasing is accounting for those two modes. @@ -639,6 +686,7 @@ Keeping the NFT contract in the "know" at the beginning of the purchase process The design needed to avoid royalty circumvention during the purchase process, therefore, the NFT must be kept in the "know", a buyer will always have to pay the NFT contract directly and not the seller for both purchasing modes. The seller is subsequently paid through the royalty distribution function in the NFT contract. As a consequence, and a key design choice, and to stay compliant with ERC-721, the NFT contract must be the owner of the NFT, and the actual owner is an `approved` address. The specification design also needed to account for that the payment process depends on whether the payment is received in ETH or an ERC-20 token: + * ERC-20 Token 1. The Buyer must `approve` the NFT contract for the purchase price, `payment` for the selected payment token (ERC-20 contract address). 2. For an ERC-20 payment token, the Buyer must then call the `executePayment` in the NFT contract -- the ERC-20 is not directly involved. @@ -658,37 +706,41 @@ An NFT with a royalty account can be burned. However, several things have to be ## Backwards Compatibility + This EIP is backwards compatible to the ERC-721 standard introducing new interfaces and functionality but retaining the core interfaces and functionality of the ERC-721 standard. ## Test Cases + A full test suite is part of the reference implementation. ## Reference Implementation + The Treetrunk reference implementation of the standard can be found in the public treetrunkio Github repo under treetrunk-nft-reference-implementation. ## Security Considerations + Given that this EIP introduces royalty collection, distribution, and payouts to the ERC-721 standard, the number of attack vectors increases. The most important attack vector categories and their mitigation are discussed below: -- **Payments and Payouts**: - - Reentrancy attacks are mitigated through a reentrancy protection on all payment functions. See for example the Open Zeppelin reference implementation . - - Payouts from unauthorized accounts. Mitigation: Royalty Sub Accounts require at least that `msg.sender` is the Royalty Sub Account owner. - - Payments could get stuck in the NFT contract if the `executePayment` function fails. Mitigation: For exchange-mediated sales, a buyer can always reverse a payment with `reversePayment` if the `executePayment` function fails. For direct sales, `reversePayment` will be directly triggered in the `executePayment` function. -- **Circumventing Royalties**: - - Offchain Key exchanges - - Exchanging a private key for money off chain can not be prevented in any scenario. - - Smart Contract Wallets as NFT owners - - A Smart Contract Wallet controlled by multiple addresses could own an NFT and the owners could transfer the asset within the wallet with an off chain money exchange. Mitigation: Prohibit that Smart Contracts can own an NFT unless explicitly allowed to accommodate special scenarios such as collections. - - Denial of Royalty Disbursement - - An attacker who has purchased one or more NFTs in a given generation of an NFT family can cause out of gas errors or run time errors for the contract, if they add many spurious royalty sub-accounts with very low royalty split percentages, and then mint more prints of those purchased NFTs, and then repeat that step until the set `maxGeneration` limit is reached. An NFT trade at the bottom of the hierarchy will then require a lot of code cycles because of the recursive nature of the royalty distribution function. Mitigation: Limit the number of royalty sub-accounts per NFT and impose a royalty split percentage limit. - - Following the same approach as above but now targeting the `addListNFT` function, an attacker can force an out of gas error or run time errors in the `executePayment` function by listing many NFTs at a low price, and then performing a purchase from another account. Mitigation: Limit the number of NFTs that can be included in one listing. - - The creator of the NFT family could set the number of generations too high such that the royalty distribution function could incur and out of gas or run time error because of the recursive nature of the function. Mitigation: Limiting the `maxNumberGeneration` by the creator. - - General Considerations: The creator of an NFT family must carefully consider the business model for the NFT family and then set the parameters such as maximum number of generations, royalty sub-accounts, number of prints per print, number of NFTs in a listing, and the maximum and minimum royalty split percentage allowed. - -- **Phishing Attacks** - - NFT phishing attacks often target the `approve` and `setApprovalForAll` functions by tricking owners of NFTs to sign transactions adding the attacker account as approved for one or all NFTs of the victim. Mitigation: This contract is not vulnerable to these type of phishing attacks because all NFT transfers are sales, and the NFT contract itself is the owner of all NFTs. This means that transfers after a purchase are achieved by setting the new owner in the `_approve` function. Calling the public `approve` function will cause the function call to error out because `msg.sender` of the malicious transaction cannot be the NFT owner. - - NFT phishing attack targeting the `addListNFT` function to trick victim to list one or more NFTs at a very low price and the attacker immediately registering a payment, and executing that payment right away. Mitigation: Implement a waiting period for a purchase can be affected giving the victim time to call the `removeListNFT` function. In addition, an implementer could require Two-Factor-Authentication either built into the contract or by utilizing an authenticator app such as Google Authenticator built into a wallet software. +* **Payments and Payouts**: + * Reentrancy attacks are mitigated through a reentrancy protection on all payment functions. See for example the Open Zeppelin reference implementation . + * Payouts from unauthorized accounts. Mitigation: Royalty Sub Accounts require at least that `msg.sender` is the Royalty Sub Account owner. + * Payments could get stuck in the NFT contract if the `executePayment` function fails. Mitigation: For exchange-mediated sales, a buyer can always reverse a payment with `reversePayment` if the `executePayment` function fails. For direct sales, `reversePayment` will be directly triggered in the `executePayment` function. +* **Circumventing Royalties**: + * Offchain Key exchanges + * Exchanging a private key for money off chain can not be prevented in any scenario. + * Smart Contract Wallets as NFT owners + * A Smart Contract Wallet controlled by multiple addresses could own an NFT and the owners could transfer the asset within the wallet with an off chain money exchange. Mitigation: Prohibit that Smart Contracts can own an NFT unless explicitly allowed to accommodate special scenarios such as collections. + * Denial of Royalty Disbursement + * An attacker who has purchased one or more NFTs in a given generation of an NFT family can cause out of gas errors or run time errors for the contract, if they add many spurious royalty sub-accounts with very low royalty split percentages, and then mint more prints of those purchased NFTs, and then repeat that step until the set `maxGeneration` limit is reached. An NFT trade at the bottom of the hierarchy will then require a lot of code cycles because of the recursive nature of the royalty distribution function. Mitigation: Limit the number of royalty sub-accounts per NFT and impose a royalty split percentage limit. + * Following the same approach as above but now targeting the `addListNFT` function, an attacker can force an out of gas error or run time errors in the `executePayment` function by listing many NFTs at a low price, and then performing a purchase from another account. Mitigation: Limit the number of NFTs that can be included in one listing. + * The creator of the NFT family could set the number of generations too high such that the royalty distribution function could incur and out of gas or run time error because of the recursive nature of the function. Mitigation: Limiting the `maxNumberGeneration` by the creator. + * General Considerations: The creator of an NFT family must carefully consider the business model for the NFT family and then set the parameters such as maximum number of generations, royalty sub-accounts, number of prints per print, number of NFTs in a listing, and the maximum and minimum royalty split percentage allowed. +* **Phishing Attacks** + * NFT phishing attacks often target the `approve` and `setApprovalForAll` functions by tricking owners of NFTs to sign transactions adding the attacker account as approved for one or all NFTs of the victim. Mitigation: This contract is not vulnerable to these type of phishing attacks because all NFT transfers are sales, and the NFT contract itself is the owner of all NFTs. This means that transfers after a purchase are achieved by setting the new owner in the `_approve` function. Calling the public `approve` function will cause the function call to error out because `msg.sender` of the malicious transaction cannot be the NFT owner. + * NFT phishing attack targeting the `addListNFT` function to trick victim to list one or more NFTs at a very low price and the attacker immediately registering a payment, and executing that payment right away. Mitigation: Implement a waiting period for a purchase can be affected giving the victim time to call the `removeListNFT` function. In addition, an implementer could require Two-Factor-Authentication either built into the contract or by utilizing an authenticator app such as Google Authenticator built into a wallet software. Besides the usage of professional security analysis tools, it is also recommended that each implementation performs a security audit of its implementation. ## Copyright + Copyright and related rights waived via [CC0](../LICENSE.md).