├── .DS_Store ├── .gitignore ├── .prettierrc.json ├── .secret ├── .solcover.js ├── LICENSE ├── README.md ├── README_Chinese_Version ├── Readme_Chinese_Version.docx ├── contracts ├── Migrations.sol ├── PaymentModule.sol ├── RoyaltyBearingToken.sol ├── RoyaltyBearingTokenStorage.sol ├── RoyaltyModule.sol ├── StorageStructure.sol └── test │ ├── SomeERC20.sol │ └── faucet.sol ├── coverage.json ├── docs ├── mythXPro-Report-PaymentModule-03-22-2022.pdf ├── mythXPro-Report-RoyaltyBearingTokenModule-03-22-2022.pdf └── mythXPro-Report-RoyaltyModule-03-22-2022.pdf ├── migrations ├── 1_initial_migration.js ├── 2_deploy_contracts.js └── 3_faucet.js ├── package.json ├── test ├── .gitkeep ├── Basic Tests ├── coverage │ ├── PaymentModule.test.js │ ├── RoyaltyBearingToken.test.js │ └── RoyaltyModule.test.js ├── royaltySplit.test.js ├── royaltySplitUpdate.test.js ├── scenarios │ └── sellWithZeroRoyalty.test.js ├── stressBatchMint.test.js ├── stressDeepRoyalty.test.js ├── transferByERC20_trxntype_0.test.js ├── transferByERC20_trxntype_1.test.js ├── transferByETH_trxnttype_0.test.js └── transferByETH_trxnttype_1.test.js └── truffle-config.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/treetrunkio/treetrunk-nft-reference-implementation/a28747e0a9d3669b0f2d8faac44e61ab80b6e0ce/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | build 3 | node_modules 4 | .coverage* 5 | package-lock.json 6 | .vscode 7 | package.json 8 | coverage.json 9 | truffle-config.js 10 | coverage/ -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 200, 3 | "trailingComma": "all", 4 | "tabWidth": 4, 5 | "semi": true, 6 | "singleQuote": true 7 | } 8 | -------------------------------------------------------------------------------- /.secret: -------------------------------------------------------------------------------- 1 | hen token assume same own mail afraid pet leader short system reward -------------------------------------------------------------------------------- /.solcover.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | norpc: false, 3 | port: 8555, 4 | buildDirPath: '/build/contracts', 5 | skipFiles: ['Migrations.sol','test/faucet.sol','test/SomeERC20.sol'], 6 | }; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | The MIT License (MIT) 204 | 205 | Copyright (c) 2018 Truffle 206 | 207 | Permission is hereby granted, free of charge, to any person obtaining a copy 208 | of this software and associated documentation files (the "Software"), to deal 209 | in the Software without restriction, including without limitation the rights 210 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 211 | copies of the Software, and to permit persons to whom the Software is 212 | furnished to do so, subject to the following conditions: 213 | 214 | The above copyright notice and this permission notice shall be included in all 215 | copies or substantial portions of the Software. 216 | 217 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 218 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 219 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 220 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 221 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 222 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 223 | SOFTWARE. 224 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Reference Implementation of the proposed Royalty Bearing NFT Smart Contract Standard from Treetrunk.io 2 | 3 | ## Abstract 4 | The proposal directly connects NFTs and royalties in a smart contract architecture extending the ERC721 standard, with the aim of precluding central authorities from manipulating or circumventing payments to those who are legally entitled to them. 5 | 6 | The proposal builds upon the [OpenZeppelin Smart Contract Toolbox](https://github.com/OpenZeppelin/openzeppelin-contracts) architecture, and extends it to include royalty account management (CRUD), royalty balance and payments management, simple trading capabilities -- Listing/Unlisting/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. 7 | 8 | ## Motivation 9 | The management of royalties is an age-old problem characterized by complex contracts, opaque management, plenty of cheating and fraud. 10 | 11 | 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. 12 | 13 | In the example below, the artist who created the original is eligible to receive proceeds from every sale, and resale, of a print. 14 | 15 | ![Fig1](https://i.imgur.com/Py6bYQw.png) 16 | 17 | 18 | The basic concept for hierarchical royalties utilizing the above "ancestry concept" is demonstrated in the figure below. 19 | 20 | ![Fig2](https://i.imgur.com/7MtWzBV.png) 21 | 22 | 23 | In order to solve for the complicated inheritance problem, this proposal breaks down the recursive problem of the hierarchy tree of depth N into N separate problems, one for each layer. This allows us to traverse the tree from its lowest level upwards to its root most efficiently. 24 | 25 | 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. 26 | 27 | ## Specification Outline 28 | 29 | This proposal introduces several new concepts as extensions to the ERC721 standard: 30 | * **Royalty Account (RA)** 31 | * 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. 32 | * **Account Type** 33 | * This specifies if an RA Sub Account belongs to an individual (user) or is another RA. If there is another RA as an RA Sub Account, the allocated balance needs to be reallocated to the Sub Accounts making up the referenced RA. 34 | * **Royalty Split** 35 | * The percentage each Sub Account receives based on a sale of an NFT that is associated with an RA 36 | * **Royalty Balance** 37 | * The royalty balance associated with an RA 38 | * **Sub Account Royalty Balance** 39 | * The royalty balance associated to each RA Sub Account. Note that only individual accounts can carry a balance that can be paid out. That means that if an RA Sub Account is an RA, its final Sub Account balance must be zero, since all RA balances must be allocated to individual accounts. 40 | * **Token Type** 41 | * Token Type is given as either ETH or the symbol of the supported ERC 20/223/777 tokens such as `DAI` 42 | * **Asset ID** 43 | * This is the `tokenId` the RA belongs to. 44 | * **Parent** 45 | * This indicates which `tokenId` is the immediate parent of the `tokenId` to which an RA belongs. 46 | 47 | ### Data Structures 48 | 49 | In order to create an interconnected data structure linking NFTs to RAs that is search optimized requires the following additions to the global data structures of an ERC721: 50 | 51 | * Adding structs for a Royalty Account and associated Royalty Sub Accounts to establish the concept of a Royalty Account with sub accounts. 52 | * Defining an `raAccountId` as the keccak256 hash of `tokenId`, the actual `owner` address, and the current block number, `block.blocknumber` 53 | * Mapping a `tokenId` to an `raAccountID` in order to connect an RA `raAccountId` to a `tokenId` 54 | * Mapping the `raAccountID` to a `RoyaltyAccount` in order to connect the account identifier to the actual account. 55 | * An `ancestry` mapping of the parent-to-child NFT relationship 56 | * A mapping of supported token types to their origin contracts and last validated balance (for trading and royalty payment purposes) 57 | * A mapping with a struct for a registered payment to be made in the `executePayment` function and validated in `safeTransferFrom`. This is sufficient, because a payment once received and distributed in the `safeTransferFrom` function will be removed from the mapping. 58 | * A mapping for listing NFTs to be sold 59 | 60 | ### Royalty Account Functions 61 | 62 | Definitions and interfaces for the Royalty Account RUD (Read-Update-Delete) functions. Because the RA is created in the minting function, there is no need to have a function to create a royalty account separately. 63 | 64 | ### Minting of a royalty bearing NFT 65 | 66 | When an NFT is minted, an RA must be created and associated with the NFT and the NFT owner, and, if there is an ancestor, with the ancestor's RA. To this end the specification utilizes the `_safemint` function in a newly defined `mint` function and applies various business rules on the input variables. 67 | 68 | ### Listing NFTs for Sale and removing a listing 69 | 70 | Authorized user addresses can list NFTs for sale for non-exchange mediated NFT purchases. 71 | 72 | ### Payment Function from Buyer to Seller 73 | 74 | 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. 75 | 76 | The payment process depends on whether the payment is received in ETH or an ERC 20 token: 77 | * ERC 20 Token 78 | 1. The Buyer must `approve` the NFT contract for the purchase price, `payment` for the selected payment token (ERC20 contract address). 79 | 2. For an ERC20 payment token, the Buyer must then call the `executePayment` in the NFT contract -- the ERC20 is not directly involved. 80 | * For a non-ERC20 payment, the Buyer must send a protocol token (ETH) to the NFT contract, and is required to send `msg.data` encoded as an array of purchased NFTs `uint256[] tokenId`. 81 | 82 | ### Modified NFT Transfer function including required Trade data to allocate royalties 83 | 84 | 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. 85 | 86 | The proposal defines: 87 | * Input parameter validation 88 | * Payment Parameter Validation 89 | * Distributing Royalties 90 | * Update RA ownership with payout 91 | * Transferring Ownership of the NFT 92 | * Removing the Payment entry in `registeredPayment` after successful transfer 93 | 94 | ##### Distributing Royalties 95 | 96 | The approach to distributing royalties is to break down the hierarchical structure of interconnected RAs into layers and then process one layer at time, where each relationship between a token and its ancestor is utilized to traverse the RA chain until the root ancestor and associated RA is reached. 97 | 98 | ### Paying out Royalties to the NFT owner -- `from` address in `safeTransferFrom` function 99 | 100 | This is the final part of the proposal. 101 | 102 | There are two versions of the payout function -- a `public` function and an `internal` function. 103 | 104 | The public function has the following interface: 105 | ``` 106 | function royaltyPayOut (uint256 tokenId, address _RAsubaccount, address payable _payoutaccount, payable uint256 _amount) public virtual nonReentrant returns (bool) 107 | ``` 108 | 109 | 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](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/security/ReentrancyGuard.sol), because funds are being payed out. 110 | 111 | #### Sending a Payout Payment 112 | 113 | The following steps need to be taken: 114 | * find the RA Sub Account based on `RAaccount` and the `subaccountPos` and extract the balance 115 | * extract `tokentype` from the Sub Account 116 | * based on the token type, send the payout payment (not exceeding the available balance) 117 | 118 | ## Installation & Tests 119 | 120 | Follow the steps below to run the smart contracts test and generate coverage reports: 121 | 122 | - Fork this repo 123 | - [Install NodeJS](https://nodejs.org/en/download/) 124 | - [Install a NodeJS Lite Server](https://www.npmjs.com/package/lite-server) 125 | - [Install Truffle](https://trufflesuite.com/docs/truffle/getting-started/installation.html) 126 | - [Install Truffle Assertions Library](https://www.npmjs.com/package/truffle-assertions) 127 | - [Install Truffle Contract Size Library](https://www.npmjs.com/package/truffle-contract-size) 128 | - [Select & Install an Ethereum client of your choice for local testing only](https://trufflesuite.com/docs/truffle/reference/choosing-an-ethereum-client) 129 | - [Install Prettier](https://www.npmjs.com/package/prettier) and its [Solidity Plugin](https://www.npmjs.com/package/prettier-plugin-solidity) 130 | - [Install Solidity Test Coverage](https://www.npmjs.com/package/solidity-coverage) 131 | - [Install the Eth Gas Reporter](https://www.npmjs.com/package/eth-gas-reporter) 132 | - [Install the Open Zeppelin Contract Module](https://www.npmjs.com/package/@openzeppelin/contracts) 133 | - [Install the ABDK Numerical Solidity Libraries](https://www.npmjs.com/package/abdk-libraries-solidity) 134 | - Run Migrations 135 | - Run the Truffle tests in the different test folders 136 | 137 | Note that we are pointing to the Polygon Mumbai Test network in `truffle-config.js`. Please, adjust this if you want a different network. 138 | 139 | ### Coverage Report 140 | To generate a coverage report, this command needs to be executed: 141 | ```sh 142 | truffle run coverage 143 | ``` 144 | The generated HTML report can be found in the `/coverage` folder. 145 | 146 | ### Smart Contracts Test 147 | The following commands should be performed to run a specific test: 148 | ```sh 149 | truffle test test/transferByERC20_trxntype_0.test.js 150 | ``` 151 | or to run a group of tests: 152 | ```sh 153 | truffle test test/transfer* 154 | ``` 155 | 156 | ## Security Testing 157 | 158 | The MythX Pro deep analysis security reports of the contracts can be found [here](https://github.com/treetrunkio/treetrunk-nft-reference-implementation/blob/main/docs). 159 | 160 | ## Licensing 161 | 162 | This repo is licensed under [Apache 2.0](https://github.com/treetrunkio/treetrunk-nft-reference-implementation/blob/main/LICENSE). 163 | 164 | ## Authors 165 | - Andreas Freund (@Therecanbeonlyone1969) 166 | - Alexander Pyatakov (@Pyatakov) 167 | - Volodymyr Shvets (@vshvets-bc) 168 | 169 | ## Contact 170 | 171 | andreas.freund@treetrunk.io -------------------------------------------------------------------------------- /README_Chinese_Version: -------------------------------------------------------------------------------- 1 | ===== 2 | Treetrunk.io 版税友好NFT智能合约标准 3 | ===== 4 | 摘要 5 | -------- 6 | 该解决方案在扩展ERC-721标准的智能合约框架中直接链接了NFT和版税,以防止链上核心结构操纵或规避版税享有者的合法收入。 7 | 8 | 该提案建立在OpenZeppelin智能合约工具箱架构的基础上,并将其扩展到包括版税账户管理(CRUD)、版税余额和支付管理、 9 | 简单的交易能力如发行/取消发行/购买等功能--以及追踪交易所交易的功能。版税管理功能支持分层树状结构(在此称为版税树), 10 | 通过逻辑上连接原始NFT迂衍生艺术品,并以递归形式允许衍生品再创作。 11 | 12 | 动机 13 | ------- 14 | 难以实装合约、管理模棱两可、存在大量欺诈事件等问题一直是NFT版税分配的阻碍。 15 | 16 | 上述情况对于结构性版税来说尤其如此,在这种情况下,一种或多种资产来自于原始资产,如一幅原画的印刷品,或一首歌曲被用于创作另一首歌曲,或发行权和报酬通过一系列关联公司管理。 17 | 18 | 在下面的例子中,创作原作的艺术家有资格从每一次印刷品的销售和转售中获得收益。 19 | 20 | 下图展示了利用上述阶梯版税机制的基本概念: 21 | 22 | 为了解决复杂的继承问题,本解决方案将N层树状结构的递归问题分解为N个独立的问题。这种结构能够最有效地实行底层向上的遍历问题。 23 | 24 | 这使得创作者和原作艺术品分销者能够从再创作过程中获取收入,除却原本内在价值外,NFT价值因持续现金流的存在而进一步提升。 25 | 26 | 合约概述 27 | ------- 28 | 这一解决方案,引入了几个新概念,作为ERC721标准的扩展。 29 | - 版税账户(Royalty Account,RA) 30 | 版税账户通过NFT特定tokenId与之相连,并将其他个人账户或版税账户作为子账户。版税账户通过账户标识符进行识别。 31 | - 账户类型(Account Type) 32 | 账户类型用于指明RA子账户属于个人拥护还是另外一个RA。若另一个RA作为子账户,则分配的余额需按照规则再分配给下辖的子账户。 33 | - 版税分成(Royalty Split) 34 | 每个子账户在销售与RA相关的NFT时获得的百分比。 35 | - 版税余额(Royalty Balance) 36 | 与RA相关的版税余额 37 | - 子账户版税余额(Sub Account Royalty Balance) 38 | 与每个RA子账户相关的版税余额。注意,只有个人账户才有可以用于支付的余额,当一个RA的子账户中有另一个RA时,其最终的子账户余额必须是0。 39 | - 代币类型(Token Type) 40 | 代币类型以ETH或支持ERC 20/23/777的代币,如DAI。 41 | - 资产ID(Asset ID) 42 | 该RA所属的tokenId 43 | - 父级(Parent) 44 | 这表明哪个tokenId是RA所属的tokenId的直接父级。 45 | 46 | 数据结构 47 | ------- 48 | 为了创建一个连接NFT和RA的互联数据结构,并进行搜索优化,需要对ERC721的全局数据结构进行以下补充: 49 | 50 | 1. 为版税账户和相关的版税子账户添加数据结构,以建立版税账户与子账户的概念。 51 | 52 | 2. 将raAccountId定义为tokenId的keccak256哈希值、实际所有者地址和当前区块编号block.blocknumber。 53 | 54 | 3. 将一个tokenId映射到raAccountID,以便将RA的raAccountId与tokenId连接起来 55 | 56 | 4. 将raAccountID映射到RoyaltyAccount,以便将账户标识符与实际账户联系起来。 57 | 58 | 5. 继承关系映射图谱 59 | 60 | 6. 支持的代币类型与它们的起源合同和最后验证的余额的映射(用于交易和版税支付) 61 | 62 | 7. 一个带有struct的映射,用于在executePayment函数中进行注册付款,并在safeTransferFrom中进行验证。其底层原理是,一旦收到付款,并调用safeTransferFrom函数进行分配,将立刻从映射中删除 63 | 64 | 8. 待售的NFT图谱映射 65 | 66 | 版税账户功能 67 | ------- 68 | 版税账户RUD(读取-更新-删除)函数的定义和接口。因为RA是在造币功能中创建的,所以没有必要单独设立一个创建版税账户的功能。 69 | 70 | Mint支持版税的NFT 71 | ------- 72 | 当一个NFT被铸造时,必须创建一个RA并与NFT和NFT所有者相关联,如果父账户存在,则与父级RA相关联。为此,本规范在一个新定义的mint函数中利用_safemint函数,通过更改函数输入,实现上述规则。 73 | 74 | 列出待售 NFT 并删除列表 75 | ------- 76 | 授权用户地址可以列出非交易所中介 NFT 购买的待售 NFT。 77 | 78 | 从买家到卖家的付款功能 79 | -------- 80 | 为避免规避版税,买方始终直接向 NFT 合约而非卖方付款。卖家通过版税分配获得报酬,之后可以要求付款。 81 | 82 | 付款流程取决于付款媒介是 ETH 还是 ERC 20 代币: 83 | 84 | 如果是ERC 20代币,买方必须批准购买价格的 NFT 合同,支付所选支付代币(ERC20 合同地址)。对于 ERC20 支付代币,买方必须调用 NFT 合约中的 不直接涉及到ERC20的executePayment函数。对于非 ERC20 支付,买方必须向 NFT 合约发送协议令牌(ETH),并且需要发送编码为购买的 NFT uint256[] tokenId 数组的 msg.data。 85 | 重载的 NFT 传输功能,包括分配版税所需的交易数据 86 | ------- 87 | 输入参数必须满足 NFT 的几个要求,以便在版税适当分配后进行转移。此外,还考虑了一次转移多个token的能力。 88 | 该合约中定义了: 89 | - 输入参数验证 90 | - 支付参数验证 91 | - 分配版税 92 | - 用支出更新 RA 所有权 93 | - 转移 NFT 的所有权 94 | - 转账成功后移除registeredPayment中的Payment条目 95 | 版税分配 96 | -------- 97 | 分配版税的方法是将相互关联的 RA 的层次结构分解成层,然后每次处理一层,其中每个token与其父节点之间的关系遍历 RA 链,直到到达根节点和关联的 RA . 98 | 99 | safeTransferFrom 函数中的地址向 NFT 所有者支付版税 100 | ------- 101 | 这是解决方案的最后一部分。 102 | 103 | 支付函数有两个版本——公共函数和内部函数。 104 | 105 | Public 函数具有如下接口: 106 | 107 | function royaltyPayOut (uint256 tokenId, address _RAsubaccount, address payable _payoutaccount, payable uint256 _amount) public virtual nonReentrant returns (bool) 108 | 109 | 其中我们只需要 tokenId、RA 子账户地址、所有者 _RAsubaccount 以及要支付的金额 _amount。注意,因为资金处于”支付”状态吗,因此该函数具有nonReentrant保护。 110 | 111 | 发送付款 112 | 113 | 需要采取以下步骤: 114 | 115 | - 根据RAaccount和subaccountPos找到RA子账户并提取余额 116 | 117 | - 从子账户中提取token类型 118 | 119 | - 根据token类型,发送payout支付(不超过可用余额) 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /Readme_Chinese_Version.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/treetrunkio/treetrunk-nft-reference-implementation/a28747e0a9d3669b0f2d8faac44e61ab80b6e0ce/Readme_Chinese_Version.docx -------------------------------------------------------------------------------- /contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | contract Migrations { 5 | address public owner = msg.sender; 6 | uint256 public last_completed_migration; 7 | 8 | modifier restricted() { 9 | require( 10 | msg.sender == owner, 11 | "This function is restricted to the contract's owner" 12 | ); 13 | _; 14 | } 15 | 16 | function setCompleted(uint256 completed) public restricted { 17 | last_completed_migration = completed; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /contracts/PaymentModule.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache 2.0 2 | pragma solidity 0.8.10; 3 | 4 | import '@openzeppelin/contracts/access/Ownable.sol'; 5 | import './StorageStructure.sol'; 6 | import 'abdk-libraries-solidity/ABDKMathQuad.sol'; 7 | 8 | //PaymentModel is mainly about 2 parts 9 | //1. NFT Listing 10 | //2. Payment & Transaction Data 11 | 12 | contract PaymentModule is StorageStructure, Ownable { 13 | mapping(uint256 => RegisteredPayment) private registeredPayment; //A mapping with a struct for a registered payment 14 | mapping(uint256 => ListedNFT) private listedNFT; //A mapping for listing NFTs to be sold 15 | mapping(uint256 => bool) private tokenLock; // lock listed token for sure one time list 16 | 17 | uint256[] private listedNFTList; // List of all listed NFT 18 | 19 | uint256 private _maxListingNumber; //Max token count int listing 20 | 21 | constructor(address owner, uint256 maxListingNumber) { 22 | transferOwnership(owner); 23 | require(maxListingNumber > 0, 'Max number must be > 0'); 24 | _maxListingNumber = maxListingNumber; 25 | } 26 | 27 | function updatelistinglimit(uint256 maxListingNumber) public onlyOwner returns (bool) { 28 | require(maxListingNumber > 0, 'Max number must be > 0'); 29 | _maxListingNumber = maxListingNumber; 30 | return true; 31 | } 32 | 33 | 34 | // NFT Listing 35 | // addListNFT requires seller provide their wallet address, tokenId, price of the token, and the type of the token. 36 | // existsInListNFT returns true there the address of the seller exist in the list 37 | // removeListNFT 38 | 39 | 40 | function addListNFT( 41 | address seller, 42 | uint256[] calldata tokenIds, 43 | uint256 price, 44 | string calldata tokenType 45 | ) public virtual onlyOwner { 46 | require(price > 0, 'Zero Price not allowed'); 47 | require(!existsInListNFT(tokenIds), 'Already exists'); 48 | require(tokenIds.length <= _maxListingNumber, 'Too many NFTs listed'); 49 | listedNFT[tokenIds[0]] = ListedNFT({seller: seller, listedtokens: tokenIds, tokenType: tokenType, price: price}); 50 | //lock tokens 51 | for (uint256 i = 0; i < tokenIds.length; i++) { 52 | tokenLock[tokenIds[i]] = true; 53 | } 54 | //add to list index 55 | listedNFTList.push(tokenIds[0]); 56 | } 57 | 58 | function existsInListNFT(uint256[] memory tokenIds) public view virtual returns (bool) { 59 | if (listedNFT[tokenIds[0]].seller != address(0)) return true; 60 | 61 | for (uint256 i = 0; i < tokenIds.length; i++) { 62 | if (tokenLock[tokenIds[i]]) return true; 63 | } 64 | return false; 65 | } 66 | 67 | function removeListNFT(uint256 tokenId) public virtual onlyOwner { 68 | require(registeredPayment[tokenId].buyer == address(0), 'RegisterPayment exists for NFT'); 69 | //unlock token 70 | for (uint256 i = 0; i < listedNFT[tokenId].listedtokens.length; i++) { 71 | tokenLock[listedNFT[tokenId].listedtokens[i]] = false; 72 | } 73 | //delete from index 74 | for (uint256 i = 0; i < listedNFTList.length; i++) { 75 | if (listedNFTList[i] == tokenId) { 76 | listedNFTList[i] = listedNFTList[listedNFTList.length - 1]; 77 | listedNFTList.pop(); 78 | break; 79 | } 80 | } 81 | 82 | delete listedNFT[tokenId]; 83 | } 84 | 85 | function getListNFT(uint256 tokenId) public view returns (ListedNFT memory) { 86 | require(listedNFT[tokenId].seller != address(0), 'Listing not exist'); 87 | return listedNFT[tokenId]; 88 | } 89 | 90 | function getAllListNFT() public view returns (uint256[] memory) { 91 | return listedNFTList; 92 | } 93 | 94 | function isValidPaymentMetadata( 95 | address seller, 96 | uint256[] calldata tokenIds, 97 | uint256 payment, 98 | string calldata tokenType 99 | ) public view virtual returns (bool) { 100 | //check if NFT(s) are even listed 101 | require(listedNFT[tokenIds[0]].seller != address(0), 'NFT(s) not listed'); 102 | //check if seller is really a seller 103 | require(listedNFT[tokenIds[0]].seller == seller, 'Submitted Seller is not Seller'); 104 | //check if payment is sufficient 105 | require(listedNFT[tokenIds[0]].price <= payment, 'Payment is too low'); 106 | //check if token type supported 107 | require(_isSameString(listedNFT[tokenIds[0]].tokenType, tokenType), 'Payment token does not match list token type'); 108 | //check if listed NFT(s) match NFT(s) in the payment and are controlled by seller 109 | uint256[] memory listedTokens = listedNFT[tokenIds[0]].listedtokens; 110 | for (uint256 i = 0; i < listedTokens.length; i++) { 111 | require(tokenIds[i] == listedTokens[i], 'One or more tokens are not listed'); 112 | } 113 | return true; 114 | } 115 | 116 | function addRegisterPayment( 117 | address buyer, 118 | uint256[] calldata tokenIds, 119 | uint256 payment, 120 | string calldata tokenType 121 | ) public virtual onlyOwner { 122 | require(registeredPayment[tokenIds[0]].buyer == address(0), 'RegisterPayment already exists'); 123 | registeredPayment[tokenIds[0]] = RegisteredPayment({buyer: buyer, boughtTokens: tokenIds, tokenType: tokenType, payment: payment}); 124 | } 125 | 126 | function getRegisterPayment(uint256 tokenId) public view virtual returns (RegisteredPayment memory) { 127 | return registeredPayment[tokenId]; 128 | } 129 | 130 | function checkRegisterPayment(uint256 tokenId, address buyer) public view virtual returns (uint256) { 131 | if (registeredPayment[tokenId].buyer == buyer) return registeredPayment[tokenId].payment; 132 | else return 0; 133 | } 134 | 135 | function checkRegisterPayment( 136 | uint256 tokenId, 137 | address buyer, 138 | string memory tokenType 139 | ) public view virtual returns (uint256) { 140 | if (registeredPayment[tokenId].buyer == buyer) { 141 | require(_isSameString(tokenType, registeredPayment[tokenId].tokenType), 'TokenType mismatch'); 142 | return registeredPayment[tokenId].payment; 143 | } else return 0; 144 | } 145 | 146 | 147 | function removeRegisterPayment(address buyer, uint256 tokenId) public virtual onlyOwner { 148 | require(registeredPayment[tokenId].buyer == buyer, 'RegisterPayment not found'); 149 | delete registeredPayment[tokenId]; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /contracts/RoyaltyBearingToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache 2.0 2 | pragma solidity 0.8.10; 3 | 4 | import '@openzeppelin/contracts/token/ERC20/IERC20.sol'; 5 | import '@openzeppelin/contracts/token/ERC721/ERC721.sol'; 6 | import '@openzeppelin/contracts/token/ERC721/extensions/ERC721Burnable.sol'; 7 | import '@openzeppelin/contracts/token/ERC721/extensions/ERC721Pausable.sol'; 8 | import '@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol'; 9 | import '@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol'; 10 | import '@openzeppelin/contracts/access/AccessControlEnumerable.sol'; 11 | import '@openzeppelin/contracts/utils/Address.sol'; 12 | import '@openzeppelin/contracts/utils/Counters.sol'; 13 | import '@openzeppelin/contracts/security/ReentrancyGuard.sol'; 14 | import './RoyaltyBearingTokenStorage.sol'; 15 | import './RoyaltyModule.sol'; 16 | import './PaymentModule.sol'; 17 | 18 | contract RoyaltyBearingToken is ERC721Burnable, ERC721Pausable, ERC721URIStorage, AccessControlEnumerable, RoyaltyBearingTokenStorage, IERC721Receiver, ReentrancyGuard { 19 | using Address for address; 20 | using Counters for Counters.Counter; 21 | bool private onlyOnce = false; 22 | 23 | constructor( 24 | string memory name, 25 | string memory symbol, 26 | string memory baseTokenURI, 27 | string[] memory allowedTokenTypes, 28 | address[] memory allowedTokenAddresses, 29 | address creatorAddress, 30 | uint256 numGenerations 31 | ) ERC721(name, symbol) { 32 | require(_msgSender() == tx.origin, 'Caller must not be a contract'); 33 | require(!creatorAddress.isContract(), 'Creator must not be a contract'); 34 | require(allowedTokenTypes.length == allowedTokenAddresses.length, 'Numbers of allowed tokens'); 35 | _baseTokenURI = baseTokenURI; 36 | _setupRole(DEFAULT_ADMIN_ROLE, _msgSender()); 37 | _setupRole(MINTER_ROLE, _msgSender()); 38 | _setupRole(PAUSER_ROLE, _msgSender()); 39 | _setupRole(CREATOR_ROLE, creatorAddress); 40 | 41 | _numGenerations = numGenerations; 42 | 43 | for (uint256 i = 0; i < allowedTokenTypes.length; i++) { 44 | addAllowedTokenType(allowedTokenTypes[i], allowedTokenAddresses[i]); 45 | } 46 | 47 | //For tree logic we need start id from 1 not 0; 48 | _tokenIdTracker.increment(); 49 | } 50 | 51 | function init(address royaltyModuleAddress, address paymentModuleAddress) public virtual { 52 | require(!onlyOnce, 'Init was called before'); 53 | require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()), 'Admin role required'); 54 | royaltyModule = RoyaltyModule(royaltyModuleAddress); 55 | paymentModule = PaymentModule(paymentModuleAddress); 56 | onlyOnce = true; 57 | } 58 | 59 | function updatelistinglimit(uint256 maxListingNumber) public virtual returns (bool) { 60 | //ensure that msg.sender has the creater role or internal call 61 | require(hasRole(CREATOR_ROLE, _msgSender()) || address(this) == _msgSender(), 'Creator role required'); 62 | return paymentModule.updatelistinglimit(maxListingNumber); 63 | } 64 | 65 | function updateRAccountLimits(uint256 maxSubAccounts, uint256 minRoyaltySplit) public virtual returns (bool) { 66 | //ensure that msg.sender has the creater role or internal call 67 | require(hasRole(CREATOR_ROLE, _msgSender()) || address(this) == _msgSender(), 'Creator role required'); 68 | return royaltyModule.updateRAccountLimits(maxSubAccounts, minRoyaltySplit); 69 | } 70 | 71 | function updateMaxGenerations(uint256 newMaxNumber) public virtual returns (bool) { 72 | //ensure that msg.sender has the creater role or internal call 73 | require(hasRole(CREATOR_ROLE, _msgSender()) || address(this) == _msgSender(), 'Creator role required'); 74 | _numGenerations = newMaxNumber; 75 | return true; 76 | } 77 | 78 | function getModules() public view returns (address, address) { 79 | return (address(royaltyModule), address(paymentModule)); 80 | } 81 | 82 | function delegateAuthority( 83 | bytes4 functionSig, 84 | bytes calldata _functionData, 85 | bytes32 documentHash, 86 | uint8[] memory sigV, 87 | bytes32[] memory sigR, 88 | bytes32[] memory sigS, 89 | uint256 chainid 90 | ) public virtual returns (bool) { 91 | require(chainid == block.chainid, 'Wrong blockchain'); 92 | require(functionSigMap[functionSig], 'Not a valid function'); 93 | 94 | bytes32 prefixedProof = keccak256(abi.encodePacked('\x19Ethereum Signed Message:\n32', documentHash)); 95 | address recovered = ecrecover(prefixedProof, sigV[0], sigR[0], sigS[0]); 96 | 97 | require(hasRole(CREATOR_ROLE, recovered), 'Signature'); //Signature was not from creator 98 | 99 | (bool success, ) = address(this).call(_functionData); 100 | require(success); 101 | return true; 102 | } 103 | 104 | //Note that functionSig must be calculated as follows 105 | //bytes4(keccak256("updateMaxGenerations(uint256)") 106 | function setFunctionSignature(bytes4 functionSig) public virtual returns (bool) { 107 | require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()) || hasRole(CREATOR_ROLE, _msgSender()), 'Admin or Creator role required'); 108 | functionSigMap[functionSig] = true; 109 | return true; 110 | } 111 | 112 | function onERC721Received( 113 | address, /*operator*/ 114 | address from, 115 | uint256, /*tokenId*/ 116 | bytes calldata /*data*/ 117 | ) external pure returns (bytes4) { 118 | require(from == address(0), 'Only minted'); 119 | //required to allow transfer mined token to this contract 120 | return bytes4(keccak256('onERC721Received(address,address,uint256,bytes)')); 121 | } 122 | 123 | function addAllowedTokenType(string memory tokenName, address tokenAddress) public { 124 | require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()), 'Admin role required'); 125 | if (_isEthToken(tokenName)) { 126 | tokenAddress = address(this); 127 | } else { 128 | require(tokenAddress != address(0x0) && tokenAddress.isContract(), 'Token must be contact'); 129 | } 130 | require(allowedTokenContract[tokenAddress] == 0, 'Token is duplicate'); 131 | 132 | allowedToken[string(tokenName)] = tokenAddress; 133 | allowedTokenList.push(tokenAddress); 134 | allowedTokenContract[tokenAddress] = allowedTokenList.length; 135 | } 136 | 137 | function getAllowedTokens() public view returns (address[] memory) { 138 | return (allowedTokenList); 139 | } 140 | 141 | //Royalty module functions 142 | //Get a Royalty Account through the NFT token index 143 | function getRoyaltyAccount(uint256 tokenId) 144 | public 145 | view 146 | virtual 147 | returns ( 148 | address accountId, 149 | RoyaltyAccount memory account, 150 | RASubAccount[] memory subaccounts 151 | ) 152 | { 153 | require(_exists(tokenId), 'NFT does not exist'); 154 | return royaltyModule.getAccount(tokenId); 155 | } 156 | 157 | // Rules: 158 | // Only subaccount owner can decrease splitRoyalty for this subaccount 159 | // Only parent token owner can decrease royalty subaccount splitRoyalty 160 | function updateRoyaltyAccount(uint256 tokenId, RASubAccount[] memory affectedSubaccounts) public virtual { 161 | uint256 parentId = ancestry[tokenId].parentId; 162 | bool isTokenOwner = getApproved(parentId) == _msgSender(); 163 | 164 | royaltyModule.updateRoyaltyAccount(tokenId, affectedSubaccounts, _msgSender(), isTokenOwner); 165 | } 166 | 167 | /** 168 | * @dev Creates a new token for `to`. Its token ID will be automatically 169 | * assigned (and available on the emitted {IERC721-Transfer} event), and the token 170 | * URI autogenerated based on the base URI passed at construction. 171 | * 172 | * See {ERC721-_mint}. 173 | * 174 | * Requirements: 175 | * 176 | * - the caller must have the `MINTER_ROLE`. 177 | */ 178 | function mint( 179 | address to, 180 | NFTToken[] memory nfttokens, 181 | string memory tokenType 182 | ) public virtual { 183 | require(nfttokens.length > 0, 'nfttokens has no value'); 184 | require(hasRole(MINTER_ROLE, _msgSender()) || hasRole(CREATOR_ROLE, _msgSender()), 'Minter or Creator role required'); 185 | //ensure to address is not a contract 186 | require(to != address(0x0), 'Zero Address cannot have active NFTs!'); 187 | //require(!to.isContract(), 'Cannot be minted to contracts'); 188 | if (to == _msgSender()) { 189 | require(tx.origin == to, 'To must not be contracts'); 190 | } else { 191 | require(!to.isContract(), 'To must not be contracts'); 192 | } 193 | 194 | //token type must exist 195 | require(allowedToken[tokenType] != address(0x0), 'Token Type not supported!'); 196 | 197 | //Loop through the array of tokens to be minted 198 | for (uint256 i = 0; i < nfttokens.length; i++) { 199 | NFTToken memory token = nfttokens[i]; 200 | 201 | //royaltySplitForItsChildren must be less or equal to 100% 202 | require(token.royaltySplitForItsChildren <= 10000, 'Royalty Split is > 100%'); 203 | 204 | //If the token cannot have offspring royaltySplitForItsChildren must be zero 205 | if (!token.canBeParent) { 206 | token.royaltySplitForItsChildren = 0; 207 | } 208 | 209 | //create RA account identifier 210 | uint256 tokenId = _tokenIdTracker.current(); 211 | 212 | //enforce business rules 213 | if (token.parent > 0) { 214 | require(_exists(token.parent), 'Parent NFT does not exist'); 215 | 216 | //update ancestry struct and mapping 217 | require(ancestry[token.parent].ancestryLevel < _numGenerations, 'Generation limit'); 218 | require(ancestry[token.parent].children.length < ancestry[token.parent].maxChildren, 'Offspring limit'); 219 | ancestry[token.parent].children.push(tokenId); 220 | // store link to parent 221 | ancestry[tokenId].parentId = token.parent; 222 | ancestry[tokenId].ancestryLevel = ancestry[token.parent].ancestryLevel + 1; 223 | } 224 | 225 | // We cannot just use balanceOf to create the new tokenId because tokens 226 | // can be burned (destroyed), so we need a separate counter. 227 | // The NFT contract address(this) must be the owner 228 | _safeMint(address(this), tokenId); 229 | 230 | //give to address minter role unless it has it already 231 | _grantRole(MINTER_ROLE, to); 232 | 233 | // after successful minting, the to address will be approved as an NFT controller. 234 | _approve(to, tokenId); 235 | 236 | //Create and link royalty account 237 | royaltyModule.createRoyaltyAccount(to, token.parent, tokenId, tokenType, token.royaltySplitForItsChildren); 238 | 239 | //set token URI 240 | _setTokenURI(tokenId, token.uri); 241 | 242 | //if new token can have children instantiate struct and add to mapping 243 | if (token.canBeParent) { 244 | ancestry[tokenId].maxChildren = token.maxChildren; 245 | } 246 | 247 | //increment token counter to know which is the next token index that can be minted 248 | _tokenIdTracker.increment(); 249 | } 250 | } 251 | 252 | function updateMaxChildren(uint256 tokenId, uint256 newMaxChildren) public virtual returns (bool) { 253 | //ensure that msg.sender has the role minter 254 | require(hasRole(CREATOR_ROLE, _msgSender()) || address(this) == _msgSender(), 'Creator role required'); 255 | require(newMaxChildren > ancestry[tokenId].children.length, 'Max < Actual'); 256 | ancestry[tokenId].maxChildren = newMaxChildren; 257 | 258 | return true; 259 | } 260 | 261 | //Functions for support ERC721 extensions 262 | function tokenURI(uint256 tokenId) public view virtual override(ERC721, ERC721URIStorage) returns (string memory) { 263 | require(_exists(tokenId), 'URI query for nonexistent token'); 264 | return super.tokenURI(tokenId); 265 | } 266 | 267 | function _baseURI() internal view virtual override returns (string memory) { 268 | return _baseTokenURI; 269 | } 270 | 271 | function pause() public virtual { 272 | require(hasRole(PAUSER_ROLE, _msgSender()), 'Pauser role required'); 273 | _pause(); 274 | } 275 | 276 | function unpause() public virtual { 277 | require(hasRole(PAUSER_ROLE, _msgSender()), 'Pauser role required'); 278 | _unpause(); 279 | } 280 | 281 | function _beforeTokenTransfer( 282 | address from, 283 | address to, 284 | uint256 tokenId, 285 | uint256 batchSize 286 | ) internal virtual override (ERC721,ERC721Pausable){ 287 | super._beforeTokenTransfer(from, to, tokenId, batchSize); 288 | } 289 | 290 | function supportsInterface(bytes4 interfaceId) public view virtual override(AccessControlEnumerable, ERC721) returns (bool) { 291 | return super.supportsInterface(interfaceId); 292 | } 293 | 294 | function _burn(uint256 tokenId) internal virtual override(ERC721, ERC721URIStorage) { 295 | super._burn(tokenId); 296 | } 297 | 298 | function burn(uint256 tokenId) public virtual override { 299 | require(_exists(tokenId), 'ERC721: approved query for nonexistent token'); 300 | require(getApproved(tokenId) == _msgSender(), 'Sender not authorized to burn'); 301 | require(ancestry[tokenId].children.length == 0, 'NFT must not have children'); 302 | //delete token from royalty (check for 0 balance included) 303 | royaltyModule.deleteRoyaltyAccount(tokenId); 304 | 305 | _burn(tokenId); 306 | 307 | uint256 parentId = ancestry[tokenId].parentId; 308 | uint256 length = ancestry[parentId].children.length; 309 | //delete burned token from ancestry 310 | for (uint256 i = 0; i < length; i++) { 311 | if (ancestry[parentId].children[i] == tokenId) { 312 | //swap with last and delete last element for less gas 313 | ancestry[parentId].children[i] = ancestry[parentId].children[length - 1]; 314 | delete ancestry[parentId].children[length - 1]; 315 | break; 316 | } 317 | } 318 | } 319 | 320 | function transferFrom( 321 | address, 322 | address, 323 | uint256 324 | ) public pure override { 325 | revert('Function not allowed'); 326 | } 327 | 328 | function safeTransferFrom( 329 | address, 330 | address, 331 | uint256 332 | ) public virtual override { 333 | revert('Function not allowed'); 334 | } 335 | 336 | function _getTokenBalance(address tokenAddress) private view returns (uint256) { 337 | return IERC20(tokenAddress).balanceOf(address(this)); 338 | } 339 | 340 | function _isEthToken(string memory tokenType) internal pure returns (bool) { 341 | return keccak256(abi.encodePacked(tokenType)) == keccak256(abi.encodePacked('ETH')); 342 | } 343 | 344 | function listNFT( 345 | uint256[] calldata tokenIds, 346 | uint256 price, 347 | string calldata tokenType 348 | ) public virtual returns (bool) { 349 | 350 | for (uint256 i = 0; i < tokenIds.length; i++) { 351 | require(_exists(tokenIds[i]), 'ERC721: approved query for nonexistent token'); 352 | require(getApproved(tokenIds[i]) == _msgSender(), 'Must be token owner'); 353 | require(royaltyModule.isSupportedTokenType(tokenIds[i], tokenType), 'Unsupported token type'); 354 | } 355 | //Put tokens to listed 356 | paymentModule.addListNFT(_msgSender(), tokenIds, price, tokenType); 357 | return true; 358 | } 359 | 360 | function removeNFTListing(uint256 tokenId) public virtual returns (bool) { 361 | require(_msgSender() == getApproved(tokenId), 'Must be token owner'); 362 | paymentModule.removeListNFT(tokenId); 363 | return true; 364 | } 365 | 366 | function _requireExistsAndOwned(uint256[] memory tokenIds, address seller) internal view { 367 | for (uint256 i = 0; i < tokenIds.length; i++) { 368 | require(_exists(tokenIds[i]), 'Token does not exist'); 369 | require(seller == getApproved(tokenIds[i]), 'Seller is not owner'); 370 | } 371 | } 372 | 373 | // ERC20 royalty payment 374 | function executePayment( 375 | address receiver, 376 | address seller, 377 | uint256[] calldata tokenIds, 378 | uint256 payment, 379 | string calldata tokenType, 380 | int256 trxntype 381 | ) public virtual nonReentrant returns (bool) { 382 | require(payment > 0, 'Payments cannot be 0!'); 383 | require(trxntype == 0 || trxntype == 1, 'Trxn type not supported'); 384 | require(receiver != address(0), 'Receiver must not be zero'); 385 | _requireExistsAndOwned(tokenIds, seller); 386 | 387 | paymentModule.isValidPaymentMetadata(seller, tokenIds, payment, tokenType); 388 | 389 | //Execute ERC20 payment 390 | address payToken = allowedToken[tokenType]; 391 | { 392 | require(payToken != address(0x0), 'Unsupported token type'); 393 | //Check for ERC20 approval 394 | uint256 allowed = IERC20(payToken).allowance(_msgSender(), address(this)); 395 | require(allowed >= payment, 'Insufficient token allowance'); 396 | 397 | uint256 balanceBefore = _getTokenBalance(payToken); 398 | 399 | //Transfer ERC20 token to contact 400 | bool success = IERC20(payToken).transferFrom(_msgSender(), address(this), payment); 401 | require(success && payment == _getTokenBalance(payToken) - balanceBefore, 'ERC20 transfer failed'); 402 | } 403 | 404 | //If the transfer is successful, the registeredPayment mapping is updated if trxntype = 1 405 | if (trxntype == 1) { 406 | paymentModule.addRegisterPayment(_msgSender(), tokenIds, payment, tokenType); 407 | } 408 | //if trxntype = 0, an internal version of the safeTransferFrom function must be called to transfer the NFTs to the buyer 409 | else if (trxntype == 0) { 410 | //encode payment data for transfer(s) 411 | bytes memory data = abi.encode(seller, _msgSender(), receiver, tokenIds, tokenType, payment, payToken, block.chainid); 412 | 413 | //transfer NFT(s) 414 | _safeTransferFrom(seller, _msgSender(), tokenIds[0], data); 415 | } 416 | 417 | return true; 418 | } 419 | 420 | function checkPayment( 421 | uint256 tokenId, 422 | string memory tokenType, 423 | address buyer 424 | ) public view virtual returns (uint256) { 425 | return paymentModule.checkRegisterPayment(tokenId, buyer, tokenType); 426 | } 427 | 428 | function reversePayment(uint256 tokenId, string memory tokenType) public virtual nonReentrant returns (bool) { 429 | uint256 payment = checkPayment(tokenId, tokenType, _msgSender()); 430 | require(payment > 0, 'No payment registered'); 431 | 432 | bool success; 433 | if (_isEthToken(tokenType)) { 434 | //ETH reverse payment 435 | (success, ) = _msgSender().call{value: payment}(''); 436 | require(success, 'Ether payout issue'); 437 | } else { 438 | //ERC20 reverse payment 439 | success = IERC20(allowedToken[tokenType]).transfer(_msgSender(), payment); 440 | require(success, 'ERC20 transfer failed'); 441 | } 442 | paymentModule.removeRegisterPayment(_msgSender(), tokenId); 443 | 444 | return success; 445 | } 446 | 447 | function safeTransferFrom( 448 | address from, 449 | address to, 450 | uint256 tokenId, 451 | bytes memory data 452 | ) public override { 453 | ( 454 | address _seller, 455 | address _buyer, 456 | address _receiver, 457 | uint256[] memory _tokenIds, 458 | string memory _tokenType, 459 | uint256 _payment, /*address _tokenTypeAddress*/ 460 | , 461 | uint256 _chainId 462 | ) = abi.decode(data, (address, address, address, uint256[], string, uint256, address, uint256)); 463 | 464 | require(_seller == from, 'Seller not From address'); 465 | require(_receiver == to, 'Receiver not To address'); 466 | require(_tokenIds[0] == tokenId, 'Wrong NFT listing'); 467 | require(_chainId == block.chainid, 'Transfer on wrong Blockchain'); 468 | 469 | //check register payment 470 | require(_payment == paymentModule.checkRegisterPayment(_tokenIds[0], _buyer, _tokenType), 'Payment not match'); 471 | 472 | _requireExistsAndOwned(_tokenIds, _seller); 473 | 474 | //remove register payment 475 | paymentModule.removeRegisterPayment(to, tokenId); 476 | 477 | //Transfer token 478 | _safeTransferFrom(from, to, tokenId, data); 479 | } 480 | 481 | function _safeTransferFrom( 482 | address from, 483 | address to, 484 | uint256, /*tokenId*/ 485 | bytes memory data 486 | ) internal virtual { 487 | (, , , uint256[] memory _tokenIds, string memory tokenType, uint256 payment, address _tokenTypeAddress, ) = abi.decode( 488 | data, 489 | (address, address, address, uint256[], string, uint256, address, uint256) 490 | ); 491 | 492 | require(allowedToken[tokenType] != address(0x0), 'Unsupported token type'); 493 | 494 | if (_isEthToken(tokenType)) { 495 | //Royalty pay in ether 496 | require(_tokenTypeAddress == address(this), 'token address must be contract'); 497 | } 498 | 499 | //Get payments split 500 | uint256[] memory _payments = royaltyModule.splitSum(payment, _tokenIds.length); 501 | 502 | for (uint256 i = 0; i < _tokenIds.length; i++) { 503 | //Distribute royalty payment 504 | royaltyModule.distributePayment(_tokenIds[i], _payments[i]); 505 | 506 | //base transfer after royalty pay 507 | _approve(to, _tokenIds[i]); 508 | //super.safeTransferFrom(from, to, _tokenId, data); 509 | 510 | //give to address minter role unless it has it already -- new in ver 1.3 511 | _grantRole(MINTER_ROLE, to); 512 | 513 | //Force royalty payout for old account 514 | uint256 balance = royaltyModule.getBalance(_tokenIds[i], payable(from)); 515 | if (balance > 0) _royaltyPayOut(_tokenIds[i], payable(from), payable(from), balance); 516 | 517 | //Transfer RA ownership 518 | royaltyModule.transferRAOwnership(from, _tokenIds[i], to); 519 | } 520 | 521 | paymentModule.removeListNFT(_tokenIds[0]); 522 | } 523 | 524 | receive() external payable {} 525 | 526 | fallback() external payable { 527 | // decode msg.data to decide which transfer route to take 528 | (address seller, uint256[] memory tokenIds, address receiver, int256 trxntype) = abi.decode(msg.data, (address, uint256[], address, int256)); 529 | 530 | _requireExistsAndOwned(tokenIds, seller); 531 | 532 | paymentModule.isValidPaymentMetadata(seller, tokenIds, msg.value, 'ETH'); 533 | //decide which transfer path to go based on trxntype (0 = direct purchase, 1 = exchange purchase) 534 | if (trxntype == 1) { 535 | //register payment for exchange based purchases which require a separate, external call to safeTransferFrom function 536 | paymentModule.addRegisterPayment(_msgSender(), tokenIds, msg.value, 'ETH'); 537 | } else if (trxntype == 0) { 538 | //encode payment data for transfer(s) 539 | bytes memory data = abi.encode(seller, _msgSender(), receiver, tokenIds, 'ETH', msg.value, address(this), block.chainid); 540 | 541 | //transfer NFT(s) 542 | _safeTransferFrom(seller, _msgSender(), tokenIds[0], data); 543 | } else { 544 | //if the trxn type is not supported then we need to revert the entire transaction. 545 | revert('Trxn type not supported'); 546 | } 547 | } 548 | 549 | function royaltyPayOut( 550 | uint256 tokenId, 551 | address RAsubaccount, 552 | address payable payoutAccount, 553 | uint256 amount 554 | ) public virtual returns (bool) { 555 | require(_msgSender() == RAsubaccount, 'Sender must be subaccount owner'); 556 | return _royaltyPayOut(tokenId, RAsubaccount, payoutAccount, amount); 557 | } 558 | 559 | function _royaltyPayOut( 560 | uint256 tokenId, 561 | address RAsubaccount, 562 | address payable payoutAccount, 563 | uint256 amount 564 | ) internal virtual returns (bool) { 565 | royaltyModule.checkBalanceForPayout(tokenId, RAsubaccount, amount); 566 | string memory tokenType = royaltyModule.getTokenType(tokenId); 567 | //Reentrancy defence 568 | royaltyModule.withdrawBalance(tokenId, RAsubaccount, amount); 569 | 570 | //payout in Ether 571 | if (_isEthToken(tokenType)) { 572 | (bool success, ) = payoutAccount.call{value: amount}(''); 573 | require(success, 'Ether payout issue'); 574 | } 575 | //payout in tokens 576 | else { 577 | bool success = IERC20(allowedToken[tokenType]).transfer(payoutAccount, amount); 578 | require(success, 'ERC20 transfer failed'); 579 | } 580 | 581 | return true; 582 | } 583 | } 584 | -------------------------------------------------------------------------------- /contracts/RoyaltyBearingTokenStorage.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache 2.0 2 | pragma solidity 0.8.10; 3 | 4 | import './StorageStructure.sol'; 5 | import '@openzeppelin/contracts/utils/Address.sol'; 6 | import '@openzeppelin/contracts/utils/Counters.sol'; 7 | import '@openzeppelin/contracts/access/AccessControlEnumerable.sol'; 8 | import './RoyaltyModule.sol'; 9 | import './PaymentModule.sol'; 10 | 11 | contract RoyaltyBearingTokenStorage is StorageStructure, AccessControlEnumerable { 12 | using Address for address; 13 | using Counters for Counters.Counter; 14 | 15 | bytes32 public constant MINTER_ROLE = keccak256('MINTER_ROLE'); 16 | bytes32 public constant PAUSER_ROLE = keccak256('PAUSER_ROLE'); 17 | bytes32 public constant CREATOR_ROLE = keccak256('CREATOR_ROLE'); 18 | string internal _baseTokenURI; 19 | Counters.Counter internal _tokenIdTracker; 20 | 21 | mapping(uint256 => Child) internal ancestry; //An ancestry mapping of the parent-to-child NFT relationship 22 | mapping(string => address) internal allowedToken; //A mapping of supported token types to their origin contracts 23 | mapping(address => uint256) internal allowedTokenContract; //A mapping of supported token types to their origin contracts 24 | address[] internal allowedTokenList; 25 | mapping(bytes4 => bool) internal functionSigMap; //functionSig mapping 26 | 27 | RoyaltyModule internal royaltyModule; 28 | PaymentModule internal paymentModule; 29 | //address internal logicModule; 30 | 31 | uint256 internal _numGenerations; 32 | address internal _ttAddress; 33 | uint256 internal _royaltySplitTT; 34 | 35 | event Received(address sender, uint256 amount, uint256 tokenId); 36 | } 37 | -------------------------------------------------------------------------------- /contracts/RoyaltyModule.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache 2.0 2 | pragma solidity 0.8.10; 3 | 4 | import '@openzeppelin/contracts/access/Ownable.sol'; 5 | import './StorageStructure.sol'; 6 | import 'abdk-libraries-solidity/ABDKMathQuad.sol'; 7 | 8 | contract RoyaltyModule is StorageStructure, Ownable { 9 | mapping(uint256 => address) private _tokenindextoRA; //Mapping a tokenId to an raAccountID in order to connect a RA raAccountId to a tokenId 10 | mapping(address => RoyaltyAccount) private _royaltyaccount; //Mapping the raAccountID to a RoyaltyAccount in order to connect the account identifier to the actual account. 11 | mapping(address => RASubAccount[]) private _royaltysubaccounts; //workaround for array in struct 12 | mapping(uint256 => Child) private ancestry; //An ancestry mapping of the parent-to-child NFT relationship 13 | 14 | event RoyalyDistributed(uint256 tokenId, address to, uint256 amount, uint256 assetId); 15 | address private _ttAddress; 16 | uint256 private _royaltySplitTT; 17 | uint256 private _maxSubAccount; 18 | uint256 private _minRoyaltySplit; 19 | 20 | constructor( 21 | address owner, 22 | address ttAddress, 23 | uint256 royaltySplitTT, 24 | uint256 minRoyaltySplit, 25 | uint256 maxSubAccounts 26 | ) { 27 | transferOwnership(owner); 28 | require(royaltySplitTT < 10000, 'Royalty Split to TT is > 100%'); //new v1.3 29 | require(royaltySplitTT + minRoyaltySplit < 10000, 'Royalty Split to TT + Minimal Split is > 100%'); 30 | require(ttAddress != address(0), 'Zero Address cannot be TT royalty account'); 31 | _ttAddress = ttAddress; 32 | _royaltySplitTT = royaltySplitTT; 33 | _maxSubAccount = maxSubAccounts; 34 | _minRoyaltySplit = minRoyaltySplit; 35 | } 36 | 37 | 38 | function updateRAccountLimits(uint256 maxSubAccounts, uint256 minRoyaltySplit) public virtual onlyOwner returns (bool) { 39 | require(_royaltySplitTT + minRoyaltySplit < 10000, 'Royalty Split to TT + Minimal Split is > 100%'); 40 | _maxSubAccount = maxSubAccounts; 41 | _minRoyaltySplit = minRoyaltySplit; 42 | return true; 43 | } 44 | 45 | function getAccount(uint256 tokenId) 46 | public 47 | view 48 | returns ( 49 | address, 50 | RoyaltyAccount memory, 51 | RASubAccount[] memory 52 | ) 53 | { 54 | address royaltyAccount = _tokenindextoRA[tokenId]; 55 | return (royaltyAccount, _royaltyaccount[royaltyAccount], _royaltysubaccounts[royaltyAccount]); 56 | } 57 | 58 | // Lib variant 59 | // Rules: 60 | // Only subaccount owner can decrease splitRoyalty for this subaccount 61 | // Only parent token owner can decrease royalty subaccount splitRoyalty 62 | function updateRoyaltyAccount( 63 | uint256 tokenId, 64 | RASubAccount[] memory affectedSubaccounts, 65 | address sender, 66 | bool isTokenOwner 67 | ) public virtual onlyOwner { 68 | address royaltyAccount = _tokenindextoRA[tokenId]; 69 | //Check total sum of royaltySplit was not changed 70 | uint256 oldSum; 71 | uint256 newSum; 72 | for (uint256 i = 0; i < affectedSubaccounts.length; i++) { 73 | require(affectedSubaccounts[i].royaltySplit >= _minRoyaltySplit, 'Royalty Split is smaller then set limit'); 74 | newSum += affectedSubaccounts[i].royaltySplit; 75 | (bool found, uint256 indexOld) = _findSubaccountIndex(royaltyAccount, affectedSubaccounts[i].accountId); 76 | if (found) { 77 | RASubAccount storage foundAcc = _royaltysubaccounts[royaltyAccount][indexOld]; 78 | oldSum += foundAcc.royaltySplit; 79 | //Check rights to decrease royalty split 80 | if (affectedSubaccounts[i].royaltySplit < foundAcc.royaltySplit) { 81 | if (foundAcc.isIndividual) { 82 | require(affectedSubaccounts[i].accountId == sender, 'Only individual subaccount owner can decrease royaltySplit'); 83 | } else { 84 | require(isTokenOwner, 'Only parent token owner can decrease royalty subaccount royaltySplit'); 85 | } 86 | } 87 | } 88 | //New subaccounts must be individual 89 | else { 90 | require(affectedSubaccounts[i].isIndividual, 'New subaccounts must be individual'); 91 | } 92 | } 93 | require(oldSum == newSum, 'Total royaltySplit must be 10000'); 94 | 95 | //Update royalty split for subaccounts and add new subaccounts 96 | for (uint256 i = 0; i < affectedSubaccounts.length; i++) { 97 | (bool found, uint256 indexOld) = _findSubaccountIndex(royaltyAccount, affectedSubaccounts[i].accountId); 98 | if (found) { 99 | _royaltysubaccounts[royaltyAccount][indexOld].royaltySplit = affectedSubaccounts[i].royaltySplit; 100 | } else { 101 | require(_royaltysubaccounts[royaltyAccount].length < _maxSubAccount, 'Too many Royalty subaccounts'); 102 | _royaltysubaccounts[royaltyAccount].push(RASubAccount(true, affectedSubaccounts[i].royaltySplit, 0, affectedSubaccounts[i].accountId)); 103 | } 104 | } 105 | } 106 | 107 | //Deleting a Royalty Account 108 | function deleteRoyaltyAccount(uint256 tokenId) public virtual onlyOwner { 109 | address royaltyAccount = _tokenindextoRA[tokenId]; 110 | for (uint256 i = 0; i < _royaltysubaccounts[royaltyAccount].length; i++) { 111 | if (_royaltysubaccounts[royaltyAccount][i].isIndividual) { 112 | require(_royaltysubaccounts[royaltyAccount][i].royaltyBalance == 0, "Can't delete non empty royalty account"); 113 | } 114 | } 115 | delete _royaltyaccount[royaltyAccount]; 116 | delete _royaltysubaccounts[royaltyAccount]; 117 | delete _tokenindextoRA[tokenId]; 118 | } 119 | 120 | //Function for create a new Royalty Account which is meet the basic requirements 121 | //1. Have correct royalty split configuration 122 | //2. Then generate a RA for the address 123 | //3. Add the RA(address) to the whole hierarchy tree. 124 | //4. Confirm the subaccount of the RA meet the requirement of the hierarchy machenism. 125 | // 126 | function createRoyaltyAccount( 127 | address to, 128 | uint256 parentTokenId, 129 | uint256 tokenId, 130 | string calldata tokenType, 131 | uint256 royaltySplitForItsChildren 132 | ) public onlyOwner returns (address) { 133 | require(royaltySplitForItsChildren <= 10000, 'Royalty Split to be received from children is > 100%'); 134 | 135 | require(_royaltySplitTT + royaltySplitForItsChildren <= 10000, 'Royalty Splits sum is > 100%'); 136 | address raAccountId = address(bytes20(keccak256(abi.encodePacked(tokenId, to, block.number)))); 137 | if (parentTokenId == 0) { 138 | //Create Royalty account without parent 139 | 140 | //create the RA subaccount for the to address 141 | _royaltysubaccounts[raAccountId].push(RASubAccount({isIndividual: true, royaltySplit: 10000 - _royaltySplitTT, royaltyBalance: 0, accountId: to})); 142 | 143 | //create the RA subaccount for TreeTrunk 144 | _royaltysubaccounts[raAccountId].push(RASubAccount({isIndividual: true, royaltySplit: _royaltySplitTT, royaltyBalance: 0, accountId: _ttAddress})); 145 | 146 | //now create the Royalty Account 147 | //map assetID to RA 148 | _royaltyaccount[raAccountId] = RoyaltyAccount({assetId: tokenId, parentId: 0, royaltySplitForItsChildren: royaltySplitForItsChildren, tokenType: tokenType, balance: 0}); 149 | } else { 150 | //Create royalty account with parent 151 | 152 | address parentRoyaltyAccount = _tokenindextoRA[parentTokenId]; 153 | //tokenType must be same as in parent 154 | require(_isSameString(tokenType, _royaltyaccount[parentRoyaltyAccount].tokenType), 'tokenType must be same as in parent'); 155 | 156 | RoyaltyAccount memory parentRA = _royaltyaccount[parentRoyaltyAccount]; 157 | //create the RA subaccount for the to address 158 | _royaltysubaccounts[raAccountId].push(RASubAccount({isIndividual: true, royaltySplit: 10000 - parentRA.royaltySplitForItsChildren - _royaltySplitTT, royaltyBalance: 0, accountId: to})); 159 | 160 | //create the RA subaccount for TreeTrunk 161 | _royaltysubaccounts[raAccountId].push(RASubAccount({isIndividual: true, royaltySplit: _royaltySplitTT, royaltyBalance: 0, accountId: _ttAddress})); 162 | 163 | //create the RA subaccount for the RA address of the ancestor 164 | _royaltysubaccounts[raAccountId].push(RASubAccount({isIndividual: false, royaltySplit: parentRA.royaltySplitForItsChildren, royaltyBalance: 0, accountId: parentRoyaltyAccount})); 165 | 166 | //now create the Royalty Account 167 | //map assetID to RA 168 | _royaltyaccount[raAccountId] = RoyaltyAccount({assetId: tokenId, parentId: parentRA.assetId, royaltySplitForItsChildren: royaltySplitForItsChildren, tokenType: tokenType, balance: 0}); 169 | } 170 | require(_royaltysubaccounts[raAccountId].length <= _maxSubAccount, 'Too many Royalty subaccounts'); 171 | _tokenindextoRA[tokenId] = raAccountId; 172 | return raAccountId; 173 | } 174 | 175 | //Function for recursive distribution royalty for RA tree 176 | function distributePayment(uint256 tokenId, uint256 payment) public virtual onlyOwner returns (bool) { 177 | address royaltyAccount = _tokenindextoRA[tokenId]; 178 | return _distributePayment(royaltyAccount, payment, tokenId); 179 | } 180 | 181 | function _distributePayment( 182 | address royaltyAccount, 183 | uint256 payment, 184 | uint256 tokenId 185 | ) internal virtual returns (bool) { 186 | uint256 remainsValue = payment; 187 | uint256 remainsSplit = 10000; 188 | uint256 assetId = _royaltyaccount[royaltyAccount].assetId; 189 | for (uint256 i = 0; i < _royaltysubaccounts[royaltyAccount].length; i++) { 190 | //skip calculate for 0% subaccounts 191 | if (_royaltysubaccounts[royaltyAccount][i].royaltySplit == 0) continue; 192 | //calculate royalty split sum 193 | uint256 paymentSplit = mulDiv(remainsValue, _royaltysubaccounts[royaltyAccount][i].royaltySplit, remainsSplit); 194 | remainsValue -= paymentSplit; 195 | remainsSplit -= _royaltysubaccounts[royaltyAccount][i].royaltySplit; 196 | //distribute if IND subaccount 197 | if (_royaltysubaccounts[royaltyAccount][i].isIndividual == true) { 198 | _royaltysubaccounts[royaltyAccount][i].royaltyBalance += paymentSplit; 199 | emit RoyalyDistributed(tokenId, _royaltysubaccounts[royaltyAccount][i].accountId, paymentSplit, assetId); 200 | } 201 | //distribute if RA subaccounts 202 | else { 203 | _distributePayment(_royaltysubaccounts[royaltyAccount][i].accountId, paymentSplit, tokenId); 204 | } 205 | } 206 | return true; 207 | } 208 | 209 | function isSupportedTokenType(uint256 tokenId, string calldata tokenType) public view returns (bool) { 210 | return _isSameString(tokenType, _royaltyaccount[_tokenindextoRA[tokenId]].tokenType); 211 | } 212 | 213 | function getTokenType(uint256 tokenId) public view returns (string memory) { 214 | return _royaltyaccount[_tokenindextoRA[tokenId]].tokenType; 215 | } 216 | 217 | function findSubaccountIndex(uint256 tokenId, address subaccount) public view virtual returns (bool, uint256) { 218 | address royaltyAccount = _tokenindextoRA[tokenId]; 219 | return _findSubaccountIndex(royaltyAccount, subaccount); 220 | } 221 | 222 | function checkBalanceForPayout( 223 | uint256 tokenId, 224 | address subaccount, 225 | uint256 amount 226 | ) public view virtual returns (bool) { 227 | (bool subaccountFound, uint256 subaccountIndex) = findSubaccountIndex(tokenId, subaccount); 228 | require(subaccountFound, 'Subaccount not found'); 229 | RASubAccount memory subAccount = getSubaccount(tokenId, subaccountIndex); 230 | require(subAccount.isIndividual == true, 'Subaccount must be individual'); 231 | require(subAccount.royaltyBalance >= amount, 'Insufficient royalty balance'); 232 | return true; 233 | } 234 | 235 | function getSubaccount(uint256 tokenId, uint256 subaccountIndex) public view virtual returns (RASubAccount memory) { 236 | return _royaltysubaccounts[_tokenindextoRA[tokenId]][subaccountIndex]; 237 | } 238 | 239 | function getBalance(uint256 tokenId, address subaccount) public view virtual returns (uint256) { 240 | (bool found, uint256 subaccountIndex) = findSubaccountIndex(tokenId, subaccount); 241 | if (!found) return 0; 242 | return getSubaccount(tokenId, subaccountIndex).royaltyBalance; 243 | } 244 | 245 | //Used for reduce royalty balance after payout 246 | //Used only in RoyaltyBearingToken._royaltyPayOut(uint256,address,address,uint256) 247 | function withdrawBalance( 248 | uint256 tokenId, 249 | address subaccount, 250 | uint256 amount 251 | ) public virtual onlyOwner { 252 | (bool subaccountFound, uint256 subaccountIndex) = findSubaccountIndex(tokenId, subaccount); 253 | require(subaccountFound, 'Subaccount not found'); 254 | require(_royaltysubaccounts[_tokenindextoRA[tokenId]][subaccountIndex].royaltyBalance >= amount, 'Insufficient royalty balance'); 255 | _royaltysubaccounts[_tokenindextoRA[tokenId]][subaccountIndex].royaltyBalance -= amount; 256 | } 257 | 258 | //Used in RoyaltyBearingToken._safeTransferFrom(address, address,uint256, bytes) 259 | //for transfer royalty account ownership after tranfer token ownership 260 | function transferRAOwnership( 261 | address seller, 262 | uint256 tokenId, 263 | address buyer 264 | ) public virtual onlyOwner { 265 | address royaltyAccount = _tokenindextoRA[tokenId]; 266 | (bool found, uint256 index) = _findSubaccountIndex(royaltyAccount, seller); 267 | require(found, 'Seller subaccount not found'); 268 | require(_royaltysubaccounts[royaltyAccount][index].royaltyBalance == uint256(0), 'Seller subaccount must have 0 balance'); 269 | 270 | //replace owner of subaccount 271 | _royaltysubaccounts[royaltyAccount][index].accountId = buyer; 272 | } 273 | 274 | // Find subaccount index by subaccount address 275 | function _findSubaccountIndex(address royaltyAccount, address subaccount) internal view virtual returns (bool, uint256) { 276 | //local variable decrease contract code size 277 | RASubAccount[] storage subAccounts = _royaltysubaccounts[royaltyAccount]; 278 | for (uint256 i = 0; i < subAccounts.length; i++) { 279 | if (subAccounts[i].accountId == subaccount) { 280 | return (true, i); 281 | } 282 | } 283 | return (false, 0); 284 | } 285 | 286 | //Util function for split royalty payment 287 | function mulDiv( 288 | uint256 x, 289 | uint256 y, 290 | uint256 z 291 | ) public pure returns (uint256) { 292 | return ABDKMathQuad.toUInt(ABDKMathQuad.div(ABDKMathQuad.mul(ABDKMathQuad.fromUInt(x), ABDKMathQuad.fromUInt(y)), ABDKMathQuad.fromUInt(z))); 293 | } 294 | 295 | //Util function for split value by pieces without remains 296 | function splitSum(uint256 sum, uint256 pieces) public pure virtual returns (uint256[] memory) { 297 | uint256[] memory result = new uint256[](pieces); 298 | uint256 remains = sum; 299 | for (uint256 i = 0; i < pieces; i++) { 300 | result[i] = mulDiv(remains, 1, pieces - i); 301 | remains -= result[i]; 302 | } 303 | return result; 304 | } 305 | } 306 | -------------------------------------------------------------------------------- /contracts/StorageStructure.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache 2.0 2 | pragma solidity 0.8.10; 3 | 4 | contract StorageStructure { 5 | struct RoyaltyAccount { 6 | //assetId is the tokenId of the NFT the RA belongs to 7 | uint256 assetId; 8 | //parentId is the tokenId of the NFT from which this NFT is derived 9 | uint256 parentId; 10 | //royaltySplit to be paid to RA from its direct offspring 11 | uint256 royaltySplitForItsChildren; 12 | //tokenType of the balance in this RA account 13 | string tokenType; 14 | //Account balance is the total RA account balance and must be equal to the sum of the subaccount balances 15 | uint256 balance; 16 | //the struct array for sub accounts (Not supported in eth) 17 | //RASubAccount[] rasubaccount; 18 | } 19 | 20 | struct RASubAccount { 21 | //accounttype is defined as isIndividual, and is a boolean variable, and if set to true, the account is that of an individual, if set to false, the account is an RA account ID 22 | bool isIndividual; 23 | // royalty split gives the percentage as a decimal value smaller than 1 24 | uint256 royaltySplit; 25 | //balance of the subaccount 26 | uint256 royaltyBalance; 27 | //we need the account id which we define as a bytes32 such that it is easy to convert to an address and can also be used to identity an RA acount by a hash value 28 | address accountId; 29 | } 30 | 31 | struct Child { 32 | //link to parent token 33 | uint256 parentId; 34 | //maximum number of children 35 | uint256 maxChildren; 36 | //ancestry level of NFT used to determine level of children 37 | uint256 ancestryLevel; //new in v1.3 38 | //link to children tokens 39 | uint256[] children; 40 | } 41 | 42 | struct NFTToken { 43 | //the parent of the (child) token, if 0 then there is no parent 44 | uint256 parent; 45 | //whether the token can be a parent 46 | bool canBeParent; 47 | //how many children the token can have 48 | uint256 maxChildren; 49 | //what the Royalty Split For Its Child is 50 | uint256 royaltySplitForItsChildren; 51 | //token URI 52 | string uri; 53 | } 54 | 55 | struct RegisteredPayment { 56 | //Buyer 57 | address buyer; 58 | //tokens bought 59 | uint256[] boughtTokens; 60 | //Type of Payment Token 61 | string tokenType; 62 | //Payment amount 63 | uint256 payment; 64 | } 65 | 66 | struct ListedNFT { 67 | //Seller 68 | address seller; 69 | //tokens listed 70 | uint256[] listedtokens; 71 | //Type of Payment Token 72 | string tokenType; 73 | //List price 74 | uint256 price; 75 | } 76 | 77 | function _isSameString(string memory left, string memory right) internal pure returns (bool) { 78 | return keccak256(abi.encodePacked(left)) == keccak256(abi.encodePacked(right)); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /contracts/test/SomeERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicenzed 2 | pragma solidity ^0.8.0; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol"; 5 | 6 | contract WETH_Mock is ERC20PresetMinterPauser { 7 | constructor(string memory name, string memory symbol) ERC20PresetMinterPauser(name, symbol) {} 8 | } 9 | 10 | contract SomeERC20_1 is ERC20PresetMinterPauser { 11 | constructor(string memory name, string memory symbol) ERC20PresetMinterPauser(name, symbol) {} 12 | } 13 | 14 | contract SomeERC20_2 is ERC20PresetMinterPauser { 15 | constructor(string memory name, string memory symbol) ERC20PresetMinterPauser(name, symbol) {} 16 | } 17 | -------------------------------------------------------------------------------- /contracts/test/faucet.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicensed 2 | pragma solidity ^0.8.10; 3 | 4 | //import '@openzeppelin/contracts/token/ERC20/IERC20.sol' 5 | 6 | interface ERC20 { 7 | function transfer(address to, uint256 value) external returns (bool); 8 | 9 | function balanceOf(address account) external view returns (uint256); 10 | 11 | event Transfer(address indexed from, address indexed to, uint256 value); 12 | } 13 | 14 | contract Faucet { 15 | uint256 public constant tokenAmount = 100000000000000000000; 16 | uint256 public constant waitTime = 10 minutes; 17 | 18 | ERC20 public tokenInstance; 19 | 20 | mapping(address => uint256) lastAccessTime; 21 | 22 | constructor(address _tokenInstance) { 23 | require(_tokenInstance != address(0)); 24 | tokenInstance = ERC20(_tokenInstance); 25 | } 26 | 27 | function getTokeInstance() public view returns (address) { 28 | return address(tokenInstance); 29 | } 30 | 31 | function getBalance() public view returns (uint256) { 32 | return tokenInstance.balanceOf(address(this)); 33 | } 34 | 35 | function requestTokens() public { 36 | requestTokensTo(msg.sender); 37 | } 38 | 39 | function requestTokensTo(address _receiver) public { 40 | require(allowedToWithdraw(_receiver)); 41 | tokenInstance.transfer(_receiver, tokenAmount); 42 | lastAccessTime[_receiver] = block.timestamp + waitTime; 43 | } 44 | 45 | function allowedToWithdraw(address _address) public view returns (bool) { 46 | if (lastAccessTime[_address] == 0) { 47 | return true; 48 | } else if (block.timestamp >= lastAccessTime[_address]) { 49 | return true; 50 | } 51 | return false; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /docs/mythXPro-Report-PaymentModule-03-22-2022.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/treetrunkio/treetrunk-nft-reference-implementation/a28747e0a9d3669b0f2d8faac44e61ab80b6e0ce/docs/mythXPro-Report-PaymentModule-03-22-2022.pdf -------------------------------------------------------------------------------- /docs/mythXPro-Report-RoyaltyBearingTokenModule-03-22-2022.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/treetrunkio/treetrunk-nft-reference-implementation/a28747e0a9d3669b0f2d8faac44e61ab80b6e0ce/docs/mythXPro-Report-RoyaltyBearingTokenModule-03-22-2022.pdf -------------------------------------------------------------------------------- /docs/mythXPro-Report-RoyaltyModule-03-22-2022.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/treetrunkio/treetrunk-nft-reference-implementation/a28747e0a9d3669b0f2d8faac44e61ab80b6e0ce/docs/mythXPro-Report-RoyaltyModule-03-22-2022.pdf -------------------------------------------------------------------------------- /migrations/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | var Migrations = artifacts.require("./Migrations.sol"); 2 | 3 | 4 | module.exports = function (deployer) { 5 | deployer.deploy(Migrations); 6 | }; 7 | -------------------------------------------------------------------------------- /migrations/2_deploy_contracts.js: -------------------------------------------------------------------------------- 1 | const RoyaltyModule = artifacts.require('RoyaltyModule'); 2 | const PaymentModule = artifacts.require('PaymentModule'); 3 | const RoyaltyBearingToken = artifacts.require('RoyaltyBearingToken'); 4 | const SomeERC20_1 = artifacts.require('SomeERC20_1'); 5 | const SomeERC20_2 = artifacts.require('SomeERC20_2'); 6 | 7 | const numGenerations = 100; 8 | 9 | module.exports = function (deployer, network, accounts) { 10 | console.log('Deploy to network:', network); 11 | if (network == 'development' || network == 'soliditycoverage' || network == 'mumbai') { 12 | deployer.then(async () => { 13 | const ERC20_1 = await deployer.deploy(SomeERC20_1, 'Some test token #1', 'ST1'); 14 | const ERC20_2 = await deployer.deploy(SomeERC20_2, 'Some test token #2', 'ST2'); 15 | 16 | const token = await deployer.deploy( 17 | RoyaltyBearingToken, 18 | 'RoyaltyBearingToken', 19 | 'RBT', 20 | 'https:\\\\some.base.url\\', 21 | ['ETH', 'ST1', 'ST2'], 22 | ['0x0000000000000000000000000000000000000000', ERC20_1.address, ERC20_2.address], 23 | accounts[0], 24 | 100, //numGenerations 25 | ); 26 | 27 | const royaltyModule = await deployer.deploy( 28 | RoyaltyModule, 29 | token.address, 30 | accounts[0], //TT Royalty, 31 | 1000, // royaltySplitTT 1000 = 10%, 32 | 500, //minRoyaltySplit 33 | 5, //maxSubAccount 34 | ); 35 | const paymentModule = await deployer.deploy( 36 | PaymentModule, 37 | token.address, 38 | 10, //maxListingNumber 39 | ); 40 | 41 | await token.init(royaltyModule.address, paymentModule.address); 42 | 43 | console.log('token:', token.address); 44 | }); 45 | /* 46 | deployer.deploy(RoyaltyBearingToken, 'A', 'RBT', 'C').then((contract) => { 47 | console.log("Token deployed with address", contract.address); 48 | }); 49 | */ 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /migrations/3_faucet.js: -------------------------------------------------------------------------------- 1 | const Faucet = artifacts.require('Faucet'); 2 | const WETH_Mock = artifacts.require('WETH_Mock'); 3 | 4 | //const WETHaddress = '0xb225B1a0873B933004AcF480Bc62cBF67533c2Bd'; 5 | const TokenToMint = '10000000000000000000000000000000000000'; 6 | 7 | module.exports = function (deployer, network, accounts) { 8 | if (network == 'mumbai') { 9 | console.log('Deploy Faucet to network:', network); 10 | /* 11 | deployer.then(async () => { 12 | const mockWETH = await deployer.deploy(WETH_Mock,'WETH Mock token', 'WETH'); 13 | const faucet = await deployer.deploy(Faucet, mockWETH.address); 14 | await mockWETH.mint(faucet.address, TokenToMint); 15 | const balance = await faucet.getBalance(); 16 | 17 | console.log('WETH mock:', mockWETH.address); 18 | console.log('Faucet:', faucet.address); 19 | console.log('Faucet balance:', balance.toString()); 20 | 21 | }); 22 | */ 23 | 24 | deployer.then(async () => { 25 | const faucet = await Faucet.at('0xf1D50435131169e4A176ef502917eCaAeA958b62'); 26 | const mockWETH = await WETH_Mock.at('0xF087BBD87Dc6188914572C4F184998bD509c480f'); 27 | //const faucet = await deployer.deploy(Faucet, mockWETH.address); 28 | await mockWETH.mint(faucet.address, TokenToMint); 29 | 30 | const listToMint = [ 31 | //'0x93F4c85915BCbe0dAF8C5466D9Ec796672336584', 32 | //'0x7Dfc51EB31eaE117d4c81E8C61622d8407bA1C0e', 33 | //'0xB22cD6298c234f7Ca2e9eE34D7B24E0A80f71C5b', 34 | //'0x9D2E14F6E616d1348c9ddb89883fE73ae0Ca5BE5', 35 | //'0x6e58E675F0D05bC5ab14806246cb7EA41D4C6dc2', 36 | //'0xB22cD6298c234f7Ca2e9eE34D7B24E0A80f71C5b', 37 | //'0xd2a54f534D65bb1C34fC8c63Adc3c91E963390E8', 38 | //'0xcd3497E7769aD22Aab2470DC5CA4494433c08180', 39 | ]; 40 | 41 | console.log('Faucet:', faucet.address); 42 | for (let i = 0; i < listToMint.length; i++) { 43 | try { 44 | await faucet.requestTokensTo(listToMint[i]); 45 | console.log('Token sent to:', listToMint[i], (await mockWETH.balanceOf(listToMint[i])).toString()); 46 | } catch (ex) { 47 | console.log('Token not sent:', listToMint[i], ex); 48 | } 49 | } 50 | }); 51 | } 52 | }; 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nft-royalty", 3 | "version": "1.0.0", 4 | "description": "Royalty Bearing NFTs: Extending the ERC721 Standard to include Royalty Allocations", 5 | "main": "truffle.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "dev": "lite-server", 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "author": "", 14 | "license": "ISC", 15 | "devDependencies": { 16 | "eth-gas-reporter": "^0.2.22", 17 | "lite-server": "^2.6.1", 18 | "prettier": "^2.4.1", 19 | "prettier-plugin-solidity": "^1.0.0-beta.18", 20 | "solidity-coverage": "^0.7.18" 21 | }, 22 | "dependencies": { 23 | "@openzeppelin/contracts": "^4.3.2", 24 | "@truffle/hdwallet-provider": "^2.1.3", 25 | "abdk-libraries-solidity": "^3.0.0", 26 | "truffle-assertions": "^0.9.2", 27 | "truffle-contract-size": "^2.0.1" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/treetrunkio/treetrunk-nft-reference-implementation/a28747e0a9d3669b0f2d8faac44e61ab80b6e0ce/test/.gitkeep -------------------------------------------------------------------------------- /test/Basic Tests: -------------------------------------------------------------------------------- 1 | Basic Tests are mainly for 3 different part: 2 | 1. Royalty Split 3 | royaltySplit.test.js: Hierarchy tree structure for RAs and Individual accounts. Basic functions tests of royalty split machenism. 4 | royaltySplitUpdate.test.js: Complex functions of royalty split, including trouble shooting. 5 | 2. Stress Tests 6 | Stress test according to large batch size and depth of the hierarchy tree. Check the loading of the contract. 7 | 3. Transfer(both ERC20 & ETH) 8 | Token transaction and balance change in RAs and individual accounts, using both ERC20 token(supporting NFT trading and royalty) and ETH. 9 | 10 | -------------------------------------------------------------------------------- /test/coverage/PaymentModule.test.js: -------------------------------------------------------------------------------- 1 | const PaymentModule = artifacts.require('PaymentModule'); 2 | const truffleAssert = require('truffle-assertions'); 3 | 4 | const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; 5 | contract('PaymentModule', (accounts) => { 6 | const accAdmin = accounts[0]; 7 | const accUser1 = accounts[1]; 8 | const accUser2 = accounts[2]; 9 | 10 | let paymentModule; 11 | 12 | before(async () => { 13 | paymentModule = await PaymentModule.deployed(); 14 | }); 15 | 16 | describe('Only owner can call update functions', async () => { 17 | it('addListNFT', async () => { 18 | await truffleAssert.reverts(paymentModule.addListNFT(ZERO_ADDRESS, [0], 0, '', { from: accAdmin }), 'Ownable: caller is not the owner'); 19 | }); 20 | it('addRegisterPayment', async () => { 21 | await truffleAssert.reverts(paymentModule.addRegisterPayment(ZERO_ADDRESS, [0], 0, '', { from: accAdmin }), 'Ownable: caller is not the owner'); 22 | }); 23 | it('removeRegisterPayment', async () => { 24 | await truffleAssert.reverts(paymentModule.removeRegisterPayment(ZERO_ADDRESS, [0], { from: accAdmin }), 'Ownable: caller is not the owner'); 25 | }); 26 | }); 27 | 28 | describe('Getter functions', async () => { 29 | it('getRegisterPayment for empty token', async () => { 30 | const result = await paymentModule.getRegisterPayment(1, { from: accAdmin }); 31 | assert.equal(result.payment, 0); 32 | }); 33 | it('checkRegisterPayment for empty token', async () => { 34 | const payment = await paymentModule.checkRegisterPayment(1, accUser1, { from: accAdmin }); 35 | assert.equal(payment, 0); 36 | }); 37 | }); 38 | 39 | }); 40 | -------------------------------------------------------------------------------- /test/coverage/RoyaltyBearingToken.test.js: -------------------------------------------------------------------------------- 1 | const RoyaltyBearingToken = artifacts.require('RoyaltyBearingToken'); 2 | const PaymentModule = artifacts.require('PaymentModule'); 3 | const SomeERC20_1 = artifacts.require('SomeERC20_1'); 4 | const SomeERC20_2 = artifacts.require('SomeERC20_2'); 5 | 6 | const truffleAssert = require('truffle-assertions'); 7 | const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; 8 | const MINTER_ROLE = web3.utils.keccak256('MINTER_ROLE'); 9 | 10 | contract('RoyaltyBearingToken', (accounts) => { 11 | const accAdmin = accounts[0]; 12 | const accSeller = accounts[1]; 13 | const accBuyer = accounts[2]; 14 | const accReceiver = accounts[3]; 15 | const accOtherBuyer = accounts[4]; 16 | const accSomeOther = accounts[5]; 17 | 18 | const costOfNFT = 100; 19 | const token_1 = 1; 20 | const token_1_1 = 2; 21 | const token_1_1_1 = 3; 22 | const token_1_1_2 = 4; 23 | const token_2 = 5; 24 | const token_not_exists = 999; 25 | 26 | let token; 27 | let someToken1; 28 | let someToken2; 29 | let paymentModule; 30 | 31 | before(async () => { 32 | someToken1 = await SomeERC20_1.deployed(); 33 | someToken2 = await SomeERC20_2.deployed(); 34 | token = await RoyaltyBearingToken.deployed(); 35 | 36 | paymentModule = await PaymentModule.deployed(); 37 | 38 | //Mint some ERC20 tokens 39 | await someToken1.mint(accBuyer, 100000000, { from: accAdmin }); 40 | await someToken2.mint(accBuyer, 100000000, { from: accAdmin }); 41 | await someToken2.mint(accOtherBuyer, 100000000, { from: accAdmin }); 42 | }); 43 | 44 | describe('addAllowedTokenType restrictions', async () => { 45 | it('Caller must have admin role', async () => { 46 | await truffleAssert.reverts(token.addAllowedTokenType('ST1', someToken1.address, { from: accSomeOther }), 'Admin role required'); 47 | }); 48 | it('Duplicate not allowed', async () => { 49 | await truffleAssert.reverts(token.addAllowedTokenType('ST1_1', someToken1.address), 'Token is duplicate'); 50 | }); 51 | it('Token address must be contract', async () => { 52 | await truffleAssert.reverts(token.addAllowedTokenType('ST_Err', accSomeOther, { from: accAdmin }), 'Token must be contact'); 53 | }); 54 | }); 55 | 56 | describe('Mint restrictions', async () => { 57 | it('Caller must have minter role', async () => { 58 | await truffleAssert.reverts(token.mint(accSeller, [[0x0, true, 10, 1000, 'uri_1']], 'ST2', { from: accSeller }), 'Minter or Creator role required'); 59 | }); 60 | it('To must not be zero', async () => { 61 | await truffleAssert.reverts(token.mint(ZERO_ADDRESS, [[0x0, true, 10, 1000, 'uri_1']], 'ST2', { from: accAdmin }), 'Zero Address cannot have active NFTs!'); 62 | }); 63 | it('To must not be contract', async () => { 64 | await truffleAssert.reverts(token.mint(someToken2.address, [[0x0, true, 10, 1000, 'uri_1']], 'ST2', { from: accAdmin }), ' To must not be contracts'); 65 | }); 66 | it('Parent must be zero or existing token', async () => { 67 | await truffleAssert.reverts(token.mint(accSeller, [[999, true, 10, 1000, 'uri_1']], 'ST2', { from: accAdmin }), 'Parent NFT does not exist'); 68 | }); 69 | it('Royalty split must be < 10000', async () => { 70 | await truffleAssert.reverts(token.mint(accSeller, [[0, true, 10, 10000 + 1, 'uri_1']], 'ST2', { from: accAdmin }), 'Royalty Split is > 100%'); 71 | }); 72 | it('Token list required', async () => { 73 | await truffleAssert.reverts(token.mint(accSeller, [], 'ST2', { from: accAdmin }), 'nfttokens has no value'); 74 | }); 75 | it('Token for payment must be registered', async () => { 76 | await truffleAssert.reverts(token.mint(accSeller, [[0x0, true, 10, 1000, 'uri_1']], 'ST3', { from: accAdmin }), 'Token Type not supported!'); 77 | }); 78 | it('Mint some tokens', async () => { 79 | //ERC20 tokens 80 | await token.mint( 81 | accSeller, 82 | [ 83 | [0x0, true, 10, 1000, 'uri_1'], // id = 1 84 | [0x1, true, 10, 1000, 'uri_1.1'], // id = 2 85 | [0x2, false, 10, 1000, 'uri_1.1.1'], // id = 3 86 | [0x2, false, 10, 1000, 'uri_1.1.2'], // id = 4 87 | ], 88 | 'ST2', 89 | { from: accAdmin }, 90 | ); 91 | //ETH tokens 92 | await token.mint( 93 | accSeller, 94 | [ 95 | [0x0, true, 10, 1000, 'uri_2'], // id = 5 96 | ], 97 | 'ETH', 98 | { from: accAdmin }, 99 | ); 100 | assert.equal((await token.balanceOf(token.address)).toString(), 5, 'Token balance must changed'); 101 | assert.equal(await token.hasRole(MINTER_ROLE, accSeller), true, 'CREATOR role must granted'); 102 | assert.equal(await token.getApproved(1), accSeller, 'Token approved for owner'); 103 | assert.equal(await token.getApproved(2), accSeller, 'Token approved for owner'); 104 | assert.equal(await token.getApproved(3), accSeller, 'Token approved for owner'); 105 | }); 106 | }); 107 | describe('listNFT restriction', async () => { 108 | it('Caller must be token owner', async () => { 109 | await truffleAssert.reverts(token.listNFT([1, 2, 3], costOfNFT, 'ST2', { from: accBuyer }), 'Must be token owner'); 110 | }); 111 | it('Token must exists', async () => { 112 | await truffleAssert.reverts(token.listNFT([1, 2, 99], costOfNFT, 'ST2', { from: accSeller }), 'ERC721: approved query for nonexistent token'); 113 | }); 114 | it('Payment token must be supported', async () => { 115 | await truffleAssert.reverts(token.listNFT([1, 2, 3], costOfNFT, 'ST3', { from: accSeller }), 'Unsupported token type'); 116 | }); 117 | it('Numbers of tokens must be less than limit', async () => { 118 | await token.updatelistinglimit(1, { from: accAdmin }); 119 | await truffleAssert.reverts(token.listNFT([2, 3], costOfNFT, 'ST2', { from: accSeller }), 'Too many NFTs listed'); 120 | await token.updatelistinglimit(10, { from: accAdmin }); 121 | }); 122 | it('Zero Price is not allowed', async () => { 123 | await truffleAssert.reverts(token.listNFT([1], 0, 'ST2', { from: accSeller }), 'Zero Price not allowed'); 124 | }); 125 | it('List NFT (2,3)', async () => { 126 | await token.listNFT([2, 3], costOfNFT, 'ST2', { from: accSeller }); 127 | }); 128 | it('Only one list allowed. Try list (2,3) when (2,3) are listed', async () => { 129 | await truffleAssert.reverts(token.listNFT([2, 3], costOfNFT, 'ST2', { from: accSeller }), 'Already exists'); 130 | }); 131 | it('Token can listed only once in bundles. Try (1,2) when (2,3) are listed', async () => { 132 | await truffleAssert.reverts(token.listNFT([1, 2], costOfNFT, 'ST2', { from: accSeller }), 'Already exists'); 133 | }); 134 | it('List NFT (1)', async () => { 135 | await token.listNFT([1], costOfNFT, 'ST2', { from: accSeller }); 136 | }); 137 | it('List NFT (5) by ETH', async () => { 138 | await token.listNFT([5], costOfNFT, 'ETH', { from: accSeller }); 139 | }); 140 | }); 141 | describe('removeNFTListing restriction', async () => { 142 | it('Caller must be token owner', async () => { 143 | await truffleAssert.reverts(token.removeNFTListing(1, { from: accBuyer }), 'Must be token owner'); 144 | }); 145 | it('Unlist NFT (1)', async () => { 146 | await token.removeNFTListing(1, { from: accSeller }); 147 | }); 148 | it('List NFT (1)', async () => { 149 | await token.listNFT([1], costOfNFT, 'ST2', { from: accSeller }); 150 | }); 151 | }); 152 | describe('PaymentModule', async () => { 153 | it('getListNFT function', async () => { 154 | const result = await paymentModule.getListNFT(1); 155 | assert.equal(result.seller, accSeller); 156 | assert.equal(result.tokenType, 'ST2'); 157 | assert.equal(result.price, 100); 158 | }); 159 | it('getAllListNFT function', async () => { 160 | const result = await paymentModule.getAllListNFT(); 161 | assert.equal(result[0].toNumber(), 2); 162 | assert.equal(result[1].toNumber(), 5); 163 | assert.equal(result[2].toNumber(), 1); 164 | }); 165 | it('getListNFT but listing not exist', async () => { 166 | await truffleAssert.reverts(paymentModule.getListNFT(token_not_exists), 'Listing not exist'); 167 | }); 168 | }); 169 | 170 | describe('executePayment restriction', (async) => { 171 | it('Only supported transaction type allowed', async () => { 172 | await truffleAssert.reverts(token.executePayment(accReceiver, accSeller, [2, 3], costOfNFT, 'ST2', 2, { from: accBuyer }), 'Trxn type not supported'); 173 | }); 174 | it('Receiver must be non zero', async () => { 175 | await truffleAssert.reverts(token.executePayment(ZERO_ADDRESS, accSeller, [2, 3], costOfNFT, 'ST2', 0, { from: accBuyer }), 'Receiver must not be zero'); 176 | }); 177 | it('Only supported token type allowed', async () => { 178 | await truffleAssert.reverts(token.executePayment(accReceiver, accSeller, [2, 3], costOfNFT, 'ST1', 0, { from: accBuyer }), 'Payment token does not match list token type'); 179 | }); 180 | it('Token allowance must be set', async () => { 181 | await truffleAssert.reverts(token.executePayment(accReceiver, accSeller, [2, 3], costOfNFT, 'ST2', 0, { from: accBuyer }), 'Insufficient token allowance'); 182 | }); 183 | it('Payment must be for existing list', async () => { 184 | await truffleAssert.reverts(token.executePayment(accReceiver, accSeller, [token_not_exists], costOfNFT, 'ST2', 0, { from: accBuyer }), 'Token does not exist'); 185 | }); 186 | it('Seller must be equals to seller in list', async () => { 187 | await truffleAssert.reverts(token.executePayment(accReceiver, accSomeOther, [2, 3], costOfNFT, 'ST2', 0, { from: accBuyer }), 'Seller is not owner'); 188 | }); 189 | it('Payment must be not low', async () => { 190 | await truffleAssert.reverts(token.executePayment(accReceiver, accSeller, [2, 3], costOfNFT * 0.5, 'ST2', 0, { from: accBuyer }), 'Payment is too low'); 191 | }); 192 | it('Token list must mach to listed tokens', async () => { 193 | await truffleAssert.reverts(token.executePayment(accReceiver, accSeller, [2, 1], costOfNFT, 'ST2', 0, { from: accBuyer }), 'One or more tokens are not listed'); 194 | }); 195 | it('Payment nust be > 0', async () => { 196 | await truffleAssert.reverts(token.executePayment(accReceiver, accSeller, [2, 3], 0, 'ST2', 0, { from: accBuyer }), 'Payments cannot be 0!'); 197 | }); 198 | it('Payment ignore other trxntype', async () => { 199 | await truffleAssert.reverts(token.executePayment(accReceiver, accSeller, [2, 3], costOfNFT, 'ST2', 4, { from: accBuyer }), 'Trxn type not supported'); 200 | }); 201 | it('NFT(s) not listed', async () => { 202 | await truffleAssert.reverts(token.executePayment(accReceiver, accSeller, [4], costOfNFT, 'ST2', 0, { from: accBuyer }), 'NFT(s) not listed'); 203 | }); 204 | 205 | it('Payment for (2,3) trxntype=0 success', async () => { 206 | await someToken2.approve(token.address, costOfNFT, { from: accBuyer }); 207 | await token.executePayment(accReceiver, accSeller, [2, 3], costOfNFT, 'ST2', 0, { from: accBuyer }); 208 | assert.equal(await token.getApproved(2), accBuyer, 'Token must transfer to buyer'); 209 | assert.equal(await token.getApproved(3), accBuyer, 'Token must transfer to buyer'); 210 | }); 211 | it('Only 1 payment allowed for (2,3). (2,3) was already sold', async () => { 212 | await someToken2.approve(token.address, costOfNFT, { from: accBuyer }); 213 | await truffleAssert.reverts(token.executePayment(accReceiver, accSeller, [2, 3], costOfNFT, 'ST2', 0, { from: accBuyer }), 'Seller is not owner'); 214 | }); 215 | it('Payment for (1) trxntype=1 must have right token type', async () => { 216 | await someToken1.approve(token.address, costOfNFT, { from: accBuyer }); 217 | await truffleAssert.reverts(token.executePayment(accReceiver, accSeller, [1], costOfNFT, 'ST1', 1, { from: accBuyer }), 'Payment token does not match list token type'); 218 | }); 219 | it('Payment for (1) trxntype=1 success', async () => { 220 | await someToken2.approve(token.address, costOfNFT, { from: accBuyer }); 221 | await token.executePayment(accReceiver, accSeller, [1], costOfNFT, 'ST2', 1, { from: accBuyer }); 222 | assert.equal(await token.getApproved(1), accSeller, 'Token must transfer manual later'); 223 | }); 224 | it('Undo payment for (1) trxntype=1 success', async () => { 225 | await token.reversePayment(1, 'ST2', { from: accBuyer }); 226 | }); 227 | it('Retry payment for (1) trxntype=1 success', async () => { 228 | await someToken2.approve(token.address, costOfNFT, { from: accBuyer }); 229 | await token.executePayment(accReceiver, accSeller, [1], costOfNFT, 'ST2', 1, { from: accBuyer }); 230 | }); 231 | it('Second payment for (1) trxntype=1 not allowed', async () => { 232 | await someToken2.approve(token.address, costOfNFT, { from: accOtherBuyer }); 233 | await truffleAssert.reverts(token.executePayment(accReceiver, accSeller, [1], costOfNFT, 'ST2', 1, { from: accOtherBuyer }), 'RegisterPayment already exists'); 234 | }); 235 | it('Can not unlist token after pay', async () => { 236 | await truffleAssert.reverts(token.removeNFTListing(1, { from: accSeller }), 'RegisterPayment exists for NFT'); 237 | }); 238 | it('checkPayment must have valid token type', async () => { 239 | await truffleAssert.reverts(token.checkPayment(1, 'ST1', accBuyer, { from: accSeller }), 'TokenType mismatch'); 240 | }); 241 | it('reversePayment fails if payment not exists', async () => { 242 | await truffleAssert.reverts(token.reversePayment(1, 'ST2', { from: accSomeOther }), 'No payment registered'); 243 | }); 244 | it('Payment for (5) must have right transaction type', async () => { 245 | const data = web3.eth.abi.encodeParameters(['address', 'uint256[]', 'address', 'int256'], [accSeller, [5], accBuyer, 3]); 246 | await truffleAssert.reverts(web3.eth.sendTransaction({ from: accBuyer, to: token.address, value: costOfNFT, data: data, gas: 1000000 }), 'Trxn type not supported'); 247 | }); 248 | 249 | it('Payment for (5) trxntype=1 success', async () => { 250 | const data = web3.eth.abi.encodeParameters(['address', 'uint256[]', 'address', 'int256'], [accSeller, [5], accBuyer, 1]); 251 | await web3.eth.sendTransaction({ from: accBuyer, to: token.address, value: costOfNFT, data: data, gas: 1000000 }); 252 | assert.equal(await token.getApproved(5), accSeller, 'Token must transfer manual later'); 253 | const payment = await token.checkPayment(5, 'ETH', accBuyer, { from: accSeller }); 254 | assert.equal(payment.toNumber(), costOfNFT); 255 | }); 256 | it('reversePayment for (5) success', async () => { 257 | await token.reversePayment(5, 'ETH', { from: accBuyer }); 258 | const payment = await token.checkPayment(5, 'ETH', accBuyer, { from: accSeller }); 259 | assert.equal(payment.toNumber(), 0); 260 | }); 261 | }); 262 | describe('safeTransferFrom restrictions', async () => { 263 | it('Wrong metadata: seller address', async () => { 264 | const chainId = await web3.eth.getChainId(); 265 | const data = web3.eth.abi.encodeParameters( 266 | ['address', 'address', 'address', 'uint256[]', 'string', 'uint256', 'address', 'uint256'], 267 | [accSomeOther, accBuyer, accBuyer, [1], 'ST2', costOfNFT, someToken2.address, chainId], 268 | ); 269 | await truffleAssert.reverts(token.methods['safeTransferFrom(address,address,uint256,bytes)'](accSeller, accBuyer, 1, data, { from: accSeller }), 'Seller not From address'); 270 | }); 271 | it('Wrong metadata: receiver address', async () => { 272 | const chainId = await web3.eth.getChainId(); 273 | const data = web3.eth.abi.encodeParameters( 274 | ['address', 'address', 'address', 'uint256[]', 'string', 'uint256', 'address', 'uint256'], 275 | [accSeller, accBuyer, accSomeOther, [1], 'ST2', costOfNFT, someToken2.address, chainId], 276 | ); 277 | await truffleAssert.reverts(token.methods['safeTransferFrom(address,address,uint256,bytes)'](accSeller, accBuyer, 1, data, { from: accSeller }), 'Receiver not To address'); 278 | }); 279 | 280 | it('Wrong metadata: wrong payment', async () => { 281 | const chainId = await web3.eth.getChainId(); 282 | const data = web3.eth.abi.encodeParameters( 283 | ['address', 'address', 'address', 'uint256[]', 'string', 'uint256', 'address', 'uint256'], 284 | [accSeller, accBuyer, accBuyer, [1], 'ST2', costOfNFT + 1, someToken2.address, chainId], 285 | ); 286 | await truffleAssert.reverts(token.methods['safeTransferFrom(address,address,uint256,bytes)'](accSeller, accBuyer, 1, data, { from: accSeller }), 'Payment not match'); 287 | }); 288 | 289 | it('Wrong metadata: token ids', async () => { 290 | const chainId = await web3.eth.getChainId(); 291 | const data = web3.eth.abi.encodeParameters( 292 | ['address', 'address', 'address', 'uint256[]', 'string', 'uint256', 'address', 'uint256'], 293 | [accSeller, accBuyer, accBuyer, [2], 'ST2', costOfNFT, someToken2.address, chainId], 294 | ); 295 | await truffleAssert.reverts(token.methods['safeTransferFrom(address,address,uint256,bytes)'](accSeller, accBuyer, 1, data, { from: accSeller }), 'Wrong NFT listing'); 296 | }); 297 | it('Wrong metadata: pay token symbol', async () => { 298 | const chainId = await web3.eth.getChainId(); 299 | const data = web3.eth.abi.encodeParameters( 300 | ['address', 'address', 'address', 'uint256[]', 'string', 'uint256', 'address', 'uint256'], 301 | [accSeller, accBuyer, accBuyer, [1], 'ST1', costOfNFT, someToken2.address, chainId], 302 | ); 303 | await truffleAssert.reverts(token.methods['safeTransferFrom(address,address,uint256,bytes)'](accSeller, accBuyer, 1, data, { from: accSeller }), 'TokenType mismatch'); 304 | }); 305 | it('Wrong metadata: wrong chain id', async () => { 306 | const data = web3.eth.abi.encodeParameters( 307 | ['address', 'address', 'address', 'uint256[]', 'string', 'uint256', 'address', 'uint256'], 308 | [accSeller, accBuyer, accBuyer, [1], 'ST2', costOfNFT, someToken2.address, 999], 309 | ); 310 | await truffleAssert.reverts(token.methods['safeTransferFrom(address,address,uint256,bytes)'](accSeller, accBuyer, 1, data, { from: accSeller }), 'Transfer on wrong Blockchain'); 311 | }); 312 | it('Token list must be owned by seller', async () => { 313 | const chainId = await web3.eth.getChainId(); 314 | const data = web3.eth.abi.encodeParameters( 315 | ['address', 'address', 'address', 'uint256[]', 'string', 'uint256', 'address', 'uint256'], 316 | [accSeller, accBuyer, accBuyer, [1, 2], 'ST2', costOfNFT, someToken2.address, chainId], 317 | ); 318 | await truffleAssert.reverts(token.methods['safeTransferFrom(address,address,uint256,bytes)'](accSeller, accBuyer, 1, data, { from: accSeller }), 'Seller is not owner'); 319 | }); 320 | 321 | it('Transfer (1) success', async () => { 322 | const chainId = await web3.eth.getChainId(); 323 | const data = web3.eth.abi.encodeParameters( 324 | ['address', 'address', 'address', 'uint256[]', 'string', 'uint256', 'address', 'uint256'], 325 | [accSeller, accBuyer, accBuyer, [1], 'ST2', costOfNFT, someToken2.address, chainId], 326 | ); 327 | //truffle fail to select valid method safeTransferFrom 328 | //await token.safeTransferFrom(accSeller, accOwner3, tokenId, data,{from:accSeller}); 329 | //workaround for this 330 | await token.methods['safeTransferFrom(address,address,uint256,bytes)'](accSeller, accBuyer, 1, data, { from: accSeller }); 331 | assert.equal(await token.getApproved(1), accBuyer, 'Token must transferred'); 332 | }); 333 | 334 | it('Repeat transfer not allowed', async () => { 335 | const chainId = await web3.eth.getChainId(); 336 | const data = web3.eth.abi.encodeParameters( 337 | ['address', 'address', 'address', 'uint256[]', 'string', 'uint256', 'address', 'uint256'], 338 | [accSeller, accBuyer, accBuyer, [1], 'ST2', costOfNFT, someToken2.address, chainId], 339 | ); 340 | await truffleAssert.reverts(token.methods['safeTransferFrom(address,address,uint256,bytes)'](accSeller, accBuyer, 1, data, { from: accSeller }), 'Payment not match'); 341 | }); 342 | }); 343 | describe('royaltyPayOut restrictions', async () => { 344 | it('Only subaccount owner can run payout', async () => { 345 | await truffleAssert.reverts(token.royaltyPayOut(token_1, accBuyer, accBuyer, 1, { from: accOtherBuyer }), 'Sender must be subaccount owner'); 346 | await truffleAssert.reverts(token.royaltyPayOut(token_1_1, accBuyer, accBuyer, 1, { from: accOtherBuyer }), 'Sender must be subaccount owner'); 347 | await truffleAssert.reverts(token.royaltyPayOut(token_1_1_1, accBuyer, accBuyer, 1, { from: accOtherBuyer }), 'Sender must be subaccount owner'); 348 | }); 349 | it('Payout limited to royalty balance', async () => { 350 | await truffleAssert.reverts(token.royaltyPayOut(token_1_1, accBuyer, accBuyer, 999, { from: accBuyer }), 'Insufficient royalty balance'); 351 | }); 352 | it('Payout for non exist NFT restricted', async () => { 353 | await truffleAssert.reverts(token.royaltyPayOut(token_not_exists, accBuyer, accBuyer, 1, { from: accBuyer }), 'Subaccount not found'); 354 | }); 355 | it('Success payout', async () => { 356 | const ra_1_1_before = await token.getRoyaltyAccount(token_1_1); 357 | await token.royaltyPayOut(2, accBuyer, accBuyer, 1, { from: accBuyer }); 358 | const ra_1_1_after = await token.getRoyaltyAccount(token_1_1); 359 | assert.equal(ra_1_1_before.subaccounts[0].royaltyBalance - 1, ra_1_1_after.subaccounts[0].royaltyBalance, 'Royalty changed after payout'); 360 | }); 361 | }); 362 | describe('burn restrictions', async () => { 363 | it('Burn restricted for MINTER_ROLE', async () => { 364 | await truffleAssert.reverts(token.burn(token_1_1_1, { from: accSomeOther }), 'Sender not authorized to burn'); 365 | }); 366 | it('Burn token with royalty ballance not allowed', async () => { 367 | await truffleAssert.reverts(token.burn(token_1_1_1, { from: accBuyer }), "Can't delete non empty royalty account"); 368 | }); 369 | it('Burn token with children not allowed', async () => { 370 | await truffleAssert.reverts(token.burn(token_1_1, { from: accBuyer }), 'NFT must not have children'); 371 | }); 372 | it('Burn token must exists', async () => { 373 | await truffleAssert.reverts(token.burn(token_not_exists, { from: accBuyer }), 'ERC721: approved query for nonexistent token'); 374 | }); 375 | it('Payout TT royalty from (3)', async () => { 376 | const ra_1_1_1_before = await token.getRoyaltyAccount(token_1_1_1); 377 | await token.royaltyPayOut(token_1_1_1, accAdmin, accAdmin, ra_1_1_1_before.subaccounts[1].royaltyBalance, { from: accAdmin }); 378 | const ra_1_1_1_after = await token.getRoyaltyAccount(token_1_1_1); 379 | assert.equal(ra_1_1_1_after.subaccounts[1].royaltyBalance, 0); 380 | }); 381 | it('Burn token without children and 0 royalty success', async () => { 382 | await token.burn(token_1_1_1, { from: accBuyer }); 383 | }); 384 | }); 385 | describe('Other transfer function restrictions', async () => { 386 | it('transferFrom(address,address,uint256) not allowed', async () => { 387 | await truffleAssert.reverts(token.transferFrom(ZERO_ADDRESS, ZERO_ADDRESS, 0, { from: accBuyer }), 'Function not allowed'); 388 | }); 389 | it('safeTransferFrom(address,address,uint256) not allowed', async () => { 390 | await truffleAssert.reverts(token.safeTransferFrom(ZERO_ADDRESS, ZERO_ADDRESS, 0, { from: accBuyer }), 'Function not allowed'); 391 | }); 392 | }); 393 | describe('updateMaxChildren restrictions', async () => { 394 | it('updateMaxChildren not allowed without CREATOR_ROLE', async () => { 395 | await truffleAssert.reverts(token.updateMaxChildren(token_1_1, 0, { from: accSomeOther }), 'Creator role required'); 396 | }); 397 | it('updateMaxChildren not allowed new limit bellow actual children', async () => { 398 | await truffleAssert.reverts(token.updateMaxChildren(token_1_1, 0, { from: accAdmin }), 'Max < Actual'); 399 | }); 400 | it('updateMaxChildren success', async () => { 401 | await token.updateMaxChildren(token_1_1, 3, { from: accAdmin }); 402 | }); 403 | }); 404 | describe('updateMaxGenerations restrictions', async () => { 405 | it('updateMaxGenerations not allowed without CREATOR_ROLE', async () => { 406 | await truffleAssert.reverts(token.updateMaxGenerations(5, { from: accSomeOther }), 'Creator role required'); 407 | }); 408 | it('updateMaxGenerations success', async () => { 409 | await token.updateMaxGenerations(1, { from: accAdmin }); 410 | }); 411 | it('mint not allowed for new generations', async () => { 412 | await truffleAssert.reverts(token.mint(accSeller, [[token_1_1, true, 10, 1000, 'uri_1.1.1.1']], 'ST2', { from: accAdmin }), 'Generation limit'); 413 | }); 414 | it('updateMaxGenerations success', async () => { 415 | await token.updateMaxGenerations(5, { from: accAdmin }); 416 | }); 417 | }); 418 | describe('Minor function coverage', async () => { 419 | it('getAllowedTokens', async () => { 420 | const result = await token.getAllowedTokens(); 421 | assert.equal(result.length, 3); 422 | assert.equal(result[0], token.address); 423 | assert.equal(result[1], someToken1.address); 424 | assert.equal(result[2], someToken2.address); 425 | }); 426 | it('getModules', async () => { 427 | const modules = await token.getModules(); 428 | assert.equal(Object.keys(modules).length, 2); 429 | }); 430 | it('tokenURI', async () => { 431 | const uri = await token.tokenURI(token_1_1); 432 | assert.equal(uri, 'https:\\\\some.base.url\\uri_1.1'); 433 | }); 434 | it('tokenURI for burned not allowed', async () => { 435 | await await truffleAssert.reverts(token.tokenURI(token_1_1_1), 'URI query for nonexistent token'); 436 | }); 437 | it('pause not allowed without PAUSER_ROLE', async () => { 438 | await truffleAssert.reverts(token.pause({ from: accSomeOther }), 'Pauser role required'); 439 | }); 440 | it('unpause not allowed without PAUSER_ROLE', async () => { 441 | await truffleAssert.reverts(token.unpause({ from: accSomeOther }), 'Pauser role required'); 442 | }); 443 | it('pause success for PAUSER_ROLE', async () => { 444 | await token.pause({ from: accAdmin }); 445 | }); 446 | it('unpause success for PAUSER_ROLE', async () => { 447 | await token.unpause({ from: accAdmin }); 448 | }); 449 | it('supportsInterface', async () => { 450 | const result = await token.supportsInterface('0x0000'); 451 | assert.equal(result, false); 452 | }); 453 | it('second init call not allowed', async () => { 454 | await truffleAssert.reverts(token.init(ZERO_ADDRESS, ZERO_ADDRESS, { from: accAdmin }), 'Init was called before'); 455 | }); 456 | it('updatelistinglimit caller must be creator', async () => { 457 | await truffleAssert.reverts(token.updatelistinglimit(10, { from: accSomeOther }), 'Creator role required'); 458 | }); 459 | it('maxListingNumber > 0', async () => { 460 | await truffleAssert.reverts(token.updatelistinglimit(0, { from: accAdmin }), 'Max number must be > 0'); 461 | }); 462 | it('updateRAccountLimits caller must be creator', async () => { 463 | await truffleAssert.reverts(token.updateRAccountLimits(10, 10, { from: accSomeOther }), 'Creator role required'); 464 | }); 465 | it('onERC721Received accept only own tokens', async () => { 466 | await truffleAssert.reverts(token.onERC721Received(ZERO_ADDRESS, accSomeOther, 1, '0x0', { from: accSomeOther }), 'Only minted'); 467 | }); 468 | it('getRoyaltyAccount cant get not exist token', async () => { 469 | await truffleAssert.reverts(token.getRoyaltyAccount(token_not_exists, { from: accSomeOther }), 'NFT does not exist'); 470 | }); 471 | }); 472 | describe('Delegate call', async () => { 473 | const funcSig1 = web3.utils.keccak256('updateMaxGenerations(uint256)').substring(0, 6); 474 | const funcSig2 = web3.utils.keccak256('updatelistinglimit(uint256)').substring(0, 6); 475 | console.log('funcSig2', funcSig2); 476 | it('Only creator can call setFunctionSignature', async () => { 477 | await truffleAssert.reverts(token.setFunctionSignature(funcSig1, { from: accSomeOther }), 'Admin or Creator role required'); 478 | }); 479 | it('Set signatures', async () => { 480 | await token.setFunctionSignature(funcSig1, { from: accAdmin }); 481 | }); 482 | 483 | it('Only registered function can be called', async () => { 484 | const chainId = await web3.eth.getChainId(); 485 | await truffleAssert.reverts( 486 | token.delegateAuthority( 487 | funcSig2, 488 | web3.utils.randomHex(32), // 489 | web3.utils.randomHex(32), 490 | [0,1,2], 491 | [web3.utils.randomHex(32)], 492 | [web3.utils.randomHex(32)], 493 | chainId, 494 | { from: accAdmin }, 495 | ), 496 | 'Not a valid function', 497 | ); 498 | }); 499 | it('Invalid signature not allowed', async () => { 500 | const chainId = await web3.eth.getChainId(); 501 | await truffleAssert.reverts( 502 | token.delegateAuthority( 503 | funcSig1, 504 | web3.utils.randomHex(32), // 505 | web3.utils.randomHex(32), 506 | [0,1,2], 507 | [web3.utils.randomHex(32)], 508 | [web3.utils.randomHex(32)], 509 | chainId, 510 | { from: accAdmin }, 511 | ), 512 | 'Signature', 513 | ); 514 | }); 515 | it('Wrong blockchain not allowed', async () => { 516 | await truffleAssert.reverts( 517 | token.delegateAuthority( 518 | funcSig1, 519 | web3.utils.randomHex(32), // 520 | web3.utils.randomHex(32), 521 | [0,1,2], 522 | [web3.utils.randomHex(32)], 523 | [web3.utils.randomHex(32)], 524 | 999, 525 | { from: accAdmin }, 526 | ), 527 | 'Wrong blockchain', 528 | ); 529 | }); 530 | }); 531 | }); 532 | -------------------------------------------------------------------------------- /test/coverage/RoyaltyModule.test.js: -------------------------------------------------------------------------------- 1 | const RoyaltyModule = artifacts.require('RoyaltyModule'); 2 | const truffleAssert = require('truffle-assertions'); 3 | 4 | const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; 5 | contract('RoyaltyModule', (accounts) => { 6 | const accAdmin = accounts[0]; 7 | const accUser1 = accounts[1]; 8 | const accUser2 = accounts[2]; 9 | 10 | let royaltyModule, token; 11 | 12 | before(async () => { 13 | royaltyModule = await RoyaltyModule.deployed(); 14 | }); 15 | 16 | describe('Only owner can call update functions', async () => { 17 | it('createRoyaltyAccount', async () => { 18 | await truffleAssert.reverts(royaltyModule.createRoyaltyAccount(ZERO_ADDRESS, 0, 0, '', 0, { from: accAdmin }), 'Ownable: caller is not the owner'); 19 | }); 20 | it('updateRoyaltyAccount', async () => { 21 | await truffleAssert.reverts(royaltyModule.updateRoyaltyAccount(0, [], ZERO_ADDRESS, true, { from: accAdmin }), 'Ownable: caller is not the owner'); 22 | }); 23 | it('deleteRoyaltyAccount', async () => { 24 | await truffleAssert.reverts(royaltyModule.deleteRoyaltyAccount(0, { from: accAdmin }), 'Ownable: caller is not the owner'); 25 | }); 26 | it('distributePayment', async () => { 27 | await truffleAssert.reverts(royaltyModule.distributePayment(0, 0, { from: accAdmin }), 'Ownable: caller is not the owner'); 28 | }); 29 | it('withdrawBalance', async () => { 30 | await truffleAssert.reverts(royaltyModule.withdrawBalance(0, ZERO_ADDRESS, 0, { from: accAdmin }), 'Ownable: caller is not the owner'); 31 | }); 32 | it('transferRAOwnership', async () => { 33 | await truffleAssert.reverts(royaltyModule.transferRAOwnership(ZERO_ADDRESS, 0, ZERO_ADDRESS, { from: accAdmin }), 'Ownable: caller is not the owner'); 34 | }); 35 | }); 36 | describe('Getter functions', async () => { 37 | it('getBalance for empty token', async () => { 38 | const balance = await royaltyModule.getBalance(1, ZERO_ADDRESS, { from: accAdmin }); 39 | assert.equal(balance, 0); 40 | }); 41 | }); 42 | 43 | }); 44 | -------------------------------------------------------------------------------- /test/royaltySplit.test.js: -------------------------------------------------------------------------------- 1 | const RoyaltyBearingToken = artifacts.require('RoyaltyBearingToken'); 2 | const SomeERC20_1 = artifacts.require('SomeERC20_1'); 3 | const SomeERC20_2 = artifacts.require('SomeERC20_2'); 4 | const DIGITS = 1000000000; 5 | 6 | contract('RoyaltyBearingToken', (accounts) => { 7 | //create a mapping of Buyer & Seller account (RA & Users) 8 | const accAdmin = accounts[0]; 9 | const accOwner1 = accounts[1]; 10 | const accOwner2 = accounts[2]; 11 | const accOwner3 = accounts[3]; 12 | const accOwner4 = accounts[4]; 13 | const accBuyer = accounts[5]; 14 | const accSeller = accounts[6]; 15 | 16 | const costOfNFT = 100 * DIGITS; 17 | const tokenId_1 = 1; 18 | const tokenId_1_1 = 2; 19 | const tokenId_1_1_1 = 3; 20 | 21 | let token; 22 | let someToken1; 23 | let someToken2; 24 | 25 | before(async () => { 26 | //Suppose token to be ERC20 token and RoyaltyBearing ERC721 tokens 27 | someToken1 = await SomeERC20_1.deployed(); 28 | someToken2 = await SomeERC20_2.deployed(); 29 | token = await RoyaltyBearingToken.deployed(); 30 | 31 | //Mint some ERC20 tokens 32 | //await someToken2.mint(accBuyer, cost, { from: accAdmin }); 33 | }); 34 | 35 | //Hierarchy tree of recursion porblems 36 | //Recursion: 1. RA mint token and get approved 37 | // 2. RA sell token to sub_accounts and get payments approved 38 | // . 3. check account balance of seller and buyer 39 | describe('Royalty split scenario', async () => { 40 | it('accOwner1 mint root token_1 with 20%', async () => { 41 | await token.mint(accOwner1, [[0x0, true, 10, 2000, 'uri_1']], 'ST2', { from: accAdmin }); 42 | assert.equal(await token.getApproved(tokenId_1), accOwner1, 'Token approved for owner'); 43 | }); 44 | it('accOwner1 sell token_1 to accOwner2 -- (accOwner1 receive 90% directly)', async () => { 45 | await token.listNFT([tokenId_1], costOfNFT, 'ST2', { from: accOwner1 }); 46 | //Mint and approve ERC20 47 | await someToken2.mint(accOwner2, costOfNFT, { from: accAdmin }); 48 | await someToken2.approve(token.address, costOfNFT, { from: accOwner2 }); 49 | //Buy NFT 50 | await token.executePayment(accOwner2, accOwner1, [tokenId_1], costOfNFT, 'ST2', 0, { from: accOwner2 }); 51 | 52 | const a1_after = await someToken2.balanceOf(accOwner1); 53 | const a2_after = await someToken2.balanceOf(accOwner2); 54 | 55 | //check balance in 2 accounts -- balance_of_seller == 0.9 * cost && balance_of_buyer == 0 56 | assert.equal(a1_after.toNumber(), 0.9 * costOfNFT); 57 | assert.equal(a2_after.toNumber(), 0); 58 | }); 59 | it('accOwner2 mint token_1_1 to with 50%', async () => { 60 | await token.mint(accOwner2, [[tokenId_1, true, 10, 5000, 'uri_1_1']], 'ST2', { from: accAdmin }); 61 | assert.equal(await token.getApproved(tokenId_1_1), accOwner2, 'Token approved for owner'); 62 | }); 63 | it('accOwner2 sell token_1_1 to accOwner3 -- (accOwner2 receive 70% directly; accOwner2 receive 20% on royalty acc for token_1)', async () => { 64 | const a1_before = (await someToken2.balanceOf(accOwner1)).toNumber(); 65 | const a2_before = (await someToken2.balanceOf(accOwner2)).toNumber(); 66 | const a3_before = (await someToken2.balanceOf(accOwner3)).toNumber(); 67 | 68 | const ra1_before = await token.getRoyaltyAccount(tokenId_1); 69 | 70 | await token.listNFT([tokenId_1_1], costOfNFT, 'ST2', { from: accOwner2 }); 71 | //Mint and approve ERC20 72 | await someToken2.mint(accOwner3, costOfNFT, { from: accAdmin }); 73 | await someToken2.approve(token.address, costOfNFT, { from: accOwner3 }); 74 | //Buy NFT 75 | await token.executePayment(accOwner3, accOwner2, [tokenId_1_1], costOfNFT, 'ST2', 0, { from: accOwner3 }); 76 | 77 | const a1_after = (await someToken2.balanceOf(accOwner1)).toNumber(); 78 | const a2_after = (await someToken2.balanceOf(accOwner2)).toNumber(); 79 | const a3_after = (await someToken2.balanceOf(accOwner3)).toNumber(); 80 | 81 | const ra1_after = await token.getRoyaltyAccount(tokenId_1); 82 | 83 | assert.equal(a1_after - a1_before, 0); 84 | assert.equal(a2_after - a2_before, 0.7 * costOfNFT); 85 | assert.equal(a3_after - a3_before, 0); 86 | 87 | assert.equal(ra1_after.subaccounts[0].accountId, accOwner2); //accOwner2 own token_1 anf receive 20% 88 | assert.equal(ra1_after.subaccounts[0].royaltyBalance - ra1_before.subaccounts[0].royaltyBalance, Math.floor(0.2 * 0.9 * costOfNFT)); // 90% of 20% royalty 89 | }); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /test/royaltySplitUpdate.test.js: -------------------------------------------------------------------------------- 1 | const RoyaltyBearingToken = artifacts.require('RoyaltyBearingToken'); 2 | const SomeERC20_1 = artifacts.require('SomeERC20_1'); 3 | const SomeERC20_2 = artifacts.require('SomeERC20_2'); 4 | 5 | const truffleAssert = require('truffle-assertions'); 6 | const MINTER_ROLE = web3.utils.keccak256('MINTER_ROLE'); 7 | 8 | contract('RoyaltyBearingToken', (accounts) => { 9 | const accAdmin = accounts[0]; 10 | const accSeller = accounts[1]; 11 | const accBuyer = accounts[2]; 12 | const accSomeone1 = accounts[3]; 13 | const accSomeone2 = accounts[4]; 14 | const accNewRoyalty = accounts[4]; 15 | 16 | const costOfNFT = 100; 17 | const tokenRootId = 1; 18 | const tokenId_1_1 = 2; 19 | const tokenId_1_1_1 = 3; 20 | 21 | let token; 22 | let someToken1; 23 | let someToken2; 24 | 25 | before(async () => { 26 | someToken1 = await SomeERC20_1.deployed(); 27 | someToken2 = await SomeERC20_2.deployed(); 28 | token = await RoyaltyBearingToken.deployed(); 29 | }); 30 | 31 | // Token and NFT List preparation for royalty split update testing 32 | describe('Prepare tokens for test', async () => { 33 | it('mint tokens to accSeller', async () => { 34 | await token.mint( 35 | accSeller, 36 | [ 37 | [0x0, true, 10, 1000, 'uri_1'], 38 | [0x1, true, 10, 1000, 'uri_1.1'], 39 | [0x2, true, 10, 1000, 'uri_1.1.1'], 40 | ], 41 | 'ETH', 42 | { from: accAdmin }, 43 | ); 44 | //1st minter should be the creator to ensure the correct flow of the royalty 45 | assert.equal((await token.balanceOf(token.address)).toString(), 3, 'Token balance must changed'); 46 | assert.equal(await token.hasRole(MINTER_ROLE, accSeller), true, 'CREATOR role must granted'); 47 | assert.equal(await token.getApproved(1), accSeller, 'Token approved for owner'); 48 | assert.equal(await token.getApproved(2), accSeller, 'Token approved for owner'); 49 | assert.equal(await token.getApproved(3), accSeller, 'Token approved for owner'); 50 | }); 51 | it('seller make listNFT (2)', async () => { 52 | await token.listNFT([tokenId_1_1], costOfNFT, 'ETH', { from: accSeller }); 53 | }); 54 | 55 | it('buyer execute the ETH payment to Token contract and receive tokens (2)', async () => { 56 | const data = web3.eth.abi.encodeParameters(['address', 'uint256[]', 'address', 'int256'], [accSeller, [tokenId_1_1], accBuyer, 0]); 57 | await web3.eth.sendTransaction({ from: accBuyer, to: token.address, value: costOfNFT, data: data, gas: 6000000 }); 58 | }); 59 | }); 60 | 61 | // Troubleshoot during royalty split 62 | // 1. Sum of royalty should be constant + sum of proportion should == 1 63 | // 2. RA should be token owner or royalty receiver 64 | // 3. Inheritance between accounts 65 | // 4. Difference between RA & individual account 66 | // 5. Royalty payback to parent accounts 67 | 68 | describe('Edit royalty split functionality', async () => { 69 | //Token 1.1 royalty after init 70 | //Owner/TT/parent 71 | //8000/1000/1000 72 | 73 | it('updateRoyaltyAccount not allowed for if caller is not token owner or royalty receiver', async () => { 74 | await truffleAssert.reverts(token.updateRoyaltyAccount(tokenId_1_1, [[true, 1000, 0, accSomeone1]], { from: accSomeone1 }), 'Total royaltySplit must be 10000'); 75 | }); 76 | it('Sum of royalty split must be = 10000', async () => { 77 | const updates = [ 78 | [true, 8000, 0, accBuyer], 79 | [true, 1000, 0, accSomeone1], 80 | ]; 81 | await truffleAssert.reverts(token.updateRoyaltyAccount(tokenId_1_1, updates, { from: accSomeone1 }), 'Total royaltySplit must be 10000'); 82 | }); 83 | it('Only subaccount owner can reduce own royalty split', async () => { 84 | const updates = [ 85 | [true, 7000, 0, accBuyer], 86 | [true, 1000, 0, accSomeone1], 87 | ]; 88 | await truffleAssert.reverts(token.updateRoyaltyAccount(tokenId_1_1, updates, { from: accSomeone1 }), 'Only individual subaccount owner can decrease royaltySplit'); 89 | }); 90 | it('Only parent token owner can reduce royalty split for parent', async () => { 91 | const ra = await token.getRoyaltyAccount(tokenId_1_1); 92 | assert.equal(ra.subaccounts[2].isIndividual, false); 93 | const updates = [ 94 | [true, 500, 0, ra.subaccounts[2].accountId], 95 | [true, 8500, 0, accBuyer], 96 | ]; 97 | await truffleAssert.reverts(token.updateRoyaltyAccount(tokenId_1_1, updates, { from: accSomeone1 }), 'Only parent token owner can decrease royalty subaccount royaltySplit'); 98 | }); 99 | it('Parent owner decrease royalty and transfer royaltySplit to token owner', async () => { 100 | //8000/1000/1000 >> 8500/1000/500 101 | const ra = await token.getRoyaltyAccount(tokenId_1_1); 102 | assert.equal(ra.subaccounts[2].isIndividual, false); 103 | const updates = [ 104 | [false, 500, 0, ra.subaccounts[2].accountId], 105 | [true, 8500, 0, accBuyer], 106 | ]; 107 | await token.updateRoyaltyAccount(tokenId_1_1, updates, { from: accSeller }); 108 | 109 | const ra_after = await token.getRoyaltyAccount(tokenId_1_1); 110 | assert.equal(ra_after.subaccounts[0].royaltySplit, 8500); 111 | assert.equal(ra_after.subaccounts[1].royaltySplit, 1000); 112 | assert.equal(ra_after.subaccounts[2].royaltySplit, 500); 113 | }); 114 | it('Only individual account allowed as new', async () => { 115 | //8500/1000/500 116 | const ra = await token.getRoyaltyAccount(tokenId_1_1); 117 | assert.equal(ra.subaccounts[2].isIndividual, false); 118 | const updates = [ 119 | [false, 1000, 0, ra.subaccounts[2].accountId], 120 | [true, 6000, 0, accBuyer], 121 | [false, 2000, 0, accNewRoyalty], 122 | ]; 123 | await truffleAssert.reverts(token.updateRoyaltyAccount(tokenId_1_1, updates, { from: accBuyer }), 'New subaccounts must be individual'); 124 | }); 125 | it('Token owner transfer royaltySplit back to parent and split royalty to new account', async () => { 126 | //8500/1000/500 >> 6000/1000/1000/2000 127 | const ra = await token.getRoyaltyAccount(tokenId_1_1); 128 | assert.equal(ra.subaccounts[2].isIndividual, false); 129 | const updates = [ 130 | [false, 1000, 0, ra.subaccounts[2].accountId], 131 | [true, 6000, 0, accBuyer], 132 | [true, 2000, 0, accNewRoyalty], 133 | ]; 134 | await token.updateRoyaltyAccount(tokenId_1_1, updates, { from: accBuyer }); 135 | 136 | const ra_after = await token.getRoyaltyAccount(tokenId_1_1); 137 | assert.equal(ra_after.subaccounts[0].royaltySplit, 6000); 138 | assert.equal(ra_after.subaccounts[1].royaltySplit, 1000); 139 | assert.equal(ra_after.subaccounts[2].royaltySplit, 1000); 140 | assert.equal(ra_after.subaccounts[3].royaltySplit, 2000); 141 | }); 142 | it('Royalty split for TT + minimal must be <= 100%', async () => { 143 | await truffleAssert.reverts(token.updateRAccountLimits(5, 9500, { from: accAdmin }), 'Royalty Split to TT + Minimal Split is > 100%'); 144 | }); 145 | it('Update royalty limits to 5 max subaccount and 5% min royalty split', async () => { 146 | await token.updateRAccountLimits(5, 500, { from: accAdmin }); 147 | }); 148 | it('Token owner can not split royalty less than limit (5%)', async () => { 149 | const ra = await token.getRoyaltyAccount(tokenId_1_1); 150 | assert.equal(ra.subaccounts[2].isIndividual, false); 151 | const updates = [ 152 | [true, 7900, 0, accBuyer], 153 | [true, 100, 0, accNewRoyalty], 154 | ]; 155 | await truffleAssert.reverts(token.updateRoyaltyAccount(tokenId_1_1, updates, { from: accBuyer }), 'Royalty Split is smaller then set limit'); 156 | }); 157 | it('Token owner can not split royalty to more than limit account numbers (5)', async () => { 158 | //Token already have 4 subaccounts (Parent royalty, TT fee, ) 159 | const ra = await token.getRoyaltyAccount(tokenId_1_1); 160 | assert.equal(ra.subaccounts[2].isIndividual, false); 161 | const updates = [ 162 | [true, 2000, 0, accBuyer], 163 | [true, 1000, 0, accounts[5]], 164 | [true, 1000, 0, accounts[6]], 165 | [true, 1000, 0, accounts[7]], 166 | [true, 1000, 0, accounts[8]], 167 | ]; 168 | await truffleAssert.reverts(token.updateRoyaltyAccount(tokenId_1_1, updates, { from: accBuyer }), 'Too many Royalty subaccounts'); 169 | }); 170 | }); 171 | }); 172 | -------------------------------------------------------------------------------- /test/scenarios/sellWithZeroRoyalty.test.js: -------------------------------------------------------------------------------- 1 | const RoyaltyBearingToken = artifacts.require('RoyaltyBearingToken'); 2 | const SomeERC20_1 = artifacts.require('SomeERC20_1'); 3 | const SomeERC20_2 = artifacts.require('SomeERC20_2'); 4 | 5 | contract('RoyaltyBearingToken', (accounts) => { 6 | const accAdmin = accounts[0]; 7 | const accOwner1 = accounts[1]; 8 | const accOwner2 = accounts[2]; 9 | const accOwner3 = accounts[3]; 10 | const accOwner4 = accounts[4]; 11 | const accBuyer = accounts[5]; 12 | const accSeller = accounts[6]; 13 | 14 | const costOfNFT = 100; 15 | const tokenId = 4; 16 | 17 | let token; 18 | let someToken1; 19 | let someToken2; 20 | 21 | before(async () => { 22 | someToken1 = await SomeERC20_1.deployed(); 23 | someToken2 = await SomeERC20_2.deployed(); 24 | token = await RoyaltyBearingToken.deployed(); 25 | 26 | //Mint some ERC20 tokens 27 | await someToken2.mint(accBuyer, 100000000, { from: accAdmin }); 28 | }); 29 | 30 | describe('Transfer NFT with 0% royalty', async () => { 31 | it('Create new NFT with few generations and few prints. Set royalty 0%', async () => { 32 | await token.mint(accSeller, [[0x0, true, 10, 1000, 'uri_1']], 'ST2', { from: accAdmin }); 33 | await token.mint(accSeller, [[1, true, 10, 1000, 'uri_1']], 'ST2', { from: accAdmin }); 34 | await token.mint(accSeller, [[2, true, 10, 0, 'uri_1']], 'ST2', { from: accAdmin }); // id=3 royalty from children = 0% 35 | await token.mint(accSeller, [[3, true, 10, 1000, 'uri_1']], 'ST2', { from: accAdmin }); //id=4 royalty to parent = 0% 36 | }); 37 | 38 | it('seller make listNFT', async () => { 39 | await token.listNFT([tokenId], costOfNFT, 'ST2', { from: accSeller }); 40 | }); 41 | 42 | it('buyer (Bob) approve ERC20 transfer for NFT Contract', async () => { 43 | await someToken2.approve(token.address, costOfNFT, { from: accBuyer }); 44 | assert.equal((await someToken2.allowance(accBuyer, token.address, { from: accBuyer })).toString(), costOfNFT); 45 | }); 46 | it('buyer execute the ERC20 payment with trxnt = 0 and buy tokens', async () => { 47 | await token.executePayment(accOwner4, accSeller, [tokenId], costOfNFT, 'ST2', 0, { from: accBuyer }); 48 | assert.equal(await token.getApproved(tokenId), accBuyer, 'Token approved for owner'); 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /test/stressBatchMint.test.js: -------------------------------------------------------------------------------- 1 | const RoyaltyBearingToken = artifacts.require('RoyaltyBearingToken'); 2 | const SomeERC20_1 = artifacts.require('SomeERC20_1'); 3 | const SomeERC20_2 = artifacts.require('SomeERC20_2'); 4 | 5 | contract('RoyaltyBearingToken', (accounts) => { 6 | const accAdmin = accounts[0]; 7 | const accOwner1 = accounts[1]; 8 | const accOwner2 = accounts[2]; 9 | const accOwner3 = accounts[3]; 10 | const accOwner4 = accounts[4]; 11 | const accBuyer = accounts[5]; 12 | const accSeller = accounts[6]; 13 | 14 | const costOfNFT = 100; 15 | const tokenRootId = 1; 16 | const tokenId_1 = 2; 17 | const tokenId_2 = 3; 18 | 19 | const maxChildren = 10000000; 20 | const steps = [10, 20, 40, 50, 60, 70, 80, 90, 100]; 21 | 22 | let token; 23 | let someToken1; 24 | let someToken2; 25 | 26 | before(async () => { 27 | someToken1 = await SomeERC20_1.deployed(); 28 | someToken2 = await SomeERC20_2.deployed(); 29 | token = await RoyaltyBearingToken.deployed(); 30 | }); 31 | 32 | describe('Stress test batch token mint', async () => { 33 | it('updateMaxGenerations success', async () => { 34 | await token.updateMaxGenerations(5000000, { from: accAdmin }); 35 | }); 36 | 37 | it('mint root token to accOwner1', async () => { 38 | await token.mint(accOwner1, [[0x0, true, maxChildren, 100, 'uri_1']], 'ETH', { from: accAdmin }); 39 | assert.equal((await token.balanceOf(token.address)).toString(), 1, 'Token balance must changed'); 40 | assert.equal(await token.getApproved(tokenRootId), accOwner1, 'Token approved for owner'); 41 | }); 42 | 43 | for (let n = 0; n < steps.length; n++) { 44 | const count = steps[n]; 45 | const tokens = []; 46 | for (let i = 0; i < count; i++) { 47 | tokens.push([1, true, maxChildren, 100, 'uri_' + (i + 1)]); 48 | } 49 | it(`mint ${count} children`, async () => { 50 | //const balanceBefore = (await token.balanceOf(token.address)).toNumber(); 51 | await token.mint(accOwner1, tokens, 'ETH', { from: accAdmin }); 52 | //const balanceAfter = (await token.balanceOf(token.address)).toNumber(); 53 | 54 | //assert.equal(balanceAfter - balanceBefore, accOwner1, 'Token approved for owner'); 55 | }); 56 | } 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /test/stressDeepRoyalty.test.js: -------------------------------------------------------------------------------- 1 | const RoyaltyBearingToken = artifacts.require('RoyaltyBearingToken'); 2 | const SomeERC20_1 = artifacts.require('SomeERC20_1'); 3 | const SomeERC20_2 = artifacts.require('SomeERC20_2'); 4 | 5 | contract('RoyaltyBearingToken', (accounts) => { 6 | const accAdmin = accounts[0]; 7 | const accOwner1 = accounts[1]; 8 | const accOwner2 = accounts[2]; 9 | const accOwner3 = accounts[3]; 10 | const accOwner4 = accounts[4]; 11 | const accBuyer = accounts[5]; 12 | const accSeller = accounts[6]; 13 | 14 | const costOfNFT = 100; 15 | const tokenRootId = 1; 16 | const tokenId_1 = 2; 17 | const tokenId_2 = 3; 18 | 19 | const maxChildren = 10000000; 20 | const bathCount = 10; 21 | const bathSize = 10; 22 | 23 | let token; 24 | let someToken1; 25 | let someToken2; 26 | 27 | before(async () => { 28 | someToken1 = await SomeERC20_1.deployed(); 29 | someToken2 = await SomeERC20_2.deployed(); 30 | token = await RoyaltyBearingToken.deployed(); 31 | 32 | //Mint some ERC20 tokens 33 | await someToken2.mint(accBuyer, 100000000, { from: accAdmin }); 34 | }); 35 | 36 | describe('Stress test royalty calculation', async () => { 37 | it('updateMaxGenerations success', async () => { 38 | await token.updateMaxGenerations(5000000, { from: accAdmin }); 39 | }); 40 | it('mint root token to accOwner1', async () => { 41 | await token.mint(accOwner1, [[0x0, true, maxChildren, 100, 'uri_1']], 'ST2', { from: accAdmin }); 42 | assert.equal((await token.balanceOf(token.address)).toString(), 1, 'Token balance must changed'); 43 | assert.equal(await token.getApproved(tokenRootId), accOwner1, 'Token approved for owner'); 44 | }); 45 | 46 | it(`mint ${bathSize * bathCount} nested children`, async () => { 47 | let lastId = 1; 48 | for (let n = 0; n < bathCount; n++) { 49 | const tokens = []; 50 | for (let i = 0; i < bathSize; i++) { 51 | tokens.push([lastId, true, maxChildren, 100, 'uri_' + (lastId + 1)]); 52 | lastId++; 53 | } 54 | await token.mint(accSeller, tokens, 'ST2', { from: accAdmin }); 55 | } 56 | }); 57 | const step = 10; 58 | for (let level = step; level < bathSize * bathCount; level += step) { 59 | it(`transfer token with ${level} nesting`, async () => { 60 | const tokenId = level; 61 | await token.listNFT([tokenId], costOfNFT, 'ST2', { from: accSeller }); 62 | await someToken2.approve(token.address, costOfNFT, { from: accBuyer }); 63 | await token.executePayment(accOwner4, accSeller, [tokenId], costOfNFT, 'ST2', 0, { from: accBuyer }); 64 | 65 | }); 66 | } 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /test/transferByERC20_trxntype_0.test.js: -------------------------------------------------------------------------------- 1 | const RoyaltyBearingToken = artifacts.require('RoyaltyBearingToken'); 2 | const SomeERC20_1 = artifacts.require('SomeERC20_1'); 3 | const SomeERC20_2 = artifacts.require('SomeERC20_2'); 4 | 5 | contract('RoyaltyBearingToken', (accounts) => { 6 | const accAdmin = accounts[0]; 7 | const accOwner1 = accounts[1]; 8 | const accOwner2 = accounts[2]; 9 | const accOwner3 = accounts[3]; 10 | const accOwner4 = accounts[4]; 11 | const accBuyer = accounts[5]; 12 | const accSeller = accounts[6]; 13 | 14 | const costOfNFT = 100; 15 | const tokenRootId = 1; 16 | const tokenId_1 = 2; 17 | const tokenId_2 = 3; 18 | 19 | let token; 20 | let someToken1; 21 | let someToken2; 22 | 23 | before(async () => { 24 | someToken1 = await SomeERC20_1.deployed(); 25 | someToken2 = await SomeERC20_2.deployed(); 26 | token = await RoyaltyBearingToken.deployed(); 27 | 28 | //Mint some ERC20 tokens 29 | await someToken2.mint(accBuyer, 100000000, { from: accAdmin }); 30 | }); 31 | 32 | describe('Transfer NFT token using ERC20', async () => { 33 | before('', async () => {}); 34 | 35 | it('mint root token to accOwner1', async () => { 36 | await token.mint(accOwner1, [[0x0, true, 10, 1000,"uri_1"]], 'ST2', { from: accAdmin }); 37 | assert.equal((await token.balanceOf(token.address)).toString(), 1, 'Token balance must changed'); 38 | assert.equal(await token.getApproved(tokenRootId), accOwner1, 'Token approved for owner'); 39 | }); 40 | 41 | it('mint first offspring token to accSeller', async () => { 42 | await token.mint( 43 | accSeller, 44 | [ 45 | [tokenRootId, true, 10, 200,"uri_2"], 46 | [tokenRootId, true, 10, 200,"uri_3"], 47 | ], 48 | 'ST2', 49 | { from: accAdmin }, 50 | ); 51 | assert.equal((await token.balanceOf(token.address)).toString(), 1 + 2, 'Token balance must changed'); 52 | assert.equal(await token.getApproved(tokenId_1), accSeller, 'Token approved for owner'); 53 | assert.equal(await token.getApproved(tokenId_2), accSeller, 'Token approved for owner'); 54 | }); 55 | 56 | it('seller make listNFT', async () => { 57 | await token.listNFT([tokenId_1, tokenId_2], costOfNFT, 'ST2', { from: accSeller }); 58 | }); 59 | 60 | it('buyer (Bob) approve ERC20 transfer for NFT Contract', async () => { 61 | await someToken2.approve(token.address, costOfNFT, { from: accBuyer }); 62 | assert.equal((await someToken2.allowance(accBuyer, token.address, { from: accBuyer })).toString(), costOfNFT); 63 | }); 64 | 65 | it('buyer execute the ERC20 payment with trxnt = 0 and buy tokens', async () => { 66 | const balanceBefore = { 67 | accSeller: await someToken2.balanceOf(accSeller), 68 | accBuyer: await someToken2.balanceOf(accBuyer), 69 | }; 70 | 71 | const royaltyBefore = { 72 | t1: await token.getRoyaltyAccount(tokenId_1), 73 | t2: await token.getRoyaltyAccount(tokenId_2), 74 | }; 75 | assert.equal(royaltyBefore.t1.subaccounts[0].accountId, accSeller); 76 | assert.equal(royaltyBefore.t2.subaccounts[0].accountId, accSeller); 77 | 78 | await token.executePayment(accOwner4, accSeller, [tokenId_1, tokenId_2], costOfNFT, 'ST2', 0, { from: accBuyer }); 79 | 80 | assert.equal(await token.getApproved(tokenId_1), accBuyer, 'Token approved for owner'); 81 | assert.equal(await token.getApproved(tokenId_2), accBuyer, 'Token approved for owner'); 82 | 83 | const royaltyAfter = { 84 | root: await token.getRoyaltyAccount(tokenRootId), 85 | t1: await token.getRoyaltyAccount(tokenId_1), 86 | t2: await token.getRoyaltyAccount(tokenId_2), 87 | }; 88 | assert.equal(royaltyAfter.t1.subaccounts[0].accountId, accBuyer); 89 | assert.equal(royaltyAfter.t2.subaccounts[0].accountId, accBuyer); 90 | 91 | const balanceAfter = { 92 | accSeller: await someToken2.balanceOf(accSeller), 93 | accBuyer: await someToken2.balanceOf(accBuyer), 94 | }; 95 | assert.equal(royaltyAfter.root.subaccounts[0].royaltyBalance, 8, 'Royalty for parent must received'); //(90% of 5) x2 for owner 96 | assert.equal(royaltyAfter.root.subaccounts[1].royaltyBalance, 2, 'TT Royalty for parent must received'); //(10% of 5) x2 for TT 97 | assert.equal(royaltyAfter.t1.subaccounts[1].royaltyBalance, 5, 'TT Royalty for token1 must received'); //(10% of 50) for TT 98 | assert.equal(royaltyAfter.t2.subaccounts[1].royaltyBalance, 5, 'TT Royalty for token2 must received'); //(10% of 50) for TT 99 | 100 | assert.equal(balanceAfter.accSeller.toNumber() - balanceBefore.accSeller.toNumber(), costOfNFT - 10 - 10, 'Payout for Seller'); 101 | }); 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /test/transferByERC20_trxntype_1.test.js: -------------------------------------------------------------------------------- 1 | const RoyaltyBearingToken = artifacts.require('RoyaltyBearingToken'); 2 | const SomeERC20_1 = artifacts.require('SomeERC20_1'); 3 | const SomeERC20_2 = artifacts.require('SomeERC20_2'); 4 | 5 | contract('RoyaltyBearingToken', (accounts) => { 6 | const accAdmin = accounts[0]; 7 | const accOwner1 = accounts[1]; 8 | const accOwner2 = accounts[2]; 9 | const accOwner3 = accounts[3]; 10 | const accOwner4 = accounts[4]; 11 | const accBuyer = accounts[5]; 12 | const accSeller = accounts[6]; 13 | 14 | const costOfNFT = 100; 15 | const tokenRootId = 1; 16 | const tokenId_1 = 2; 17 | const tokenId_2 = 3; 18 | 19 | let token; 20 | let someToken1; 21 | let someToken2; 22 | 23 | before(async () => { 24 | someToken1 = await SomeERC20_1.deployed(); 25 | someToken2 = await SomeERC20_2.deployed(); 26 | token = await RoyaltyBearingToken.deployed(); 27 | 28 | //Mint some ERC20 tokens 29 | await someToken2.mint(accBuyer, 100000000, { from: accAdmin }); 30 | }); 31 | 32 | describe('Transfer NFT token using ERC20', async () => { 33 | before('', async () => {}); 34 | 35 | it('mint root token to accOwner1', async () => { 36 | await token.mint(accOwner1, [[0x0, true, 10, 1000,"uri_1"]], 'ST2', { from: accAdmin }); 37 | assert.equal((await token.balanceOf(token.address)).toString(), 1, 'Token balance must changed'); 38 | assert.equal(await token.getApproved(tokenRootId), accOwner1, 'Token approved for owner'); 39 | }); 40 | 41 | it('mint first offspring token to accSeller', async () => { 42 | await token.mint( 43 | accSeller, 44 | [ 45 | [tokenRootId, true, 10, 200,"uri_2"], 46 | [tokenRootId, true, 10, 200,"uri_3"], 47 | ], 48 | 'ST2', 49 | { from: accAdmin }, 50 | ); 51 | assert.equal((await token.balanceOf(token.address)).toString(), 1 + 2, 'Token balance must changed'); 52 | assert.equal(await token.getApproved(tokenId_1), accSeller, 'Token approved for owner'); 53 | assert.equal(await token.getApproved(tokenId_2), accSeller, 'Token approved for owner'); 54 | }); 55 | 56 | it('seller make listNFT', async () => { 57 | await token.listNFT([tokenId_1, tokenId_2], costOfNFT, 'ST2', { from: accSeller }); 58 | }); 59 | 60 | it('buyer (Bob) approve ERC20 transfer for NFT Contract', async () => { 61 | await someToken2.approve(token.address, costOfNFT, { from: accBuyer }); 62 | assert.equal((await someToken2.allowance(accBuyer, token.address, { from: accBuyer })).toString(), costOfNFT); 63 | }); 64 | 65 | it('buyer execute the ERC20 payment', async () => { 66 | await token.executePayment(accOwner4, accSeller, [tokenId_1, tokenId_2], costOfNFT, 'ST2', 1, { from: accBuyer }); 67 | //assert.equal((await token.checkPayment(tokenId, 'ST2', { from: accBuyer })).toString(), costOfNFT, 'Payment after transfer must changed'); 68 | }); 69 | 70 | it('seller transfer NTF token to buyer', async () => { 71 | const balanceBefore = { 72 | accSeller: await someToken2.balanceOf(accSeller), 73 | accBuyer: await someToken2.balanceOf(accBuyer), 74 | }; 75 | 76 | const royaltyBefore = { 77 | t1: await token.getRoyaltyAccount(tokenId_1), 78 | t2: await token.getRoyaltyAccount(tokenId_2), 79 | }; 80 | assert.equal(royaltyBefore.t1.subaccounts[0].accountId, accSeller); 81 | assert.equal(royaltyBefore.t2.subaccounts[0].accountId, accSeller); 82 | 83 | const chainId = await web3.eth.getChainId(); 84 | const data = web3.eth.abi.encodeParameters( 85 | ['address', 'address', 'address', 'uint256[]', 'string', 'uint256', 'address', 'uint256'], 86 | [accSeller, accBuyer, accBuyer, [tokenId_1, tokenId_2], 'ST2', costOfNFT, someToken2.address, chainId], 87 | ); 88 | //truffle fail to select valid method safeTransferFrom 89 | //await token.safeTransferFrom(accSeller, accOwner3, tokenId, data,{from:accSeller}); 90 | //workaround for this 91 | await token.methods['safeTransferFrom(address,address,uint256,bytes)'](accSeller, accBuyer, tokenId_1, data, { from: accSeller }); 92 | 93 | assert.equal(await token.getApproved(tokenId_1), accBuyer, 'Token approved for owner'); 94 | assert.equal(await token.getApproved(tokenId_2), accBuyer, 'Token approved for owner'); 95 | 96 | const royaltyAfter = { 97 | root: await token.getRoyaltyAccount(tokenRootId), 98 | t1: await token.getRoyaltyAccount(tokenId_1), 99 | t2: await token.getRoyaltyAccount(tokenId_2), 100 | }; 101 | assert.equal(royaltyAfter.t1.subaccounts[0].accountId, accBuyer); 102 | assert.equal(royaltyAfter.t2.subaccounts[0].accountId, accBuyer); 103 | 104 | const balanceAfter = { 105 | accSeller: await someToken2.balanceOf(accSeller), 106 | accBuyer: await someToken2.balanceOf(accBuyer), 107 | }; 108 | assert.equal(royaltyAfter.root.subaccounts[0].royaltyBalance, 8, 'Royalty for parent must received'); //(90% of 5) x2 for owner 109 | assert.equal(royaltyAfter.root.subaccounts[1].royaltyBalance, 2, 'TT Royalty for parent must received'); //(10% of 5) x2 for TT 110 | assert.equal(royaltyAfter.t1.subaccounts[1].royaltyBalance, 5, 'TT Royalty for token1 must received'); //(10% of 50) for TT 111 | assert.equal(royaltyAfter.t2.subaccounts[1].royaltyBalance, 5, 'TT Royalty for token2 must received'); //(10% of 50) for TT 112 | assert.equal(balanceAfter.accSeller.toNumber() - balanceBefore.accSeller.toNumber(), costOfNFT - 10 - 10, 'Payout for Seller'); 113 | }); 114 | }); 115 | }); 116 | -------------------------------------------------------------------------------- /test/transferByETH_trxnttype_0.test.js: -------------------------------------------------------------------------------- 1 | const RoyaltyBearingToken = artifacts.require('RoyaltyBearingToken'); 2 | const SomeERC20_1 = artifacts.require('SomeERC20_1'); 3 | const SomeERC20_2 = artifacts.require('SomeERC20_2'); 4 | 5 | contract('RoyaltyBearingToken', (accounts) => { 6 | const accAdmin = accounts[0]; 7 | const accOwner1 = accounts[1]; 8 | const accOwner2 = accounts[2]; 9 | const accOwner3 = accounts[3]; 10 | const accOwner4 = accounts[4]; 11 | const accBuyer = accounts[5]; 12 | const accSeller = accounts[6]; 13 | console.log('seller', accSeller); 14 | 15 | const costOfNFT = 100; 16 | const tokenRootId = 1; 17 | const tokenId_1 = 2; 18 | const tokenId_2 = 3; 19 | 20 | let token; 21 | let someToken1; 22 | let someToken2; 23 | 24 | before(async () => { 25 | someToken1 = await SomeERC20_1.deployed(); 26 | someToken2 = await SomeERC20_2.deployed(); 27 | token = await RoyaltyBearingToken.deployed(); 28 | }); 29 | 30 | describe('Transfer NFT token using ETH', async () => { 31 | before('', async () => {}); 32 | 33 | it('mint root token to accOwner1', async () => { 34 | await token.mint(accOwner1, [[0x0, true, 10, 1000, 'uri_1']], 'ETH', { from: accAdmin }); 35 | assert.equal((await token.balanceOf(token.address)).toString(), 1, 'Token balance must changed'); 36 | assert.equal(await token.getApproved(tokenRootId), accOwner1, 'Token approved for owner'); 37 | }); 38 | 39 | it('mint first offspring token to accSeller', async () => { 40 | await token.mint( 41 | accSeller, 42 | [ 43 | [tokenRootId, true, 10, 200, 'uri_2'], 44 | [tokenRootId, true, 10, 200, 'uri_3'], 45 | ], 46 | 'ETH', 47 | { from: accAdmin }, 48 | ); 49 | assert.equal((await token.balanceOf(token.address)).toString(), 1 + 2, 'Token balance must changed'); 50 | assert.equal(await token.getApproved(tokenId_1), accSeller, 'Token approved for owner'); 51 | assert.equal(await token.getApproved(tokenId_2), accSeller, 'Token approved for owner'); 52 | }); 53 | 54 | it('seller make listNFT', async () => { 55 | await token.listNFT([tokenId_1, tokenId_2], costOfNFT, 'ETH', { from: accSeller }); 56 | }); 57 | 58 | it('buyer execute the ETH payment to Token contract and receive tokens', async () => { 59 | const balanceBefore = { 60 | accSeller: await web3.eth.getBalance(accSeller), 61 | accBuyer: await web3.eth.getBalance(accBuyer), 62 | token: await web3.eth.getBalance(token.address), 63 | }; 64 | 65 | const royaltyBefore = { 66 | t1: await token.getRoyaltyAccount(tokenId_1), 67 | t2: await token.getRoyaltyAccount(tokenId_2), 68 | }; 69 | assert.equal(royaltyBefore.t1.subaccounts[0].accountId, accSeller); 70 | assert.equal(royaltyBefore.t2.subaccounts[0].accountId, accSeller); 71 | 72 | const data = web3.eth.abi.encodeParameters(['address', 'uint256[]', 'address', 'int256'], [accSeller, [tokenId_1, tokenId_2], accBuyer, 0]); 73 | await web3.eth.sendTransaction({ from: accBuyer, to: token.address, value: costOfNFT, data: data, gas: 6000000 }); 74 | 75 | const royaltyAfter = { 76 | root: await token.getRoyaltyAccount(tokenRootId), 77 | t1: await token.getRoyaltyAccount(tokenId_1), 78 | t2: await token.getRoyaltyAccount(tokenId_2), 79 | }; 80 | assert.equal(royaltyAfter.t1.subaccounts[0].accountId, accBuyer); 81 | assert.equal(royaltyAfter.t2.subaccounts[0].accountId, accBuyer); 82 | 83 | const balanceAfter = { 84 | accSeller: await web3.eth.getBalance(accSeller), 85 | accBuyer: await web3.eth.getBalance(accBuyer), 86 | token: await web3.eth.getBalance(token.address), 87 | }; 88 | 89 | assert.equal(royaltyAfter.root.subaccounts[0].royaltyBalance, 8, 'Royalty for parent must received'); //(90% of 5) x2 for owner 90 | assert.equal(royaltyAfter.root.subaccounts[1].royaltyBalance, 2, 'TT Royalty for parent must received'); //(10% of 5) x2 for TT 91 | assert.equal(royaltyAfter.t1.subaccounts[1].royaltyBalance, 5, 'TT Royalty for token1 must received'); //(10% of 50) for TT 92 | assert.equal(royaltyAfter.t2.subaccounts[1].royaltyBalance, 5, 'TT Royalty for token2 must received'); //(10% of 50) for TT 93 | assert.equal(balanceAfter.token - balanceBefore.token, 10 + 10, 'Balance changed only for royalty'); 94 | }); 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /test/transferByETH_trxnttype_1.test.js: -------------------------------------------------------------------------------- 1 | const RoyaltyBearingToken = artifacts.require('RoyaltyBearingToken'); 2 | const SomeERC20_1 = artifacts.require('SomeERC20_1'); 3 | const SomeERC20_2 = artifacts.require('SomeERC20_2'); 4 | 5 | contract('RoyaltyBearingToken', (accounts) => { 6 | const accAdmin = accounts[0]; 7 | const accOwner1 = accounts[1]; 8 | const accOwner2 = accounts[2]; 9 | const accOwner3 = accounts[3]; 10 | const accOwner4 = accounts[4]; 11 | const accBuyer = accounts[5]; 12 | const accSeller = accounts[6]; 13 | 14 | const costOfNFT = 100; 15 | const tokenRootId = 1; 16 | const tokenId_1 = 2; 17 | const tokenId_2 = 3; 18 | 19 | let token; 20 | let someToken1; 21 | let someToken2; 22 | 23 | before(async () => { 24 | someToken1 = await SomeERC20_1.deployed(); 25 | someToken2 = await SomeERC20_2.deployed(); 26 | token = await RoyaltyBearingToken.deployed(); 27 | }); 28 | 29 | describe('Transfer NFT token using ETH', async () => { 30 | before('', async () => {}); 31 | 32 | it('mint root token to accOwner1', async () => { 33 | await token.mint(accOwner1, [[0x0, true, 10, 1000,"uri_1"]], 'ETH', { from: accAdmin }); 34 | assert.equal((await token.balanceOf(token.address)).toString(), 1, 'Token balance must changed'); 35 | assert.equal(await token.getApproved(tokenRootId), accOwner1, 'Token approved for owner'); 36 | }); 37 | 38 | it('mint first offspring token to accSeller', async () => { 39 | await token.mint( 40 | accSeller, 41 | [ 42 | [tokenRootId, true, 10, 200,"uri_2"], 43 | [tokenRootId, true, 10, 200,"uri_3"], 44 | ], 45 | 'ETH', 46 | { from: accAdmin }, 47 | ); 48 | assert.equal((await token.balanceOf(token.address)).toString(), 1 + 2, 'Token balance must changed'); 49 | assert.equal(await token.getApproved(tokenId_1), accSeller, 'Token approved for owner'); 50 | assert.equal(await token.getApproved(tokenId_2), accSeller, 'Token approved for owner'); 51 | }); 52 | 53 | it('seller make listNFT', async () => { 54 | await token.listNFT([tokenId_1, tokenId_2], costOfNFT, 'ETH', { from: accSeller }); 55 | }); 56 | 57 | it('buyer execute the ETH payment to Token contract', async () => { 58 | const data = web3.eth.abi.encodeParameters(['address', 'uint256[]', 'address', 'int256'], [accSeller, [tokenId_1, tokenId_2], accBuyer, 1]); 59 | await web3.eth.sendTransaction({ from: accBuyer, to: token.address, value: costOfNFT, data: data, gas: 1000000 }); 60 | console.log('payment', (await token.checkPayment(tokenId_1, 'ETH', accBuyer, { from: accBuyer }))); 61 | assert.equal((await token.checkPayment(tokenId_1, 'ETH', accBuyer, { from: accBuyer })).toString(), costOfNFT, 'Payment after transfer must changed'); 62 | }); 63 | 64 | it('seller transfer NTF token to buyer', async () => { 65 | const balanceBefore = { 66 | accSeller: await web3.eth.getBalance(accSeller), 67 | accBuyer: await web3.eth.getBalance(accBuyer), 68 | token: await web3.eth.getBalance(token.address), 69 | }; 70 | 71 | const royaltyBefore = { 72 | t1: await token.getRoyaltyAccount(tokenId_1), 73 | t2: await token.getRoyaltyAccount(tokenId_2), 74 | }; 75 | assert.equal(royaltyBefore.t1.subaccounts[0].accountId, accSeller); 76 | assert.equal(royaltyBefore.t2.subaccounts[0].accountId, accSeller); 77 | 78 | const chainId = await web3.eth.getChainId(); 79 | const data = web3.eth.abi.encodeParameters( 80 | ['address', 'address', 'address', 'uint256[]', 'string', 'uint256', 'address', 'uint256'], 81 | [accSeller, accBuyer, accBuyer, [tokenId_1, tokenId_2], 'ETH', costOfNFT, token.address, chainId], 82 | ); 83 | //truffle fail to select valid method safeTransferFrom 84 | //await token.safeTransferFrom(accSeller, accOwner3, tokenId, data,{from:accSeller}); 85 | //workaround for this 86 | await token.methods['safeTransferFrom(address,address,uint256,bytes)'](accSeller, accBuyer, tokenId_1, data, { from: accSeller }); 87 | 88 | const royaltyAfter = { 89 | root: await token.getRoyaltyAccount(tokenRootId), 90 | t1: await token.getRoyaltyAccount(tokenId_1), 91 | t2: await token.getRoyaltyAccount(tokenId_2), 92 | }; 93 | assert.equal(royaltyAfter.t1.subaccounts[0].accountId, accBuyer); 94 | assert.equal(royaltyAfter.t2.subaccounts[0].accountId, accBuyer); 95 | 96 | const balanceAfter = { 97 | accSeller: await web3.eth.getBalance(accSeller), 98 | accBuyer: await web3.eth.getBalance(accBuyer), 99 | token: await web3.eth.getBalance(token.address), 100 | }; 101 | 102 | assert.equal(royaltyAfter.root.subaccounts[0].royaltyBalance, 8, 'Royalty for parent must received'); //(90% of 5) x2 for owner 103 | assert.equal(royaltyAfter.root.subaccounts[1].royaltyBalance, 2, 'TT Royalty for parent must received'); //(10% of 5) x2 for TT 104 | assert.equal(royaltyAfter.t1.subaccounts[1].royaltyBalance, 5, 'TT Royalty for token1 must received'); //(10% of 50) for TT 105 | assert.equal(royaltyAfter.t2.subaccounts[1].royaltyBalance, 5, 'TT Royalty for token2 must received'); //(10% of 50) for TT 106 | 107 | assert.equal(balanceBefore.token - balanceAfter.token, costOfNFT - 10 - 10, 'Payout for Seller'); 108 | }); 109 | }); 110 | }); 111 | -------------------------------------------------------------------------------- /truffle-config.js: -------------------------------------------------------------------------------- 1 | const HDWalletProvider = require('@truffle/hdwallet-provider'); 2 | const fs = require('fs'); 3 | const mnemonic = fs.readFileSync('.secret').toString().trim(); 4 | 5 | module.exports = { 6 | plugins: ['truffle-contract-size', 'solidity-coverage'], 7 | /** 8 | * Networks define how you connect to your ethereum client and let you set the 9 | * defaults web3 uses to send transactions. If you don't specify one truffle 10 | * will spin up a development blockchain for you on port 9545 when you 11 | * run `develop` or `test`. You can ask a truffle command to use a specific 12 | * network from the command line, e.g 13 | * 14 | * $ truffle test --network 15 | */ 16 | 17 | networks: { 18 | // Useful for testing. The `development` name is special - truffle uses it by default 19 | // if it's defined here and no other network is specified at the command line. 20 | // You should run a client (like ganache-cli, geth or parity) in a separate terminal 21 | // tab if you use this network and you must also set the `host`, `port` and `network_id` 22 | // options below to some value. 23 | // 24 | development: { 25 | host: '127.0.0.1', // Localhost (default: none) 26 | port: 7545, // Standard Ethereum port (default: none) 27 | network_id: '*', // Any network (default: none) 28 | allowUnlimitedContractSize: true, 29 | }, 30 | 31 | mumbai: { 32 | provider: () => new HDWalletProvider(mnemonic, `https://matic-mumbai.chainstacklabs.com`), 33 | network_id: 80001, 34 | confirmations: 2, 35 | timeoutBlocks: 20000, 36 | skipDryRun: true, 37 | gasPrice: 0, 38 | }, 39 | // Another network with more advanced options... 40 | // advanced: { 41 | // port: 8777, // Custom port 42 | // network_id: 1342, // Custom network 43 | // gas: 8500000, // Gas sent with each transaction (default: ~6700000) 44 | // gasPrice: 20000000000, // 20 gwei (in wei) (default: 100 gwei) 45 | // from:
, // Account to send txs from (default: accounts[0]) 46 | // websocket: true // Enable EventEmitter interface for web3 (default: false) 47 | // }, 48 | // Useful for deploying to a public network. 49 | // NB: It's important to wrap the provider as a function. 50 | // ropsten: { 51 | // provider: () => new HDWalletProvider(mnemonic, `https://ropsten.infura.io/v3/YOUR-PROJECT-ID`), 52 | // network_id: 3, // Ropsten's id 53 | // gas: 5500000, // Ropsten has a lower block limit than mainnet 54 | // confirmations: 2, // # of confs to wait between deployments. (default: 0) 55 | // timeoutBlocks: 200, // # of blocks before a deployment times out (minimum/default: 50) 56 | // skipDryRun: true // Skip dry run before migrations? (default: false for public nets ) 57 | // }, 58 | // Useful for private networks 59 | // private: { 60 | // provider: () => new HDWalletProvider(mnemonic, `https://network.io`), 61 | // network_id: 2111, // This network is yours, in the cloud. 62 | // production: true // Treats this network as if it was a public net. (default: false) 63 | // } 64 | }, 65 | 66 | // Set default mocha options here, use special reporters etc. 67 | mocha: { 68 | reporter: 'eth-gas-reporter', 69 | reporterOptions: { 70 | excludeContracts: ['Migrations'], 71 | //uncomment for matic testnet 72 | //url:"https://matic-mumbai.chainstacklabs.com" 73 | }, 74 | timeout: 3000000, 75 | }, 76 | 77 | // Configure your compilers 78 | compilers: { 79 | solc: { 80 | version: '0.8.10', // Fetch exact version from solc-bin (default: truffle's version) 81 | // docker: true, // Use "0.5.1" you've installed locally with docker (default: false) 82 | settings: { 83 | // See the solidity docs for advice about optimization and evmVersion 84 | optimizer: { 85 | enabled: true, 86 | runs: 100, 87 | }, 88 | evmVersion: 'istanbul', 89 | }, 90 | }, 91 | }, 92 | 93 | // Truffle DB is currently disabled by default; to enable it, change enabled: 94 | // false to enabled: true. The default storage location can also be 95 | // overridden by specifying the adapter settings, as shown in the commented code below. 96 | // 97 | // NOTE: It is not possible to migrate your contracts to truffle DB and you should 98 | // make a backup of your artifacts to a safe location before enabling this feature. 99 | // 100 | // After you backed up your artifacts you can utilize db by running migrate as follows: 101 | // $ truffle migrate --reset --compile-all 102 | // 103 | // db: { 104 | // enabled: false, 105 | // host: "127.0.0.1", 106 | // adapter: { 107 | // name: "sqlite", 108 | // settings: { 109 | // directory: ".db" 110 | // } 111 | // } 112 | // } 113 | }; 114 | --------------------------------------------------------------------------------