├── .editorconfig ├── .eslintrc.js ├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ └── node.js.yml ├── .gitignore ├── .npmignore ├── .solhint.json ├── BUG_BOUNTY.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── hardhat.config.js ├── package-lock.json ├── package.json └── src ├── contracts ├── mocks │ ├── nf-token-enumerable-mock.sol │ ├── nf-token-metadata-base-uri-mock.sol │ ├── nf-token-metadata-enumerable-mock.sol │ ├── nf-token-metadata-mock.sol │ └── nf-token-mock.sol ├── ownership │ └── ownable.sol ├── tokens │ ├── erc721-enumerable.sol │ ├── erc721-metadata.sol │ ├── erc721-token-receiver.sol │ ├── erc721.sol │ ├── nf-token-enumerable.sol │ ├── nf-token-metadata.sol │ └── nf-token.sol └── utils │ ├── address-utils.sol │ ├── erc165.sol │ └── supports-interface.sol └── tests ├── mocks ├── address-utils-mock.sol ├── nf-token-enumerable-test-mock.sol ├── nf-token-metadata-enumerable-test-mock.sol ├── nf-token-metadata-test-mock.sol ├── nf-token-receiver-test-mock.sol ├── nf-token-test-mock.sol └── sends-to-self-on-construct.sol ├── tokens ├── nf-token-enumerable.js ├── nf-token-metadata-enumerable.js ├── nf-token-metadata.js └── nf-token.js └── utils └── address-utils.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | max_line_length = 100 7 | insert_final_newline = false 8 | trim_trailing_whitespace = true 9 | end_of_line = lf 10 | charset = utf-8 11 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'env': { 3 | 'es6': false, 4 | 'node': true, 5 | }, 6 | 'parserOptions': { 7 | 'sourceType': 'module', 8 | 'ecmaVersion': 8, 9 | }, 10 | 'rules': { 11 | 'indent': [ 12 | 'error', 13 | 2, 14 | ], 15 | 'linebreak-style': [ 16 | 'error', 17 | 'unix', 18 | ], 19 | 'quotes': [ 20 | 'error', 21 | 'single', 22 | ], 23 | 'semi': [ 24 | 'error', 25 | 'always', 26 | ], 27 | }, 28 | }; -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [xpepermint, MoMannn, fulldecent] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: [push, pull_request] 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | node-version: [16] # https://nodejs.org/en/about/releases/ 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v2 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - run: | 21 | npm ci 22 | npm run solhint && npm test 23 | npm run coverage 24 | - name: Codecov 25 | uses: codecov/codecov-action@v2 26 | with: 27 | flags: unittests 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .vscode 3 | node_modules 4 | dist 5 | abi 6 | build 7 | # Config files, 8 | .idea/ 9 | .vscode 10 | # Node.js 11 | node_modules 12 | package.json 13 | package-lock.json 14 | #Hardhat files 15 | bin 16 | cache 17 | artifacts 18 | data/ 19 | #Solidity 20 | soljson* 21 | .tmp* 22 | metadata/ 23 | #coverage 24 | coverage.json 25 | coverage -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .vscode 3 | node_modules -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:default", 3 | "rules": { 4 | "max-line-length": ["warn", 100] 5 | } 6 | } -------------------------------------------------------------------------------- /BUG_BOUNTY.md: -------------------------------------------------------------------------------- 1 | # ERC-721 Token Reference Implementation Bug Bounty 2 | 3 | *Bug bounty process and how you can get rewarded for finding issues with the ERC-721 Token Reference Implementation.* 4 | 5 | ## Leaderboard 6 | 7 | The leaderboard will list all the contributors to this bounty. 8 | 9 | | Bug report | Severity | Researcher 10 | |-|-|-| 11 | | #242 | Low severity | @sam-at-luther | 12 | 13 | ## Sponsors 14 | 15 | **Sponsor this bug bounty if you support ERC-721**. This means you will commit to paying researchers that demonstrate a problem. Contact us at [bounty@nibbstack.com](mailto:bounty@nibbstack.com) if interested. Thank you. 16 | 17 | ## Scope of this bounty program 18 | 19 | This bounty is open for an unlimited time. A previous limited-time bounty program was: 20 | 21 | * [Round 1](https://github.com/nibbstack/erc721/issues/46) — **2018-05-16 at 00:01 CET** to **2018-07-16 at 23:59 CET** 22 | 23 | Help us find any problems with this contract and with ERC-721 in general. This bounty program's function scope includes: 24 | 25 | - Overflow or break parts of the implementation 26 | - Steal ownership of a token 27 | - Give a token to somebody else and double-spend it or revert it to your control 28 | - Any undocumented and unintuitive behavior 29 | - Typos 30 | - Style guide violations (find the current style guide in effect referenced from [CONTRIBUTING.md](CONTRIBUTING.md)) 31 | 32 | ## Rules and rewards 33 | 34 | - Issues that have already been published here or are already disclosed to the Nibbstack team are not eligible for rewards (a corollary, the Nibbstack team members are ineligible for rewards). 35 | - Social engineering, XKCD#538 attacks, bringing down Mainnet/Infura are not in scope and will NOT be paid a reward. 36 | - Only the end-user contracts (`src/contracts/**/*.sol`) are in scope. 37 | - Only the latest released version of this project [![NPM Version](https://badge.fury.io/js/@nibbstack%2Ferc721.svg)](https://www.npmjs.com/package/@nibbstack/erc721) is in scope. 38 | - Only Ethereum mainnet is in scope. We intend to add other blockchains at a future date such as Hyperledger Burrow, Ethereum Classic, and POA Network. 39 | - [GitHub Issues](https://github.com/nibbstack/erc721/issues) is the only way to report issues and request rewards. 40 | - The Nibbstack team has complete and final judgment on the acceptability of bug reports. 41 | - This program is governed under the laws of the Republic of Slovenia, if there is a party that we are unable to pay due to trade embargoes or other restrictions, then we won't pay. But we are happy to cooperate by making alternate arrangements. 42 | 43 | Following is a [risk rating model](https://www.owasp.org/index.php/OWASP_Risk_Rating_Methodology) that judges the severity of an issue based on its likelihood and impact. 44 | 45 | | | LOW LIKELIHOOD | :left_right_arrow: | HIGH LIKELIHOOD | 46 | | --------------- | --------------- | ------------------ | -------------------- | 47 | | **HIGH IMPACT** | Medium severity | High severity | Highest severity | 48 | | :arrow_up_down: | Low severity | Medium severity | High severity | 49 | | **LOW IMPACT** | *Notable* | Low severity | Medium severity | 50 | 51 | Rewards: 52 | 53 | - **Highest severity** — full payout of the bug bounty (1 ETH) 54 | - **High severity** — partial payout of the bug bounty (0.5 ETH) 55 | - **Medium severity** — partial payout of the bug bounty (0.1 ETH) 56 | - All eligible reports (low severity or notable) are mentioned in [this thread in a leaderboard](#leaderboard) and are eligible to receive a special bug bounty tee shirt. 57 | - Additional rewards are available from [sponsors](#sponsors). In general, these will follow proportionally as the rewards above. 58 | - Nibbstack and sponsors reserve the right to deduct from the bounty pledge when the ongoing bug reports are rewarded. 59 | 60 | Examples of impact: 61 | 62 | - *High impact* — steal a token from someone else, impersonate the contract owner 63 | - *Medium impact* — cause metadata to fail so that the wrong data goes on the blockchain, or waste 5,000 gas or more in a transaction (ignoring transfer-to-self and no-op transactions) 64 | - *Low impact* — cause a transaction counterparty that carefully reads the contract documentation to make a mistake on some edge case type of transaction 65 | - *Notable* — it applies mostly to typos 66 | 67 | Examples of likelihood: 68 | 69 | * *High likelihood* — affects all users of the smart contract performing a certain function 70 | * *Medium likelihood* — affects several end-users in a scenario that happens naturally in production deployments 71 | * *Low likelihood* — affects two end users only if they are together to exploit a specially crafted transaction 72 | * *Notable* — affects developers and grammarians but not end-users 73 | 74 | How to win: 75 | 76 | - Be descriptive and detailed when describing your issue 77 | - Fix it — recommend a way to solve the problem 78 | - Include a [Hardhat test](https://hardhat.org/) case that we can reproduce 79 | 80 | Rules for bounty sponsor: 81 | 82 | - We will respond quickly to your questions 83 | - We will adjudicate all prizes quickly 84 | 85 | ## More questions 86 | 87 | * Are you recommending [*full disclosure*](https://en.wikipedia.org/wiki/Full_disclosure_(computer_security)) as a best practice? 88 | * Yes. [Well known losses](https://github.com/ethereum/EIPs/issues/223) due to problems with ERC-20 have exceeded [tens of millions USD]((https://github.com/ethereum/EIPs/issues/223)). The best defense we can offer to the community is to be transparent when issues come. The following are two references on this topic to explore further. 89 | * Schneier, Bruce. ["Damned Good Idea"](https://www.schneier.com/essay-146.html). CSO Online. Retrieved 29 April 2013. 90 | * Heiser, Jay (January 2001). ["Exposing Infosecurity Hype"](https://web.archive.org/web/20060328012516/http://infosecuritymag.techtarget.com/articles/january01/columns_curmudgeons_corner.shtml). *Information Security Mag*. TechTarget. Archived from [the original](http://infosecuritymag.techtarget.com/articles/january01/columns_curmudgeons_corner.shtml) on 28 March 2006. Retrieved 29 April 2013. 91 | * [:star: Star this repo](https://github.com/nibbstack/erc721/) if you are using this code. Surely you would want to know of any bugs as soon as possible. 92 | * If you prefer to send us a bug report privately so that a fix can be developed concurrently with the announcement you are welcome to mail us at [bounty@nibbstack.com](mailto:bounty@nibbstack.com). You are welcome to make a hashed bug report (set issue body to hash of your message). This will still be eligible for payment and recognition. 93 | 94 | * Will things change during the bounty program? 95 | * Yes, we are seeking sponsors and will add additional prizes here if that happens. 96 | * Yes, we will update the code and redeploy the contract. So, click [:star: STAR and :eye: WATCH](https://github.com/nibbstack/erc721/) above on this repo for updates. 97 | 98 | - Taxes? 99 | - If you earn so much money that you will need to fill out a tax form, then we will ask you to fill out a tax form. This whole program is subject to the laws of the Republic of Slovenia. 100 | - I read to the bottom of the file. 101 | - That's not even a question. Good, you're the type of person we're seeking. Here's a hint, you can see the [CryptoKitties bounty program](https://github.com/axiomzen/cryptokitties-bounty) and [Su Squares bounty program](https://github.com/su-squares/ethereum-contract) and everything that happened there. They were a great inspiration for this bounty. 102 | 103 | Released under the [MIT License](LICENSE). 104 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Issues 4 | 5 | We use GitHub issues to track public bugs. Please ensure your description is clear and has sufficient instructions to be able to reproduce the issue. 6 | 7 | ## Pull requests 8 | 9 | * Fork the repo and create your branch from master. 10 | * If you've added code that should be tested, add tests. 11 | * Ensure the test suite passes. 12 | 13 | ## Coding style 14 | 15 | The full style guide is available [here](https://github.com/0xcert/solidity-style-guide). 16 | 17 | ## Release process 18 | 19 | The release manager is responsible to follow the process for each release of this project. 20 | 21 | Summarize the changes and create a release using [GitHub releases](https://github.com/0xcert/solidity-style-guide/releases). Note that only major releases are required to be fully documented. 22 | 23 | ``` 24 | $ npm i 25 | $ npm test 26 | $ npm publish 27 | ``` 28 | 29 | Note: item 1 may become automated at some point and put into the continuous integration build. 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2021 Nibbstack, d.o.o. https://nibbstack.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Build Status](https://travis-ci.org/nibbstack/erc721.svg?branch=master) [![codecov](https://codecov.io/gh/nibbstack/erc721/branch/master/graph/badge.svg?token=F0tgRHyWSM)](https://codecov.io/gh/nibbstack/erc721) [![NPM Version](https://badge.fury.io/js/@0xcert%2Fethereum-erc721.svg)](https://www.npmjs.com/package/@nibbstack/erc721) [![Bug Bounty](https://img.shields.io/badge/bounty-open-2930e8.svg)](https://github.com/nibbstack/erc721/blob/master/BUG_BOUNTY.md) 2 | 3 | # ERC-721 Token — Reference Implementation 4 | 5 | **NOTICE: This repository has been transferred from 0xcert to Nibbstack and thus renamed from 0xcert/ethereum-erc721 to @nibbstack/erc721.** 6 | 7 | This is the complete reference implementation of the [ERC-721](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md) non-fungible token standard for the Ethereum and Wanchain blockchains. It is also compatible with other EVM compatible chains like Binance Smart Chain (BSC), Avalanche (AVAX) etc. This is an open-source project, complete with [Hardhat](https://hardhat.org/) testing. 8 | 9 | The purpose of this implementation is to provide a good starting point for anyone who wants to use and develop non-fungible tokens on the Ethereum and Wanchain blockchains. Instead of re-implementing the ERC-721 yourself you can use this code which has gone through multiple audits and we hope it will be extensively used by the community in the future. 10 | Note that this implementation is more restrictive then the ERC-721 standard since it does not support `payable` function calls out of the box. You are however free to add this yourself. 11 | 12 | If you are looking for a more feature-rich and advanced ERC-721 implementation, then check out the [0xcert Framework](https://github.com/0xcert/framework). 13 | 14 | ## Structure 15 | 16 | All contracts and tests are in the [src](src/) folder. There are multiple implementations and you can select between: 17 | 18 | - [`nf-token.sol`](src/contracts/tokens/nf-token.sol): This is the base ERC-721 token implementation (with support for ERC-165). 19 | - [`nf-token-metadata.sol`](src/contracts/tokens/nf-token-metadata.sol): This implements optional ERC-721 metadata features for the token contract. It implements a token name, a symbol and a distinct URI pointing to a publicly exposed ERC-721 JSON metadata file. 20 | - [`nf-token-enumerable.sol`](src/contracts/tokens/nf-token-enumerable.sol): This implements optional ERC-721 support for enumeration. It is useful if you want to know the total supply of tokens, to query a token by index, etc. 21 | 22 | Other files in the [tokens](src/contracts/tokens) and [utils](src/contracts/utils) directories named `erc*.sol` are interfaces and define the respective standards. 23 | 24 | Mock contracts showing basic contract usage are available in the [mocks](src/contracts/mocks) folder. 25 | 26 | There are also test mocks that can be seen [here](src/tests/mocks). These are specifically made to test different edge cases and behaviours and should NOT be used as a reference for implementation. 27 | 28 | ## Requirements 29 | 30 | * NodeJS 12+ is supported 31 | * Windows, Linux or macOS 32 | 33 | ## Installation 34 | 35 | ### npm 36 | 37 | *This is the recommended installation method if you want to use this package in your JavaScript project.* 38 | 39 | This project is [released as an npm module](https://www.npmjs.com/package/@nibbstack/erc721). You must install it using the `npm` command: 40 | 41 | ``` 42 | $ npm install @nibbstack/erc721@2.6.1 43 | ``` 44 | 45 | ### Source 46 | 47 | *This is the recommended installation method if you want to improve the `nibbstack/erc721` project.* 48 | 49 | Clone this repository and install the required `npm` dependencies: 50 | 51 | ``` 52 | $ git clone git@github.com:nibbstack/erc721.git 53 | $ cd ethereum-erc721 54 | $ npm install 55 | ``` 56 | 57 | Make sure that everything has been set up correctly: 58 | 59 | ``` 60 | $ npm run test 61 | ``` 62 | 63 | ## Usage 64 | 65 | ### npm 66 | 67 | To interact with this package's contracts within JavaScript code, you simply need to require this package's `.json` files: 68 | 69 | ```js 70 | const contract = require("@nibbstack/erc721/abi/NFTokenEnumerable.json"); 71 | console.log(contract); 72 | ``` 73 | 74 | ### Remix IDE (Ethereum only) 75 | 76 | You can quickly deploy a contract with this library using [Remix IDE](https://remix.ethereum.org). Here is one example. 77 | 78 | You have created and have possession of unique glass-blown artwork (each having a serial/lot number) which you would like to sell using the Ethereum or Wanchain mainnet. You will sell non-fungible tokens and the buyers would be able to trade those to other people. One token per piece of artwork. You commit to anybody holding these tokens that they may redeem their token and take physical possession of the art. 79 | 80 | To do this, simply paste the code below into Remix and deploy the smart contract. You will "mint" a token for each new piece of artwork you want to see. Then you will "burn" that token when you surrender physical possession of the piece. 81 | 82 | ```solidity 83 | // SPDX-License-Identifier: MIT 84 | pragma solidity ^0.8.0; 85 | 86 | import "https://github.com/nibbstack/erc721/src/contracts/tokens/nf-token-metadata.sol"; 87 | import "https://github.com/nibbstack/erc721/src/contracts/ownership/ownable.sol"; 88 | 89 | /** 90 | * @dev This is an example contract implementation of NFToken with metadata extension. 91 | */ 92 | contract MyArtSale is 93 | NFTokenMetadata, 94 | Ownable 95 | { 96 | 97 | /** 98 | * @dev Contract constructor. Sets metadata extension `name` and `symbol`. 99 | */ 100 | constructor() 101 | { 102 | nftName = "Frank's Art Sale"; 103 | nftSymbol = "FAS"; 104 | } 105 | 106 | /** 107 | * @dev Mints a new NFT. 108 | * @param _to The address that will own the minted NFT. 109 | * @param _tokenId of the NFT to be minted by the msg.sender. 110 | * @param _uri String representing RFC 3986 URI. 111 | */ 112 | function mint( 113 | address _to, 114 | uint256 _tokenId, 115 | string calldata _uri 116 | ) 117 | external 118 | onlyOwner 119 | { 120 | super._mint(_to, _tokenId); 121 | super._setTokenUri(_tokenId, _uri); 122 | } 123 | 124 | } 125 | ``` 126 | 127 | *You should contact a lawyer before holding an auction, or selling anything. Specifically, laws for auctions vary wildly by jurisdiction. This application is provided only as an example of the technology and is not legal advice.* 128 | 129 | ## Validation 130 | 131 | You can check the validity of the smart contract, the correctness of the implementation and the supported functions of the respective smart contract using the online validator at https://erc721validator.org/. 132 | 133 | ## Playground 134 | 135 | ### Ethereum - Ropsten testnet 136 | 137 | We already deployed some contracts to the [Ropsten](https://ropsten.etherscan.io/) network. You can play with them RIGHT NOW. No need to install the software. In this test version of the contract, anybody can `mint` or `burn` tokens, so don't use it for anything important. 138 | 139 | | Contract | Token address | Transaction hash | 140 | | ------------------------------------------------------------ | ------------- | ---------------- | 141 | | [`nf-token.sol`](src/contracts/tokens/nf-token.sol) | [0xd8bbf8ceb445de814fb47547436b3cfeecadd4ec](https://ropsten.etherscan.io/address/0xd8bbf8ceb445de814fb47547436b3cfeecadd4ec) | [0xaac94c9ce15f5e437bd452eb1847a1d03a923730824743e1f37b471db0f16f0c](https://ropsten.etherscan.io/tx/0xaac94c9ce15f5e437bd452eb1847a1d03a923730824743e1f37b471db0f16f0c) | 142 | | [`nf-token-metadata.sol`](src/contracts/tokens/nf-token-metadata.sol) | [0x5c007a1d8051dfda60b3692008b9e10731b67fde](https://ropsten.etherscan.io/address/0x5c007a1d8051dfda60b3692008b9e10731b67fde) | [0x1e702503aff40ea44aa4d77801464fd90a018b7b9bad670500a6e2b3cc281d3f](https://ropsten.etherscan.io/tx/0x1e702503aff40ea44aa4d77801464fd90a018b7b9bad670500a6e2b3cc281d3f) | 143 | | [`nf-token-enumerable.sol`](src/contracts/tokens/nf-token-enumerable.sol) | [0x130dc43898eb2a52c9d11043a581ce4414487ed0](https://ropsten.etherscan.io/address/0x130dc43898eb2a52c9d11043a581ce4414487ed0) | [0x8df4c9b73d43c2b255a4038eec960ca12dae9ba62709894f0d85dc90d3938280](https://ropsten.etherscan.io/tx/0x8df4c9b73d43c2b255a4038eec960ca12dae9ba62709894f0d85dc90d3938280) | 144 | 145 | ### Wanchain - testnet 146 | 147 | We already deployed some contracts to [testnet](http://testnet.wanscan.org/) network. You can play with them RIGHT NOW. No need to install software. In this test version of the contract, anybody can `mint` or `burn` tokens, so don't use it for anything important. 148 | 149 | | Contract | Token address | Transaction hash | 150 | | ------------------------------------------------------------ | ------------- | ---------------- | 151 | | [`nf-token.sol`](src/contracts/tokens/nf-token.sol) | [0x6D0eb4304026116b2A7bff3f46E9D2f320df47D9](http://testnet.wanscan.org/address/0x6D0eb4304026116b2A7bff3f46E9D2f320df47D9) | [0x9ba7a172a50fc70433e29cfdc4fba51c37d84c8a6766686a9cfb975125196c3d](http://testnet.wanscan.org/tx/0x9ba7a172a50fc70433e29cfdc4fba51c37d84c8a6766686a9cfb975125196c3d) | 152 | | [`nf-token-metadata.sol`](src/contracts/tokens/nf-token-metadata.sol) | [0xF0a3852BbFC67ba9936E661fE092C93804bf1c81](http://testnet.wanscan.org/address/0xF0a3852BbFC67ba9936E661fE092C93804bf1c81) | [0x338ca779405d39c0e0f403b01679b22603c745828211b5b2ea319affbc3e181b](http://testnet.wanscan.org/tx/0x338ca779405d39c0e0f403b01679b22603c745828211b5b2ea319affbc3e181b) | 153 | | [`nf-token-enumerable.sol`](src/contracts/tokens/nf-token-enumerable.sol) | [0x539d2CcBDc3Fc5D709b9d0f77CaE6a82e2fec1F3](http://testnet.wanscan.org/address/0x539d2CcBDc3Fc5D709b9d0f77CaE6a82e2fec1F3) | [0x755886c9a9a53189550be162410b2ae2de6fc62f6791bf38599a078daf265580](http://testnet.wanscan.org/tx/0x755886c9a9a53189550be162410b2ae2de6fc62f6791bf38599a078daf265580) | 154 | 155 | ## Contributing 156 | 157 | See [CONTRIBUTING.md](./CONTRIBUTING.md) for how to help out. 158 | 159 | ## Bug bounty 160 | 161 | You are somebody that reads the documentation of smart contracts and understands how the ERC-721 Token Reference Implementation works. So you have unique skills and your time is valuable. We will pay you for your contributions to this project in the form of bug reports. 162 | 163 | If your project depends on ERC-721 or you want to help improve the assurance of this project then you can pledge a bounty. This means you will commit to paying researchers that demonstrate a problem. Contact us at [bounty@nibbstack.com](mailto:bounty@nibbstack.com) if interested. Thank you. 164 | 165 | Read the full [bug bounty program](BUG_BOUNTY.md). 166 | 167 | ## Licence 168 | 169 | See [LICENSE](./LICENSE) for details. 170 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Only the latest minor version of ERC-721 Token — Reference Implementation is supported with security updates. These updates are published as new patch versions. All versioning follows Semantic Versioning. You can find the latest version [released on GitHub](https://github.com/nibbstack/erc721/releases) and simultaneously published on NPM under individual packages. 6 | 7 | ## Reporting a Vulnerability 8 | 9 | We really appreciate your help in finding bugs and vulnerabilities in this repository. Please report bugs and vulnerabilities using [GitHub Issues](https://github.com/nibbstack/erc721/issues) and we will pay you according to [our bug bounty](BUG_BOUNTY.md). If you wish to report issues privately to us then you are still eligible for our bug bounty program and can see additional details on that page. 10 | -------------------------------------------------------------------------------- /hardhat.config.js: -------------------------------------------------------------------------------- 1 | require('@nomiclabs/hardhat-waffle'); 2 | require('hardhat-abi-exporter'); 3 | require('solidity-coverage'); 4 | 5 | /** 6 | * @type import('hardhat/config').HardhatUserConfig 7 | */ 8 | module.exports = { 9 | solidity: '0.8.9', 10 | networks: { 11 | hardhat: { 12 | initialBaseFeePerGas: 0 // hardhat london fork error fix for coverage 13 | } 14 | }, 15 | paths: { 16 | sources: './src/*', 17 | artifacts: './build', 18 | tests: './src/tests/*' 19 | }, 20 | abiExporter: { 21 | path: './abi', 22 | clear: true, 23 | flat: true, 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nibbstack/erc721", 3 | "version": "2.6.1", 4 | "description": "Reference implementation of the ERC-721 non-fungible token standard.", 5 | "scripts": { 6 | "test": "npx hardhat test", 7 | "compile": "npx hardhat compile", 8 | "flatten": "npx hardhat flatten", 9 | "coverage": "npx hardhat coverage", 10 | "solhint": "solhint src/**/*.sol" 11 | }, 12 | "license": "MIT", 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/nibbstack/erc721.git" 16 | }, 17 | "bugs": { 18 | "url": "https://github.com/nibbstack/erc721/issues" 19 | }, 20 | "homepage": "https://github.com/nibbstack/erc721#readme", 21 | "keywords": [ 22 | "ethereum", 23 | "blockchain", 24 | "eth", 25 | "contract", 26 | "contracts", 27 | "smart", 28 | "smart-contract", 29 | "smart-contracts", 30 | "token", 31 | "tokens", 32 | "nft", 33 | "nfts", 34 | "non-fungible", 35 | "non-fungibles", 36 | "eip", 37 | "erc", 38 | "erc721", 39 | "erc-721", 40 | "deed", 41 | "standard" 42 | ], 43 | "devDependencies": { 44 | "@nomiclabs/hardhat-ethers": "2.0.2", 45 | "@nomiclabs/hardhat-waffle": "2.0.1", 46 | "chai": "4.3.4", 47 | "ethereum-waffle": "3.4.0", 48 | "ethers": "5.5.2", 49 | "hardhat": "2.7.0", 50 | "hardhat-abi-exporter": "2.3.1", 51 | "solhint": "3.3.6", 52 | "solidity-coverage": "0.7.17", 53 | "ts-node": "10.4.0" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/contracts/mocks/nf-token-enumerable-mock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "../../contracts/tokens/nf-token-enumerable.sol"; 5 | import "../ownership/ownable.sol"; 6 | 7 | /** 8 | * @dev This is an example contract implementation of NFToken with enumerable extension. 9 | */ 10 | contract NFTokenEnumerableMock is 11 | NFTokenEnumerable, 12 | Ownable 13 | { 14 | 15 | /** 16 | * @dev Mints a new NFT. 17 | * @param _to The address that will own the minted NFT. 18 | * @param _tokenId of the NFT to be minted by the msg.sender. 19 | */ 20 | function mint( 21 | address _to, 22 | uint256 _tokenId 23 | ) 24 | external 25 | onlyOwner 26 | { 27 | super._mint(_to, _tokenId); 28 | } 29 | 30 | /** 31 | * @dev Removes a NFT from owner. 32 | * @param _tokenId Which NFT we want to remove. 33 | */ 34 | function burn( 35 | uint256 _tokenId 36 | ) 37 | external 38 | onlyOwner 39 | { 40 | super._burn(_tokenId); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/contracts/mocks/nf-token-metadata-base-uri-mock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "../tokens/nf-token-metadata.sol"; 5 | import "../ownership/ownable.sol"; 6 | 7 | /** 8 | * @dev This is an example contract implementation of NFToken with metadata extension. 9 | */ 10 | contract NFTokenMetadataBaseUriMock is 11 | NFTokenMetadata, 12 | Ownable 13 | { 14 | 15 | /** 16 | * @dev URI base for tokenURI function. Token URI is constructed as baseURI + tokenId. 17 | */ 18 | string public baseURI; 19 | 20 | /** 21 | * @dev Contract constructor. 22 | * @param _name A descriptive name for a collection of NFTs. 23 | * @param _symbol An abbreviated name for NFTokens. 24 | * @param _baseURI String representing base RFC 3986 URI. 25 | */ 26 | constructor( 27 | string memory _name, 28 | string memory _symbol, 29 | string memory _baseURI 30 | ) 31 | { 32 | nftName = _name; 33 | nftSymbol = _symbol; 34 | baseURI = _baseURI; 35 | } 36 | 37 | /** 38 | * @dev Mints a new NFT. 39 | * @param _to The address that will own the minted NFT. 40 | * @param _tokenId of the NFT to be minted by the msg.sender. 41 | */ 42 | function mint( 43 | address _to, 44 | uint256 _tokenId 45 | ) 46 | external 47 | onlyOwner 48 | { 49 | super._mint(_to, _tokenId); 50 | } 51 | 52 | /** 53 | * @dev Removes a NFT from owner. 54 | * @param _tokenId Which NFT we want to remove. 55 | */ 56 | function burn( 57 | uint256 _tokenId 58 | ) 59 | external 60 | onlyOwner 61 | { 62 | super._burn(_tokenId); 63 | } 64 | 65 | /** 66 | * @dev A distinct URI (RFC 3986) for a given NFT. 67 | * @param _tokenId Id for which we want uri. 68 | * @return URI of _tokenId. 69 | */ 70 | function _tokenURI( 71 | uint256 _tokenId 72 | ) 73 | internal 74 | override 75 | view 76 | returns (string memory) 77 | { 78 | return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, _uint2str(_tokenId))) : ""; 79 | } 80 | 81 | /** 82 | * @dev Helper function that changes uint to string representation. 83 | * @return str String representation. 84 | */ 85 | function _uint2str( 86 | uint256 _i 87 | ) 88 | internal 89 | pure 90 | returns (string memory str) 91 | { 92 | if (_i == 0) { 93 | return "0"; 94 | } 95 | uint256 j = _i; 96 | uint256 length; 97 | while (j != 0) { 98 | length++; 99 | j /= 10; 100 | } 101 | bytes memory bstr = new bytes(length); 102 | uint256 k = length; 103 | j = _i; 104 | while (j != 0) { 105 | bstr[--k] = bytes1(uint8(48 + j % 10)); 106 | j /= 10; 107 | } 108 | str = string(bstr); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/contracts/mocks/nf-token-metadata-enumerable-mock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "../tokens/nf-token-metadata.sol"; 5 | import "../tokens/nf-token-enumerable.sol"; 6 | import "../ownership/ownable.sol"; 7 | 8 | /** 9 | * @dev This is an example contract implementation of NFToken with enumerable and metadata 10 | * extensions. 11 | */ 12 | contract NFTokenMetadataEnumerableMock is 13 | NFTokenEnumerable, 14 | NFTokenMetadata, 15 | Ownable 16 | { 17 | 18 | /** 19 | * @dev Contract constructor. 20 | * @param _name A descriptive name for a collection of NFTs. 21 | * @param _symbol An abbreviated name for NFTokens. 22 | */ 23 | constructor( 24 | string memory _name, 25 | string memory _symbol 26 | ) 27 | { 28 | nftName = _name; 29 | nftSymbol = _symbol; 30 | } 31 | 32 | /** 33 | * @dev Mints a new NFT. 34 | * @param _to The address that will own the minted NFT. 35 | * @param _tokenId of the NFT to be minted by the msg.sender. 36 | * @param _uri String representing RFC 3986 URI. 37 | */ 38 | function mint( 39 | address _to, 40 | uint256 _tokenId, 41 | string calldata _uri 42 | ) 43 | external 44 | onlyOwner 45 | { 46 | super._mint(_to, _tokenId); 47 | super._setTokenUri(_tokenId, _uri); 48 | } 49 | 50 | /** 51 | * @dev Removes a NFT from owner. 52 | * @param _tokenId Which NFT we want to remove. 53 | */ 54 | function burn( 55 | uint256 _tokenId 56 | ) 57 | external 58 | onlyOwner 59 | { 60 | super._burn(_tokenId); 61 | } 62 | 63 | /** 64 | * @dev Mints a new NFT. 65 | * @notice This is an internal function which should be called from user-implemented external 66 | * mint function. Its purpose is to show and properly initialize data structures when using this 67 | * implementation. 68 | * @param _to The address that will own the minted NFT. 69 | * @param _tokenId of the NFT to be minted by the msg.sender. 70 | */ 71 | function _mint( 72 | address _to, 73 | uint256 _tokenId 74 | ) 75 | internal 76 | override(NFToken, NFTokenEnumerable) 77 | virtual 78 | { 79 | NFTokenEnumerable._mint(_to, _tokenId); 80 | } 81 | 82 | /** 83 | * @dev Burns a NFT. 84 | * @notice This is an internal function which should be called from user-implemented external 85 | * burn function. Its purpose is to show and properly initialize data structures when using this 86 | * implementation. Also, note that this burn implementation allows the minter to re-mint a burned 87 | * NFT. 88 | * @param _tokenId ID of the NFT to be burned. 89 | */ 90 | function _burn( 91 | uint256 _tokenId 92 | ) 93 | internal 94 | override(NFTokenMetadata, NFTokenEnumerable) 95 | virtual 96 | { 97 | NFTokenEnumerable._burn(_tokenId); 98 | if (bytes(idToUri[_tokenId]).length != 0) 99 | { 100 | delete idToUri[_tokenId]; 101 | } 102 | } 103 | 104 | /** 105 | * @notice Use and override this function with caution. Wrong usage can have serious consequences. 106 | * @dev Removes a NFT from an address. 107 | * @param _from Address from wich we want to remove the NFT. 108 | * @param _tokenId Which NFT we want to remove. 109 | */ 110 | function _removeNFToken( 111 | address _from, 112 | uint256 _tokenId 113 | ) 114 | internal 115 | override(NFToken, NFTokenEnumerable) 116 | { 117 | NFTokenEnumerable._removeNFToken(_from, _tokenId); 118 | } 119 | 120 | /** 121 | * @notice Use and override this function with caution. Wrong usage can have serious consequences. 122 | * @dev Assigns a new NFT to an address. 123 | * @param _to Address to wich we want to add the NFT. 124 | * @param _tokenId Which NFT we want to add. 125 | */ 126 | function _addNFToken( 127 | address _to, 128 | uint256 _tokenId 129 | ) 130 | internal 131 | override(NFToken, NFTokenEnumerable) 132 | { 133 | NFTokenEnumerable._addNFToken(_to, _tokenId); 134 | } 135 | 136 | /** 137 | * @dev Helper function that gets NFT count of owner. This is needed for overriding in enumerable 138 | * extension to remove double storage(gas optimization) of owner nft count. 139 | * @param _owner Address for whom to query the count. 140 | * @return Number of _owner NFTs. 141 | */ 142 | function _getOwnerNFTCount( 143 | address _owner 144 | ) 145 | internal 146 | override(NFToken, NFTokenEnumerable) 147 | view 148 | returns (uint256) 149 | { 150 | return NFTokenEnumerable._getOwnerNFTCount(_owner); 151 | } 152 | 153 | } 154 | -------------------------------------------------------------------------------- /src/contracts/mocks/nf-token-metadata-mock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "../tokens/nf-token-metadata.sol"; 5 | import "../ownership/ownable.sol"; 6 | 7 | /** 8 | * @dev This is an example contract implementation of NFToken with metadata extension. 9 | */ 10 | contract NFTokenMetadataMock is 11 | NFTokenMetadata, 12 | Ownable 13 | { 14 | 15 | /** 16 | * @dev Contract constructor. 17 | * @param _name A descriptive name for a collection of NFTs. 18 | * @param _symbol An abbreviated name for NFTokens. 19 | */ 20 | constructor( 21 | string memory _name, 22 | string memory _symbol 23 | ) 24 | { 25 | nftName = _name; 26 | nftSymbol = _symbol; 27 | } 28 | 29 | /** 30 | * @dev Mints a new NFT. 31 | * @param _to The address that will own the minted NFT. 32 | * @param _tokenId of the NFT to be minted by the msg.sender. 33 | * @param _uri String representing RFC 3986 URI. 34 | */ 35 | function mint( 36 | address _to, 37 | uint256 _tokenId, 38 | string calldata _uri 39 | ) 40 | external 41 | onlyOwner 42 | { 43 | super._mint(_to, _tokenId); 44 | super._setTokenUri(_tokenId, _uri); 45 | } 46 | 47 | /** 48 | * @dev Removes a NFT from owner. 49 | * @param _tokenId Which NFT we want to remove. 50 | */ 51 | function burn( 52 | uint256 _tokenId 53 | ) 54 | external 55 | onlyOwner 56 | { 57 | super._burn(_tokenId); 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/contracts/mocks/nf-token-mock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "../../contracts/tokens/nf-token.sol"; 5 | import "../ownership/ownable.sol"; 6 | 7 | /** 8 | * @dev This is an example contract implementation of NFToken. 9 | */ 10 | contract NFTokenMock is 11 | NFToken, 12 | Ownable 13 | { 14 | 15 | /** 16 | * @dev Mints a new NFT. 17 | * @param _to The address that will own the minted NFT. 18 | * @param _tokenId of the NFT to be minted by the msg.sender. 19 | */ 20 | function mint( 21 | address _to, 22 | uint256 _tokenId 23 | ) 24 | external 25 | onlyOwner 26 | { 27 | super._mint(_to, _tokenId); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/contracts/ownership/ownable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | /** 5 | * @dev The contract has an owner address, and provides basic authorization control whitch 6 | * simplifies the implementation of user permissions. This contract is based on the source code at: 7 | * https://github.com/OpenZeppelin/openzeppelin-solidity/blob/master/contracts/ownership/Ownable.sol 8 | */ 9 | contract Ownable 10 | { 11 | 12 | /** 13 | * @dev Error constants. 14 | */ 15 | string public constant NOT_CURRENT_OWNER = "018001"; 16 | string public constant CANNOT_TRANSFER_TO_ZERO_ADDRESS = "018002"; 17 | 18 | /** 19 | * @dev Current owner address. 20 | */ 21 | address public owner; 22 | 23 | /** 24 | * @dev An event which is triggered when the owner is changed. 25 | * @param previousOwner The address of the previous owner. 26 | * @param newOwner The address of the new owner. 27 | */ 28 | event OwnershipTransferred( 29 | address indexed previousOwner, 30 | address indexed newOwner 31 | ); 32 | 33 | /** 34 | * @dev The constructor sets the original `owner` of the contract to the sender account. 35 | */ 36 | constructor() 37 | { 38 | owner = msg.sender; 39 | } 40 | 41 | /** 42 | * @dev Throws if called by any account other than the owner. 43 | */ 44 | modifier onlyOwner() 45 | { 46 | require(msg.sender == owner, NOT_CURRENT_OWNER); 47 | _; 48 | } 49 | 50 | /** 51 | * @dev Allows the current owner to transfer control of the contract to a newOwner. 52 | * @param _newOwner The address to transfer ownership to. 53 | */ 54 | function transferOwnership( 55 | address _newOwner 56 | ) 57 | public 58 | onlyOwner 59 | { 60 | require(_newOwner != address(0), CANNOT_TRANSFER_TO_ZERO_ADDRESS); 61 | emit OwnershipTransferred(owner, _newOwner); 62 | owner = _newOwner; 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/contracts/tokens/erc721-enumerable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | /** 5 | * @dev Optional enumeration extension for ERC-721 non-fungible token standard. 6 | * See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md. 7 | */ 8 | interface ERC721Enumerable 9 | { 10 | 11 | /** 12 | * @dev Returns a count of valid NFTs tracked by this contract, where each one of them has an 13 | * assigned and queryable owner not equal to the zero address. 14 | * @return Total supply of NFTs. 15 | */ 16 | function totalSupply() 17 | external 18 | view 19 | returns (uint256); 20 | 21 | /** 22 | * @dev Returns the token identifier for the `_index`th NFT. Sort order is not specified. 23 | * @param _index A counter less than `totalSupply()`. 24 | * @return Token id. 25 | */ 26 | function tokenByIndex( 27 | uint256 _index 28 | ) 29 | external 30 | view 31 | returns (uint256); 32 | 33 | /** 34 | * @dev Returns the token identifier for the `_index`th NFT assigned to `_owner`. Sort order is 35 | * not specified. It throws if `_index` >= `balanceOf(_owner)` or if `_owner` is the zero address, 36 | * representing invalid NFTs. 37 | * @param _owner An address where we are interested in NFTs owned by them. 38 | * @param _index A counter less than `balanceOf(_owner)`. 39 | * @return Token id. 40 | */ 41 | function tokenOfOwnerByIndex( 42 | address _owner, 43 | uint256 _index 44 | ) 45 | external 46 | view 47 | returns (uint256); 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/contracts/tokens/erc721-metadata.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | /** 5 | * @dev Optional metadata extension for ERC-721 non-fungible token standard. 6 | * See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md. 7 | */ 8 | interface ERC721Metadata 9 | { 10 | 11 | /** 12 | * @dev Returns a descriptive name for a collection of NFTs in this contract. 13 | * @return _name Representing name. 14 | */ 15 | function name() 16 | external 17 | view 18 | returns (string memory _name); 19 | 20 | /** 21 | * @dev Returns a abbreviated name for a collection of NFTs in this contract. 22 | * @return _symbol Representing symbol. 23 | */ 24 | function symbol() 25 | external 26 | view 27 | returns (string memory _symbol); 28 | 29 | /** 30 | * @dev Returns a distinct Uniform Resource Identifier (URI) for a given asset. It Throws if 31 | * `_tokenId` is not a valid NFT. URIs are defined in RFC3986. The URI may point to a JSON file 32 | * that conforms to the "ERC721 Metadata JSON Schema". 33 | * @return URI of _tokenId. 34 | */ 35 | function tokenURI(uint256 _tokenId) 36 | external 37 | view 38 | returns (string memory); 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/contracts/tokens/erc721-token-receiver.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | /** 5 | * @dev ERC-721 interface for accepting safe transfers. 6 | * See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md. 7 | */ 8 | interface ERC721TokenReceiver 9 | { 10 | 11 | /** 12 | * @notice The contract address is always the message sender. A wallet/broker/auction application 13 | * MUST implement the wallet interface if it will accept safe transfers. 14 | * @dev Handle the receipt of a NFT. The ERC721 smart contract calls this function on the 15 | * recipient after a `transfer`. This function MAY throw to revert and reject the transfer. Return 16 | * of other than the magic value MUST result in the transaction being reverted. 17 | * Returns `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))` unless throwing. 18 | * @param _operator The address which called `safeTransferFrom` function. 19 | * @param _from The address which previously owned the token. 20 | * @param _tokenId The NFT identifier which is being transferred. 21 | * @param _data Additional data with no specified format. 22 | * @return Returns `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`. 23 | */ 24 | function onERC721Received( 25 | address _operator, 26 | address _from, 27 | uint256 _tokenId, 28 | bytes calldata _data 29 | ) 30 | external 31 | returns(bytes4); 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/contracts/tokens/erc721.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | /** 5 | * @dev ERC-721 non-fungible token standard. 6 | * See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md. 7 | */ 8 | interface ERC721 9 | { 10 | 11 | /** 12 | * @dev Emits when ownership of any NFT changes by any mechanism. This event emits when NFTs are 13 | * created (`from` == 0) and destroyed (`to` == 0). Exception: during contract creation, any 14 | * number of NFTs may be created and assigned without emitting Transfer. At the time of any 15 | * transfer, the approved address for that NFT (if any) is reset to none. 16 | */ 17 | event Transfer( 18 | address indexed _from, 19 | address indexed _to, 20 | uint256 indexed _tokenId 21 | ); 22 | 23 | /** 24 | * @dev This emits when the approved address for an NFT is changed or reaffirmed. The zero 25 | * address indicates there is no approved address. When a Transfer event emits, this also 26 | * indicates that the approved address for that NFT (if any) is reset to none. 27 | */ 28 | event Approval( 29 | address indexed _owner, 30 | address indexed _approved, 31 | uint256 indexed _tokenId 32 | ); 33 | 34 | /** 35 | * @dev This emits when an operator is enabled or disabled for an owner. The operator can manage 36 | * all NFTs of the owner. 37 | */ 38 | event ApprovalForAll( 39 | address indexed _owner, 40 | address indexed _operator, 41 | bool _approved 42 | ); 43 | 44 | /** 45 | * @notice Throws unless `msg.sender` is the current owner, an authorized operator, or the 46 | * approved address for this NFT. Throws if `_from` is not the current owner. Throws if `_to` is 47 | * the zero address. Throws if `_tokenId` is not a valid NFT. When transfer is complete, this 48 | * function checks if `_to` is a smart contract (code size > 0). If so, it calls 49 | * `onERC721Received` on `_to` and throws if the return value is not 50 | * `bytes4(keccak256("onERC721Received(address,uint256,bytes)"))`. 51 | * @dev Transfers the ownership of an NFT from one address to another address. This function can 52 | * be changed to payable. 53 | * @param _from The current owner of the NFT. 54 | * @param _to The new owner. 55 | * @param _tokenId The NFT to transfer. 56 | * @param _data Additional data with no specified format, sent in call to `_to`. 57 | */ 58 | function safeTransferFrom( 59 | address _from, 60 | address _to, 61 | uint256 _tokenId, 62 | bytes calldata _data 63 | ) 64 | external; 65 | 66 | /** 67 | * @notice This works identically to the other function with an extra data parameter, except this 68 | * function just sets data to "" 69 | * @dev Transfers the ownership of an NFT from one address to another address. This function can 70 | * be changed to payable. 71 | * @param _from The current owner of the NFT. 72 | * @param _to The new owner. 73 | * @param _tokenId The NFT to transfer. 74 | */ 75 | function safeTransferFrom( 76 | address _from, 77 | address _to, 78 | uint256 _tokenId 79 | ) 80 | external; 81 | 82 | /** 83 | * @notice The caller is responsible to confirm that `_to` is capable of receiving NFTs or else 84 | * they may be permanently lost. 85 | * @dev Throws unless `msg.sender` is the current owner, an authorized operator, or the approved 86 | * address for this NFT. Throws if `_from` is not the current owner. Throws if `_to` is the zero 87 | * address. Throws if `_tokenId` is not a valid NFT. This function can be changed to payable. 88 | * @param _from The current owner of the NFT. 89 | * @param _to The new owner. 90 | * @param _tokenId The NFT to transfer. 91 | */ 92 | function transferFrom( 93 | address _from, 94 | address _to, 95 | uint256 _tokenId 96 | ) 97 | external; 98 | 99 | /** 100 | * @notice The zero address indicates there is no approved address. Throws unless `msg.sender` is 101 | * the current NFT owner, or an authorized operator of the current owner. 102 | * @param _approved The new approved NFT controller. 103 | * @dev Set or reaffirm the approved address for an NFT. This function can be changed to payable. 104 | * @param _tokenId The NFT to approve. 105 | */ 106 | function approve( 107 | address _approved, 108 | uint256 _tokenId 109 | ) 110 | external; 111 | 112 | /** 113 | * @notice The contract MUST allow multiple operators per owner. 114 | * @dev Enables or disables approval for a third party ("operator") to manage all of 115 | * `msg.sender`'s assets. It also emits the ApprovalForAll event. 116 | * @param _operator Address to add to the set of authorized operators. 117 | * @param _approved True if the operators is approved, false to revoke approval. 118 | */ 119 | function setApprovalForAll( 120 | address _operator, 121 | bool _approved 122 | ) 123 | external; 124 | 125 | /** 126 | * @dev Returns the number of NFTs owned by `_owner`. NFTs assigned to the zero address are 127 | * considered invalid, and this function throws for queries about the zero address. 128 | * @notice Count all NFTs assigned to an owner. 129 | * @param _owner Address for whom to query the balance. 130 | * @return Balance of _owner. 131 | */ 132 | function balanceOf( 133 | address _owner 134 | ) 135 | external 136 | view 137 | returns (uint256); 138 | 139 | /** 140 | * @notice Find the owner of an NFT. 141 | * @dev Returns the address of the owner of the NFT. NFTs assigned to the zero address are 142 | * considered invalid, and queries about them do throw. 143 | * @param _tokenId The identifier for an NFT. 144 | * @return Address of _tokenId owner. 145 | */ 146 | function ownerOf( 147 | uint256 _tokenId 148 | ) 149 | external 150 | view 151 | returns (address); 152 | 153 | /** 154 | * @notice Throws if `_tokenId` is not a valid NFT. 155 | * @dev Get the approved address for a single NFT. 156 | * @param _tokenId The NFT to find the approved address for. 157 | * @return Address that _tokenId is approved for. 158 | */ 159 | function getApproved( 160 | uint256 _tokenId 161 | ) 162 | external 163 | view 164 | returns (address); 165 | 166 | /** 167 | * @notice Query if an address is an authorized operator for another address. 168 | * @dev Returns true if `_operator` is an approved operator for `_owner`, false otherwise. 169 | * @param _owner The address that owns the NFTs. 170 | * @param _operator The address that acts on behalf of the owner. 171 | * @return True if approved for all, false otherwise. 172 | */ 173 | function isApprovedForAll( 174 | address _owner, 175 | address _operator 176 | ) 177 | external 178 | view 179 | returns (bool); 180 | 181 | } 182 | -------------------------------------------------------------------------------- /src/contracts/tokens/nf-token-enumerable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "./nf-token.sol"; 5 | import "./erc721-enumerable.sol"; 6 | 7 | /** 8 | * @dev Optional enumeration implementation for ERC-721 non-fungible token standard. 9 | */ 10 | contract NFTokenEnumerable is 11 | NFToken, 12 | ERC721Enumerable 13 | { 14 | 15 | /** 16 | * @dev List of revert message codes. Implementing dApp should handle showing the correct message. 17 | * Based on 0xcert framework error codes. 18 | */ 19 | string constant INVALID_INDEX = "005007"; 20 | 21 | /** 22 | * @dev Array of all NFT IDs. 23 | */ 24 | uint256[] internal tokens; 25 | 26 | /** 27 | * @dev Mapping from token ID to its index in global tokens array. 28 | */ 29 | mapping(uint256 => uint256) internal idToIndex; 30 | 31 | /** 32 | * @dev Mapping from owner to list of owned NFT IDs. 33 | */ 34 | mapping(address => uint256[]) internal ownerToIds; 35 | 36 | /** 37 | * @dev Mapping from NFT ID to its index in the owner tokens list. 38 | */ 39 | mapping(uint256 => uint256) internal idToOwnerIndex; 40 | 41 | /** 42 | * @dev Contract constructor. 43 | */ 44 | constructor() 45 | { 46 | supportedInterfaces[0x780e9d63] = true; // ERC721Enumerable 47 | } 48 | 49 | /** 50 | * @dev Returns the count of all existing NFTokens. 51 | * @return Total supply of NFTs. 52 | */ 53 | function totalSupply() 54 | external 55 | override 56 | view 57 | returns (uint256) 58 | { 59 | return tokens.length; 60 | } 61 | 62 | /** 63 | * @dev Returns NFT ID by its index. 64 | * @param _index A counter less than `totalSupply()`. 65 | * @return Token id. 66 | */ 67 | function tokenByIndex( 68 | uint256 _index 69 | ) 70 | external 71 | override 72 | view 73 | returns (uint256) 74 | { 75 | require(_index < tokens.length, INVALID_INDEX); 76 | return tokens[_index]; 77 | } 78 | 79 | /** 80 | * @dev returns the n-th NFT ID from a list of owner's tokens. 81 | * @param _owner Token owner's address. 82 | * @param _index Index number representing n-th token in owner's list of tokens. 83 | * @return Token id. 84 | */ 85 | function tokenOfOwnerByIndex( 86 | address _owner, 87 | uint256 _index 88 | ) 89 | external 90 | override 91 | view 92 | returns (uint256) 93 | { 94 | require(_index < ownerToIds[_owner].length, INVALID_INDEX); 95 | return ownerToIds[_owner][_index]; 96 | } 97 | 98 | /** 99 | * @notice This is an internal function which should be called from user-implemented external 100 | * mint function. Its purpose is to show and properly initialize data structures when using this 101 | * implementation. 102 | * @dev Mints a new NFT. 103 | * @param _to The address that will own the minted NFT. 104 | * @param _tokenId of the NFT to be minted by the msg.sender. 105 | */ 106 | function _mint( 107 | address _to, 108 | uint256 _tokenId 109 | ) 110 | internal 111 | override 112 | virtual 113 | { 114 | super._mint(_to, _tokenId); 115 | tokens.push(_tokenId); 116 | idToIndex[_tokenId] = tokens.length - 1; 117 | } 118 | 119 | /** 120 | * @notice This is an internal function which should be called from user-implemented external 121 | * burn function. Its purpose is to show and properly initialize data structures when using this 122 | * implementation. Also, note that this burn implementation allows the minter to re-mint a burned 123 | * NFT. 124 | * @dev Burns a NFT. 125 | * @param _tokenId ID of the NFT to be burned. 126 | */ 127 | function _burn( 128 | uint256 _tokenId 129 | ) 130 | internal 131 | override 132 | virtual 133 | { 134 | super._burn(_tokenId); 135 | 136 | uint256 tokenIndex = idToIndex[_tokenId]; 137 | uint256 lastTokenIndex = tokens.length - 1; 138 | uint256 lastToken = tokens[lastTokenIndex]; 139 | 140 | tokens[tokenIndex] = lastToken; 141 | 142 | tokens.pop(); 143 | // This wastes gas if you are burning the last token but saves a little gas if you are not. 144 | idToIndex[lastToken] = tokenIndex; 145 | idToIndex[_tokenId] = 0; 146 | } 147 | 148 | /** 149 | * @notice Use and override this function with caution. Wrong usage can have serious consequences. 150 | * @dev Removes a NFT from an address. 151 | * @param _from Address from wich we want to remove the NFT. 152 | * @param _tokenId Which NFT we want to remove. 153 | */ 154 | function _removeNFToken( 155 | address _from, 156 | uint256 _tokenId 157 | ) 158 | internal 159 | override 160 | virtual 161 | { 162 | require(idToOwner[_tokenId] == _from, NOT_OWNER); 163 | delete idToOwner[_tokenId]; 164 | 165 | uint256 tokenToRemoveIndex = idToOwnerIndex[_tokenId]; 166 | uint256 lastTokenIndex = ownerToIds[_from].length - 1; 167 | 168 | if (lastTokenIndex != tokenToRemoveIndex) 169 | { 170 | uint256 lastToken = ownerToIds[_from][lastTokenIndex]; 171 | ownerToIds[_from][tokenToRemoveIndex] = lastToken; 172 | idToOwnerIndex[lastToken] = tokenToRemoveIndex; 173 | } 174 | 175 | ownerToIds[_from].pop(); 176 | } 177 | 178 | /** 179 | * @notice Use and override this function with caution. Wrong usage can have serious consequences. 180 | * @dev Assigns a new NFT to an address. 181 | * @param _to Address to wich we want to add the NFT. 182 | * @param _tokenId Which NFT we want to add. 183 | */ 184 | function _addNFToken( 185 | address _to, 186 | uint256 _tokenId 187 | ) 188 | internal 189 | override 190 | virtual 191 | { 192 | require(idToOwner[_tokenId] == address(0), NFT_ALREADY_EXISTS); 193 | idToOwner[_tokenId] = _to; 194 | 195 | ownerToIds[_to].push(_tokenId); 196 | idToOwnerIndex[_tokenId] = ownerToIds[_to].length - 1; 197 | } 198 | 199 | /** 200 | * @dev Helper function that gets NFT count of owner. This is needed for overriding in enumerable 201 | * extension to remove double storage(gas optimization) of owner NFT count. 202 | * @param _owner Address for whom to query the count. 203 | * @return Number of _owner NFTs. 204 | */ 205 | function _getOwnerNFTCount( 206 | address _owner 207 | ) 208 | internal 209 | override 210 | virtual 211 | view 212 | returns (uint256) 213 | { 214 | return ownerToIds[_owner].length; 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /src/contracts/tokens/nf-token-metadata.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "./nf-token.sol"; 5 | import "./erc721-metadata.sol"; 6 | 7 | /** 8 | * @dev Optional metadata implementation for ERC-721 non-fungible token standard. 9 | */ 10 | contract NFTokenMetadata is 11 | NFToken, 12 | ERC721Metadata 13 | { 14 | 15 | /** 16 | * @dev A descriptive name for a collection of NFTs. 17 | */ 18 | string internal nftName; 19 | 20 | /** 21 | * @dev An abbreviated name for NFTokens. 22 | */ 23 | string internal nftSymbol; 24 | 25 | /** 26 | * @dev Mapping from NFT ID to metadata uri. 27 | */ 28 | mapping (uint256 => string) internal idToUri; 29 | 30 | /** 31 | * @notice When implementing this contract don't forget to set nftName and nftSymbol. 32 | * @dev Contract constructor. 33 | */ 34 | constructor() 35 | { 36 | supportedInterfaces[0x5b5e139f] = true; // ERC721Metadata 37 | } 38 | 39 | /** 40 | * @dev Returns a descriptive name for a collection of NFTokens. 41 | * @return _name Representing name. 42 | */ 43 | function name() 44 | external 45 | override 46 | view 47 | returns (string memory _name) 48 | { 49 | _name = nftName; 50 | } 51 | 52 | /** 53 | * @dev Returns an abbreviated name for NFTokens. 54 | * @return _symbol Representing symbol. 55 | */ 56 | function symbol() 57 | external 58 | override 59 | view 60 | returns (string memory _symbol) 61 | { 62 | _symbol = nftSymbol; 63 | } 64 | 65 | /** 66 | * @dev A distinct URI (RFC 3986) for a given NFT. 67 | * @param _tokenId Id for which we want uri. 68 | * @return URI of _tokenId. 69 | */ 70 | function tokenURI( 71 | uint256 _tokenId 72 | ) 73 | external 74 | override 75 | view 76 | validNFToken(_tokenId) 77 | returns (string memory) 78 | { 79 | return _tokenURI(_tokenId); 80 | } 81 | 82 | /** 83 | * @notice This is an internal function that can be overriden if you want to implement a different 84 | * way to generate token URI. 85 | * @param _tokenId Id for which we want uri. 86 | * @return URI of _tokenId. 87 | */ 88 | function _tokenURI( 89 | uint256 _tokenId 90 | ) 91 | internal 92 | virtual 93 | view 94 | returns (string memory) 95 | { 96 | return idToUri[_tokenId]; 97 | } 98 | 99 | /** 100 | * @notice This is an internal function which should be called from user-implemented external 101 | * burn function. Its purpose is to show and properly initialize data structures when using this 102 | * implementation. Also, note that this burn implementation allows the minter to re-mint a burned 103 | * NFT. 104 | * @dev Burns a NFT. 105 | * @param _tokenId ID of the NFT to be burned. 106 | */ 107 | function _burn( 108 | uint256 _tokenId 109 | ) 110 | internal 111 | override 112 | virtual 113 | { 114 | super._burn(_tokenId); 115 | 116 | delete idToUri[_tokenId]; 117 | } 118 | 119 | /** 120 | * @notice This is an internal function which should be called from user-implemented external 121 | * function. Its purpose is to show and properly initialize data structures when using this 122 | * implementation. 123 | * @dev Set a distinct URI (RFC 3986) for a given NFT ID. 124 | * @param _tokenId Id for which we want URI. 125 | * @param _uri String representing RFC 3986 URI. 126 | */ 127 | function _setTokenUri( 128 | uint256 _tokenId, 129 | string memory _uri 130 | ) 131 | internal 132 | validNFToken(_tokenId) 133 | { 134 | idToUri[_tokenId] = _uri; 135 | } 136 | 137 | } 138 | -------------------------------------------------------------------------------- /src/contracts/tokens/nf-token.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "./erc721.sol"; 5 | import "./erc721-token-receiver.sol"; 6 | import "../utils/supports-interface.sol"; 7 | import "../utils/address-utils.sol"; 8 | 9 | /** 10 | * @dev Implementation of ERC-721 non-fungible token standard. 11 | */ 12 | contract NFToken is 13 | ERC721, 14 | SupportsInterface 15 | { 16 | using AddressUtils for address; 17 | 18 | /** 19 | * @dev List of revert message codes. Implementing dApp should handle showing the correct message. 20 | * Based on 0xcert framework error codes. 21 | */ 22 | string constant ZERO_ADDRESS = "003001"; 23 | string constant NOT_VALID_NFT = "003002"; 24 | string constant NOT_OWNER_OR_OPERATOR = "003003"; 25 | string constant NOT_OWNER_APPROVED_OR_OPERATOR = "003004"; 26 | string constant NOT_ABLE_TO_RECEIVE_NFT = "003005"; 27 | string constant NFT_ALREADY_EXISTS = "003006"; 28 | string constant NOT_OWNER = "003007"; 29 | string constant IS_OWNER = "003008"; 30 | 31 | /** 32 | * @dev Magic value of a smart contract that can receive NFT. 33 | * Equal to: bytes4(keccak256("onERC721Received(address,address,uint256,bytes)")). 34 | */ 35 | bytes4 internal constant MAGIC_ON_ERC721_RECEIVED = 0x150b7a02; 36 | 37 | /** 38 | * @dev A mapping from NFT ID to the address that owns it. 39 | */ 40 | mapping (uint256 => address) internal idToOwner; 41 | 42 | /** 43 | * @dev Mapping from NFT ID to approved address. 44 | */ 45 | mapping (uint256 => address) internal idToApproval; 46 | 47 | /** 48 | * @dev Mapping from owner address to count of their tokens. 49 | */ 50 | mapping (address => uint256) private ownerToNFTokenCount; 51 | 52 | /** 53 | * @dev Mapping from owner address to mapping of operator addresses. 54 | */ 55 | mapping (address => mapping (address => bool)) internal ownerToOperators; 56 | 57 | /** 58 | * @dev Guarantees that the msg.sender is an owner or operator of the given NFT. 59 | * @param _tokenId ID of the NFT to validate. 60 | */ 61 | modifier canOperate( 62 | uint256 _tokenId 63 | ) 64 | { 65 | address tokenOwner = idToOwner[_tokenId]; 66 | require( 67 | tokenOwner == msg.sender || ownerToOperators[tokenOwner][msg.sender], 68 | NOT_OWNER_OR_OPERATOR 69 | ); 70 | _; 71 | } 72 | 73 | /** 74 | * @dev Guarantees that the msg.sender is allowed to transfer NFT. 75 | * @param _tokenId ID of the NFT to transfer. 76 | */ 77 | modifier canTransfer( 78 | uint256 _tokenId 79 | ) 80 | { 81 | address tokenOwner = idToOwner[_tokenId]; 82 | require( 83 | tokenOwner == msg.sender 84 | || idToApproval[_tokenId] == msg.sender 85 | || ownerToOperators[tokenOwner][msg.sender], 86 | NOT_OWNER_APPROVED_OR_OPERATOR 87 | ); 88 | _; 89 | } 90 | 91 | /** 92 | * @dev Guarantees that _tokenId is a valid Token. 93 | * @param _tokenId ID of the NFT to validate. 94 | */ 95 | modifier validNFToken( 96 | uint256 _tokenId 97 | ) 98 | { 99 | require(idToOwner[_tokenId] != address(0), NOT_VALID_NFT); 100 | _; 101 | } 102 | 103 | /** 104 | * @dev Contract constructor. 105 | */ 106 | constructor() 107 | { 108 | supportedInterfaces[0x80ac58cd] = true; // ERC721 109 | } 110 | 111 | /** 112 | * @notice Throws unless `msg.sender` is the current owner, an authorized operator, or the 113 | * approved address for this NFT. Throws if `_from` is not the current owner. Throws if `_to` is 114 | * the zero address. Throws if `_tokenId` is not a valid NFT. When transfer is complete, this 115 | * function checks if `_to` is a smart contract (code size > 0). If so, it calls 116 | * `onERC721Received` on `_to` and throws if the return value is not 117 | * `bytes4(keccak256("onERC721Received(address,uint256,bytes)"))`. 118 | * @dev Transfers the ownership of an NFT from one address to another address. This function can 119 | * be changed to payable. 120 | * @param _from The current owner of the NFT. 121 | * @param _to The new owner. 122 | * @param _tokenId The NFT to transfer. 123 | * @param _data Additional data with no specified format, sent in call to `_to`. 124 | */ 125 | function safeTransferFrom( 126 | address _from, 127 | address _to, 128 | uint256 _tokenId, 129 | bytes calldata _data 130 | ) 131 | external 132 | override 133 | { 134 | _safeTransferFrom(_from, _to, _tokenId, _data); 135 | } 136 | 137 | /** 138 | * @notice This works identically to the other function with an extra data parameter, except this 139 | * function just sets data to "". 140 | * @dev Transfers the ownership of an NFT from one address to another address. This function can 141 | * be changed to payable. 142 | * @param _from The current owner of the NFT. 143 | * @param _to The new owner. 144 | * @param _tokenId The NFT to transfer. 145 | */ 146 | function safeTransferFrom( 147 | address _from, 148 | address _to, 149 | uint256 _tokenId 150 | ) 151 | external 152 | override 153 | { 154 | _safeTransferFrom(_from, _to, _tokenId, ""); 155 | } 156 | 157 | /** 158 | * @notice The caller is responsible to confirm that `_to` is capable of receiving NFTs or else 159 | * they may be permanently lost. 160 | * @dev Throws unless `msg.sender` is the current owner, an authorized operator, or the approved 161 | * address for this NFT. Throws if `_from` is not the current owner. Throws if `_to` is the zero 162 | * address. Throws if `_tokenId` is not a valid NFT. This function can be changed to payable. 163 | * @param _from The current owner of the NFT. 164 | * @param _to The new owner. 165 | * @param _tokenId The NFT to transfer. 166 | */ 167 | function transferFrom( 168 | address _from, 169 | address _to, 170 | uint256 _tokenId 171 | ) 172 | external 173 | override 174 | canTransfer(_tokenId) 175 | validNFToken(_tokenId) 176 | { 177 | address tokenOwner = idToOwner[_tokenId]; 178 | require(tokenOwner == _from, NOT_OWNER); 179 | require(_to != address(0), ZERO_ADDRESS); 180 | 181 | _transfer(_to, _tokenId); 182 | } 183 | 184 | /** 185 | * @notice The zero address indicates there is no approved address. Throws unless `msg.sender` is 186 | * the current NFT owner, or an authorized operator of the current owner. 187 | * @dev Set or reaffirm the approved address for an NFT. This function can be changed to payable. 188 | * @param _approved Address to be approved for the given NFT ID. 189 | * @param _tokenId ID of the token to be approved. 190 | */ 191 | function approve( 192 | address _approved, 193 | uint256 _tokenId 194 | ) 195 | external 196 | override 197 | canOperate(_tokenId) 198 | validNFToken(_tokenId) 199 | { 200 | address tokenOwner = idToOwner[_tokenId]; 201 | require(_approved != tokenOwner, IS_OWNER); 202 | 203 | idToApproval[_tokenId] = _approved; 204 | emit Approval(tokenOwner, _approved, _tokenId); 205 | } 206 | 207 | /** 208 | * @notice This works even if sender doesn't own any tokens at the time. 209 | * @dev Enables or disables approval for a third party ("operator") to manage all of 210 | * `msg.sender`'s assets. It also emits the ApprovalForAll event. 211 | * @param _operator Address to add to the set of authorized operators. 212 | * @param _approved True if the operators is approved, false to revoke approval. 213 | */ 214 | function setApprovalForAll( 215 | address _operator, 216 | bool _approved 217 | ) 218 | external 219 | override 220 | { 221 | ownerToOperators[msg.sender][_operator] = _approved; 222 | emit ApprovalForAll(msg.sender, _operator, _approved); 223 | } 224 | 225 | /** 226 | * @dev Returns the number of NFTs owned by `_owner`. NFTs assigned to the zero address are 227 | * considered invalid, and this function throws for queries about the zero address. 228 | * @param _owner Address for whom to query the balance. 229 | * @return Balance of _owner. 230 | */ 231 | function balanceOf( 232 | address _owner 233 | ) 234 | external 235 | override 236 | view 237 | returns (uint256) 238 | { 239 | require(_owner != address(0), ZERO_ADDRESS); 240 | return _getOwnerNFTCount(_owner); 241 | } 242 | 243 | /** 244 | * @dev Returns the address of the owner of the NFT. NFTs assigned to the zero address are 245 | * considered invalid, and queries about them do throw. 246 | * @param _tokenId The identifier for an NFT. 247 | * @return _owner Address of _tokenId owner. 248 | */ 249 | function ownerOf( 250 | uint256 _tokenId 251 | ) 252 | external 253 | override 254 | view 255 | returns (address _owner) 256 | { 257 | _owner = idToOwner[_tokenId]; 258 | require(_owner != address(0), NOT_VALID_NFT); 259 | } 260 | 261 | /** 262 | * @notice Throws if `_tokenId` is not a valid NFT. 263 | * @dev Get the approved address for a single NFT. 264 | * @param _tokenId ID of the NFT to query the approval of. 265 | * @return Address that _tokenId is approved for. 266 | */ 267 | function getApproved( 268 | uint256 _tokenId 269 | ) 270 | external 271 | override 272 | view 273 | validNFToken(_tokenId) 274 | returns (address) 275 | { 276 | return idToApproval[_tokenId]; 277 | } 278 | 279 | /** 280 | * @dev Checks if `_operator` is an approved operator for `_owner`. 281 | * @param _owner The address that owns the NFTs. 282 | * @param _operator The address that acts on behalf of the owner. 283 | * @return True if approved for all, false otherwise. 284 | */ 285 | function isApprovedForAll( 286 | address _owner, 287 | address _operator 288 | ) 289 | external 290 | override 291 | view 292 | returns (bool) 293 | { 294 | return ownerToOperators[_owner][_operator]; 295 | } 296 | 297 | /** 298 | * @notice Does NO checks. 299 | * @dev Actually performs the transfer. 300 | * @param _to Address of a new owner. 301 | * @param _tokenId The NFT that is being transferred. 302 | */ 303 | function _transfer( 304 | address _to, 305 | uint256 _tokenId 306 | ) 307 | internal 308 | virtual 309 | { 310 | address from = idToOwner[_tokenId]; 311 | _clearApproval(_tokenId); 312 | 313 | _removeNFToken(from, _tokenId); 314 | _addNFToken(_to, _tokenId); 315 | 316 | emit Transfer(from, _to, _tokenId); 317 | } 318 | 319 | /** 320 | * @notice This is an internal function which should be called from user-implemented external 321 | * mint function. Its purpose is to show and properly initialize data structures when using this 322 | * implementation. 323 | * @dev Mints a new NFT. 324 | * @param _to The address that will own the minted NFT. 325 | * @param _tokenId of the NFT to be minted by the msg.sender. 326 | */ 327 | function _mint( 328 | address _to, 329 | uint256 _tokenId 330 | ) 331 | internal 332 | virtual 333 | { 334 | require(_to != address(0), ZERO_ADDRESS); 335 | require(idToOwner[_tokenId] == address(0), NFT_ALREADY_EXISTS); 336 | 337 | _addNFToken(_to, _tokenId); 338 | 339 | emit Transfer(address(0), _to, _tokenId); 340 | } 341 | 342 | /** 343 | * @notice This is an internal function which should be called from user-implemented external burn 344 | * function. Its purpose is to show and properly initialize data structures when using this 345 | * implementation. Also, note that this burn implementation allows the minter to re-mint a burned 346 | * NFT. 347 | * @dev Burns a NFT. 348 | * @param _tokenId ID of the NFT to be burned. 349 | */ 350 | function _burn( 351 | uint256 _tokenId 352 | ) 353 | internal 354 | virtual 355 | validNFToken(_tokenId) 356 | { 357 | address tokenOwner = idToOwner[_tokenId]; 358 | _clearApproval(_tokenId); 359 | _removeNFToken(tokenOwner, _tokenId); 360 | emit Transfer(tokenOwner, address(0), _tokenId); 361 | } 362 | 363 | /** 364 | * @notice Use and override this function with caution. Wrong usage can have serious consequences. 365 | * @dev Removes a NFT from owner. 366 | * @param _from Address from which we want to remove the NFT. 367 | * @param _tokenId Which NFT we want to remove. 368 | */ 369 | function _removeNFToken( 370 | address _from, 371 | uint256 _tokenId 372 | ) 373 | internal 374 | virtual 375 | { 376 | require(idToOwner[_tokenId] == _from, NOT_OWNER); 377 | ownerToNFTokenCount[_from] -= 1; 378 | delete idToOwner[_tokenId]; 379 | } 380 | 381 | /** 382 | * @notice Use and override this function with caution. Wrong usage can have serious consequences. 383 | * @dev Assigns a new NFT to owner. 384 | * @param _to Address to which we want to add the NFT. 385 | * @param _tokenId Which NFT we want to add. 386 | */ 387 | function _addNFToken( 388 | address _to, 389 | uint256 _tokenId 390 | ) 391 | internal 392 | virtual 393 | { 394 | require(idToOwner[_tokenId] == address(0), NFT_ALREADY_EXISTS); 395 | 396 | idToOwner[_tokenId] = _to; 397 | ownerToNFTokenCount[_to] += 1; 398 | } 399 | 400 | /** 401 | * @dev Helper function that gets NFT count of owner. This is needed for overriding in enumerable 402 | * extension to remove double storage (gas optimization) of owner NFT count. 403 | * @param _owner Address for whom to query the count. 404 | * @return Number of _owner NFTs. 405 | */ 406 | function _getOwnerNFTCount( 407 | address _owner 408 | ) 409 | internal 410 | virtual 411 | view 412 | returns (uint256) 413 | { 414 | return ownerToNFTokenCount[_owner]; 415 | } 416 | 417 | /** 418 | * @dev Actually perform the safeTransferFrom. 419 | * @param _from The current owner of the NFT. 420 | * @param _to The new owner. 421 | * @param _tokenId The NFT to transfer. 422 | * @param _data Additional data with no specified format, sent in call to `_to`. 423 | */ 424 | function _safeTransferFrom( 425 | address _from, 426 | address _to, 427 | uint256 _tokenId, 428 | bytes memory _data 429 | ) 430 | private 431 | canTransfer(_tokenId) 432 | validNFToken(_tokenId) 433 | { 434 | address tokenOwner = idToOwner[_tokenId]; 435 | require(tokenOwner == _from, NOT_OWNER); 436 | require(_to != address(0), ZERO_ADDRESS); 437 | 438 | _transfer(_to, _tokenId); 439 | 440 | if (_to.isContract()) 441 | { 442 | bytes4 retval = ERC721TokenReceiver(_to).onERC721Received(msg.sender, _from, _tokenId, _data); 443 | require(retval == MAGIC_ON_ERC721_RECEIVED, NOT_ABLE_TO_RECEIVE_NFT); 444 | } 445 | } 446 | 447 | /** 448 | * @dev Clears the current approval of a given NFT ID. 449 | * @param _tokenId ID of the NFT to be transferred. 450 | */ 451 | function _clearApproval( 452 | uint256 _tokenId 453 | ) 454 | private 455 | { 456 | delete idToApproval[_tokenId]; 457 | } 458 | 459 | } 460 | -------------------------------------------------------------------------------- /src/contracts/utils/address-utils.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | /** 5 | * @notice Based on: 6 | * https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Address.sol 7 | * Requires EIP-1052. 8 | * @dev Utility library of inline functions on addresses. 9 | */ 10 | library AddressUtils 11 | { 12 | 13 | /** 14 | * @dev Returns whether the target address is a contract. 15 | * @param _addr Address to check. 16 | * @return addressCheck True if _addr is a contract, false if not. 17 | */ 18 | function isContract( 19 | address _addr 20 | ) 21 | internal 22 | view 23 | returns (bool addressCheck) 24 | { 25 | // This method relies in extcodesize, which returns 0 for contracts in 26 | // construction, since the code is only stored at the end of the 27 | // constructor execution. 28 | 29 | // According to EIP-1052, 0x0 is the value returned for not-yet created accounts 30 | // and 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470 is returned 31 | // for accounts without code, i.e. `keccak256('')` 32 | bytes32 codehash; 33 | bytes32 accountHash = 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470; 34 | assembly { codehash := extcodehash(_addr) } // solhint-disable-line 35 | addressCheck = (codehash != 0x0 && codehash != accountHash); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/contracts/utils/erc165.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | /** 5 | * @dev A standard for detecting smart contract interfaces. 6 | * See: https://eips.ethereum.org/EIPS/eip-165. 7 | */ 8 | interface ERC165 9 | { 10 | 11 | /** 12 | * @dev Checks if the smart contract includes a specific interface. 13 | * This function uses less than 30,000 gas. 14 | * @param _interfaceID The interface identifier, as specified in ERC-165. 15 | * @return True if _interfaceID is supported, false otherwise. 16 | */ 17 | function supportsInterface( 18 | bytes4 _interfaceID 19 | ) 20 | external 21 | view 22 | returns (bool); 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/contracts/utils/supports-interface.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "./erc165.sol"; 5 | 6 | /** 7 | * @dev Implementation of standard for detect smart contract interfaces. 8 | */ 9 | contract SupportsInterface is 10 | ERC165 11 | { 12 | 13 | /** 14 | * @dev Mapping of supported intefraces. You must not set element 0xffffffff to true. 15 | */ 16 | mapping(bytes4 => bool) internal supportedInterfaces; 17 | 18 | /** 19 | * @dev Contract constructor. 20 | */ 21 | constructor() 22 | { 23 | supportedInterfaces[0x01ffc9a7] = true; // ERC165 24 | } 25 | 26 | /** 27 | * @dev Function to check which interfaces are suported by this contract. 28 | * @param _interfaceID Id of the interface. 29 | * @return True if _interfaceID is supported, false otherwise. 30 | */ 31 | function supportsInterface( 32 | bytes4 _interfaceID 33 | ) 34 | external 35 | override 36 | view 37 | returns (bool) 38 | { 39 | return supportedInterfaces[_interfaceID]; 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/tests/mocks/address-utils-mock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "../../contracts/utils/address-utils.sol"; 5 | 6 | contract AddressUtilsMock { 7 | using AddressUtils for address; 8 | 9 | function isContract( 10 | address _addr 11 | ) 12 | external 13 | view 14 | returns (bool addressCheck) 15 | { 16 | addressCheck = _addr.isContract(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/tests/mocks/nf-token-enumerable-test-mock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "../../contracts/mocks/nf-token-enumerable-mock.sol"; 5 | 6 | contract NFTokenEnumerableTestMock is 7 | NFTokenEnumerableMock 8 | { 9 | 10 | function ownerToIdsLen( 11 | address _owner 12 | ) 13 | external 14 | view 15 | returns (uint256) 16 | { 17 | return ownerToIds[_owner].length; 18 | } 19 | 20 | function ownerToIdbyIndex( 21 | address _owner, 22 | uint256 _index 23 | ) 24 | external 25 | view 26 | returns (uint256) 27 | { 28 | return ownerToIds[_owner][_index]; 29 | } 30 | 31 | function idToOwnerIndexWrapper( 32 | uint256 _tokenId 33 | ) 34 | external 35 | view 36 | returns (uint256) 37 | { 38 | return idToOwnerIndex[_tokenId]; 39 | } 40 | 41 | function idToIndexWrapper( 42 | uint256 _tokenId 43 | ) 44 | external 45 | view 46 | returns (uint256) 47 | { 48 | return idToIndex[_tokenId]; 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/tests/mocks/nf-token-metadata-enumerable-test-mock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "../../contracts/mocks/nf-token-metadata-enumerable-mock.sol"; 5 | 6 | contract NFTokenMetadataEnumerableTestMock is 7 | NFTokenMetadataEnumerableMock 8 | { 9 | 10 | constructor( 11 | string memory _name, 12 | string memory _symbol 13 | ) 14 | NFTokenMetadataEnumerableMock(_name, _symbol) 15 | { 16 | } 17 | 18 | function checkUri( 19 | uint256 _tokenId 20 | ) 21 | external 22 | view 23 | returns (string memory) 24 | { 25 | return idToUri[_tokenId]; 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/tests/mocks/nf-token-metadata-test-mock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "../../contracts/mocks/nf-token-metadata-mock.sol"; 5 | 6 | contract NFTokenMetadataTestMock is 7 | NFTokenMetadataMock 8 | { 9 | 10 | constructor( 11 | string memory _name, 12 | string memory _symbol 13 | ) 14 | NFTokenMetadataMock(_name, _symbol) 15 | { 16 | } 17 | 18 | function checkUri( 19 | uint256 _tokenId 20 | ) 21 | external 22 | view 23 | returns (string memory) 24 | { 25 | return idToUri[_tokenId]; 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/tests/mocks/nf-token-receiver-test-mock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "../../contracts/tokens/erc721-token-receiver.sol"; 5 | 6 | contract NFTokenReceiverTestMock is 7 | ERC721TokenReceiver 8 | { 9 | event Received(); 10 | 11 | function onERC721Received( 12 | address _operator, 13 | address _from, 14 | uint256 _tokenId, 15 | bytes calldata _data 16 | ) 17 | external 18 | override 19 | returns(bytes4) 20 | { 21 | _operator; 22 | _from; 23 | _tokenId; 24 | _data; 25 | emit Received(); 26 | return 0x150b7a02; 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/tests/mocks/nf-token-test-mock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "../../contracts/mocks/nf-token-mock.sol"; 5 | 6 | contract NFTokenTestMock is 7 | NFTokenMock 8 | { 9 | 10 | /** 11 | * @dev Removes a NFT from owner. 12 | * @param _tokenId Which NFT we want to remove. 13 | */ 14 | function burn( 15 | uint256 _tokenId 16 | ) 17 | external 18 | onlyOwner 19 | { 20 | super._burn(_tokenId); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/tests/mocks/sends-to-self-on-construct.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "../../contracts/mocks/nf-token-mock.sol"; 5 | import "./nf-token-receiver-test-mock.sol"; 6 | 7 | contract SendsToSelfOnConstruct is 8 | NFTokenReceiverTestMock 9 | { 10 | uint constant TOKEN_ID = 1; 11 | 12 | constructor() 13 | { 14 | NFTokenMock tokens = new NFTokenMock(); 15 | tokens.mint(address(this), TOKEN_ID); 16 | tokens.safeTransferFrom(address(this), address(this), TOKEN_ID); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/tests/tokens/nf-token-enumerable.js: -------------------------------------------------------------------------------- 1 | 2 | const { expect } = require('chai'); 3 | 4 | describe('nf-token-enumerable', function() { 5 | let nfToken, owner, bob, jane, sara; 6 | const id1 = 123; 7 | const id2 = 124; 8 | const id3 = 125; 9 | 10 | beforeEach(async () => { 11 | const nftContract = await ethers.getContractFactory('NFTokenEnumerableTestMock'); 12 | nfToken = await nftContract.deploy(); 13 | [ owner, bob, jane, sara] = await ethers.getSigners(); 14 | await nfToken.deployed(); 15 | }); 16 | 17 | it('correctly checks all the supported interfaces', async function() { 18 | expect(await nfToken.supportsInterface('0x80ac58cd')).to.equal(true); 19 | expect(await nfToken.supportsInterface('0x780e9d63')).to.equal(true); 20 | expect(await nfToken.supportsInterface('0x5b5e139f')).to.equal(false); 21 | }); 22 | 23 | it('correctly mints a NFT', async function() { 24 | expect(await nfToken.connect(owner).mint(bob.address, id1)).to.emit(nfToken, 'Transfer'); 25 | expect(await nfToken.balanceOf(bob.address)).to.equal(1); 26 | expect(await nfToken.totalSupply()).to.equal(1); 27 | }); 28 | 29 | it('returns the correct token by index', async function() { 30 | await nfToken.connect(owner).mint(bob.address, id1); 31 | await nfToken.connect(owner).mint(bob.address, id2); 32 | await nfToken.connect(owner).mint(bob.address, id3); 33 | expect(await nfToken.tokenByIndex(0)).to.equal(id1); 34 | expect(await nfToken.tokenByIndex(1)).to.equal(id2); 35 | expect(await nfToken.tokenByIndex(2)).to.equal(id3); 36 | }); 37 | 38 | it('throws when trying to get token by non-existing index', async function() { 39 | await nfToken.connect(owner).mint(bob.address, id1); 40 | await expect(nfToken.tokenByIndex(1)).to.be.revertedWith('005007'); 41 | }); 42 | 43 | it('returns the correct token of owner by index', async function() { 44 | await nfToken.connect(owner).mint(bob.address, id1); 45 | await nfToken.connect(owner).mint(bob.address, id2); 46 | await nfToken.connect(owner).mint(sara.address, id3); 47 | expect(await nfToken.tokenOfOwnerByIndex(bob.address, 1)).to.equal(id2); 48 | }); 49 | 50 | it('throws when trying to get token of owner by non-existing index', async function() { 51 | await nfToken.connect(owner).mint(bob.address, id1); 52 | await expect(nfToken.tokenOfOwnerByIndex(bob.address, 1)).to.be.revertedWith('005007'); 53 | }); 54 | 55 | it('mint should correctly set ownerToIds and idToOwnerIndex and idToIndex', async function() { 56 | await nfToken.connect(owner).mint(bob.address, id1); 57 | await nfToken.connect(owner).mint(bob.address, id3); 58 | await nfToken.connect(owner).mint(bob.address, id2); 59 | 60 | expect(await nfToken.idToOwnerIndexWrapper(id1)).to.equal(0); 61 | expect(await nfToken.idToOwnerIndexWrapper(id3)).to.equal(1); 62 | expect(await nfToken.idToOwnerIndexWrapper(id2)).to.equal(2); 63 | expect(await nfToken.ownerToIdsLen(bob.address)).to.equal(3); 64 | expect(await nfToken.ownerToIdbyIndex(bob.address, 0)).to.equal(id1); 65 | expect(await nfToken.ownerToIdbyIndex(bob.address, 1)).to.equal(id3); 66 | expect(await nfToken.ownerToIdbyIndex(bob.address, 2)).to.equal(id2); 67 | expect(await nfToken.idToIndexWrapper(id1)).to.equal(0); 68 | expect(await nfToken.idToIndexWrapper(id3)).to.equal(1); 69 | expect(await nfToken.idToIndexWrapper(id2)).to.equal(2); 70 | }); 71 | 72 | it('burn should correctly set ownerToIds and idToOwnerIndex and idToIndex', async function() { 73 | await nfToken.connect(owner).mint(bob.address, id1); 74 | await nfToken.connect(owner).mint(bob.address, id3); 75 | await nfToken.connect(owner).mint(bob.address, id2); 76 | 77 | //burn id1 78 | await nfToken.connect(owner).burn(id1); 79 | expect(await nfToken.idToOwnerIndexWrapper(id3)).to.equal(1); 80 | expect(await nfToken.idToOwnerIndexWrapper(id2)).to.equal(0); 81 | expect(await nfToken.ownerToIdsLen(bob.address)).to.equal(2); 82 | expect(await nfToken.ownerToIdbyIndex(bob.address, 0)).to.equal(id2); 83 | expect(await nfToken.ownerToIdbyIndex(bob.address, 1)).to.equal(id3); 84 | expect(await nfToken.idToIndexWrapper(id2)).to.equal(0); 85 | expect(await nfToken.idToIndexWrapper(id3)).to.equal(1); 86 | expect(await nfToken.tokenByIndex(0)).to.equal(id2); 87 | expect(await nfToken.tokenByIndex(1)).to.equal(id3); 88 | 89 | //burn id2 90 | await nfToken.connect(owner).burn(id2); 91 | expect(await nfToken.idToOwnerIndexWrapper(id3)).to.equal(0); 92 | expect(await nfToken.ownerToIdsLen(bob.address)).to.equal(1); 93 | expect(await nfToken.ownerToIdbyIndex(bob.address, 0)).to.equal(id3); 94 | expect(await nfToken.idToIndexWrapper(id3)).to.equal(0); 95 | expect(await nfToken.tokenByIndex(0)).to.equal(id3); 96 | 97 | //burn id3 98 | await nfToken.connect(owner).burn(id3); 99 | expect(await nfToken.idToOwnerIndexWrapper(id3)).to.equal(0); 100 | expect(await nfToken.ownerToIdsLen(bob.address)).to.equal(0); 101 | await expect(nfToken.ownerToIdbyIndex(bob.address, 0)).to.be.revertedWith('VM Exception while processing transaction: reverted with panic code 0x32 (Array accessed at an out-of-bounds or negative index)'); 102 | expect(await nfToken.idToIndexWrapper(id3)).to.equal(0); 103 | }); 104 | 105 | it('transfer should correctly set ownerToIds and idToOwnerIndex and idToIndex', async function() { 106 | await nfToken.connect(owner).mint(bob.address, id1); 107 | await nfToken.connect(owner).mint(bob.address, id3); 108 | await nfToken.connect(owner).mint(bob.address, id2); 109 | await nfToken.connect(bob).transferFrom(bob.address, sara.address, id1); 110 | 111 | expect(await nfToken.idToOwnerIndexWrapper(id1)).to.equal(0); 112 | expect(await nfToken.idToOwnerIndexWrapper(id3)).to.equal(1); 113 | expect(await nfToken.idToOwnerIndexWrapper(id2)).to.equal(0); 114 | 115 | expect(await nfToken.ownerToIdsLen(bob.address)).to.equal(2); 116 | expect(await nfToken.ownerToIdbyIndex(bob.address, 0)).to.equal(id2); 117 | expect(await nfToken.ownerToIdbyIndex(bob.address, 1)).to.equal(id3); 118 | await expect(nfToken.ownerToIdbyIndex(bob.address, 2)).to.be.revertedWith('VM Exception while processing transaction: reverted with panic code 0x32 (Array accessed at an out-of-bounds or negative index)'); 119 | 120 | expect(await nfToken.ownerToIdsLen(sara.address)).to.equal(1); 121 | expect(await nfToken.ownerToIdbyIndex(sara.address, 0)).to.equal(id1); 122 | }); 123 | 124 | }); 125 | -------------------------------------------------------------------------------- /src/tests/tokens/nf-token-metadata-enumerable.js: -------------------------------------------------------------------------------- 1 | 2 | const { expect } = require('chai'); 3 | 4 | describe('nf-token-enumerable', function() { 5 | let nfToken, owner, bob, jane, sara; 6 | const id1 = 123; 7 | const id2 = 124; 8 | const id3 = 125; 9 | const uri1 = 'http://nibbstack.com/1'; 10 | const uri2 = 'http://nibbstack.com/2'; 11 | const uri3 = 'http://nibbstack.com/3'; 12 | 13 | beforeEach(async () => { 14 | const nftContract = await ethers.getContractFactory('NFTokenMetadataEnumerableTestMock'); 15 | nfToken = await nftContract.deploy( 16 | 'Foo', 17 | 'F', 18 | ); 19 | [ owner, bob, jane, sara] = await ethers.getSigners(); 20 | await nfToken.deployed(); 21 | }); 22 | 23 | it('correctly checks all the supported interfaces', async function() { 24 | expect(await nfToken.supportsInterface('0x80ac58cd')).to.equal(true); 25 | expect(await nfToken.supportsInterface('0x5b5e139f')).to.equal(true); 26 | expect(await nfToken.supportsInterface('0x780e9d63')).to.equal(true); 27 | }); 28 | 29 | it('returns the correct contract name', async function() { 30 | expect(await nfToken.name()).to.equal('Foo'); 31 | }); 32 | 33 | it('returns the correct contract symbol', async function() { 34 | expect(await nfToken.symbol()).to.equal('F'); 35 | }); 36 | 37 | it('returns the correct NFT id 1 url', async function() { 38 | await nfToken.connect(owner).mint(bob.address, id1, uri1); 39 | expect(await nfToken.tokenURI(id1)).to.equal(uri1); 40 | }); 41 | 42 | it('throws when trying to get URI of invalid NFT ID', async function() { 43 | await expect(nfToken.tokenURI(id1)).to.be.revertedWith('003002'); 44 | }); 45 | 46 | it('correctly mints a NFT', async function() { 47 | expect(await nfToken.connect(owner).mint(bob.address, id1, uri1)).to.emit(nfToken, 'Transfer'); 48 | expect(await nfToken.balanceOf(bob.address)).to.equal(1); 49 | expect(await nfToken.totalSupply()).to.equal(1); 50 | }); 51 | 52 | it('returns the correct token by index', async function() { 53 | await nfToken.connect(owner).mint(bob.address, id1, uri1); 54 | await nfToken.connect(owner).mint(bob.address, id2, uri2); 55 | await nfToken.connect(owner).mint(bob.address, id3, uri3); 56 | expect(await nfToken.tokenByIndex(0)).to.equal(id1); 57 | expect(await nfToken.tokenByIndex(1)).to.equal(id2); 58 | expect(await nfToken.tokenByIndex(2)).to.equal(id3); 59 | }); 60 | 61 | it('throws when trying to get token by non-existing index', async function() { 62 | await nfToken.connect(owner).mint(bob.address, id1, uri1); 63 | await expect(nfToken.tokenByIndex(1)).to.be.revertedWith('005007'); 64 | }); 65 | 66 | it('returns the correct token of owner by index', async function() { 67 | await nfToken.connect(owner).mint(bob.address, id1, uri1); 68 | await nfToken.connect(owner).mint(bob.address, id2, uri2); 69 | await nfToken.connect(owner).mint(sara.address, id3, uri3); 70 | expect(await nfToken.tokenOfOwnerByIndex(bob.address, 1)).to.equal(id2); 71 | }); 72 | 73 | it('throws when trying to get token of owner by non-existing index', async function() { 74 | await nfToken.connect(owner).mint(bob.address, id1, uri1); 75 | await expect(nfToken.tokenOfOwnerByIndex(bob.address, 1)).to.be.revertedWith('005007'); 76 | }); 77 | 78 | it('correctly burns a NFT', async function() { 79 | await nfToken.connect(owner).mint(bob.address, id1, uri1); 80 | expect(await nfToken.connect(owner).burn(id1)).to.emit(nfToken, 'Transfer'); 81 | expect(await nfToken.balanceOf(bob.address)).to.equal(0); 82 | await expect(nfToken.ownerOf(id1)).to.be.revertedWith('003002'); 83 | await expect(nfToken.tokenByIndex(0)).to.be.revertedWith('005007'); 84 | await expect(nfToken.tokenOfOwnerByIndex(bob.address, 0)).to.be.revertedWith('005007'); 85 | }); 86 | 87 | }); 88 | -------------------------------------------------------------------------------- /src/tests/tokens/nf-token-metadata.js: -------------------------------------------------------------------------------- 1 | 2 | const { expect } = require('chai'); 3 | 4 | describe('nf-token-metadata', function() { 5 | let nfToken, owner, bob; 6 | const id1 = 1; 7 | const uri1 = 'http://nibbstack.com/1'; 8 | 9 | beforeEach(async () => { 10 | const nftContract = await ethers.getContractFactory('NFTokenMetadataTestMock'); 11 | nfToken = await nftContract.deploy( 12 | 'Foo', 13 | 'F', 14 | ); 15 | [ owner, bob] = await ethers.getSigners(); 16 | await nfToken.deployed(); 17 | }); 18 | 19 | it('correctly checks all the supported interfaces', async function() { 20 | expect(await nfToken.supportsInterface('0x80ac58cd')).to.equal(true); 21 | expect(await nfToken.supportsInterface('0x5b5e139f')).to.equal(true); 22 | expect(await nfToken.supportsInterface('0x780e9d63')).to.equal(false); 23 | }); 24 | 25 | it('returns the correct contract name', async function() { 26 | expect(await nfToken.name()).to.equal('Foo'); 27 | }); 28 | 29 | it('returns the correct contract symbol', async function() { 30 | expect(await nfToken.symbol()).to.equal('F'); 31 | }); 32 | 33 | it('correctly mints a NFT', async function() { 34 | expect(await nfToken.connect(owner).mint(bob.address, id1, uri1)).to.emit(nfToken, 'Transfer'); 35 | expect(await nfToken.balanceOf(bob.address)).to.equal(1); 36 | expect(await nfToken.tokenURI(id1)).to.equal(uri1); 37 | }); 38 | 39 | it('throws when trying to get URI of invalid NFT ID', async function() { 40 | await expect(nfToken.tokenURI(id1)).to.be.revertedWith('003002'); 41 | }); 42 | 43 | it('correctly burns a NFT', async function() { 44 | await nfToken.connect(owner).mint(bob.address, id1, uri1); 45 | expect(await nfToken.connect(owner).burn(id1)).to.emit(nfToken, 'Transfer'); 46 | expect(await nfToken.balanceOf(bob.address)).to.equal(0); 47 | await expect(nfToken.ownerOf(id1)).to.be.revertedWith('003002'); 48 | expect(await nfToken.checkUri(id1)).to.equal(''); 49 | }); 50 | 51 | }); 52 | -------------------------------------------------------------------------------- /src/tests/tokens/nf-token.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); 2 | 3 | describe('nf-token', function() { 4 | let nfToken, owner, bob, jane, sara; 5 | const zeroAddress = '0x0000000000000000000000000000000000000000'; 6 | const id1 = 123; 7 | const id2 = 124; 8 | 9 | beforeEach(async () => { 10 | const nftContract = await ethers.getContractFactory('NFTokenTestMock'); 11 | nfToken = await nftContract.deploy(); 12 | [ owner, bob, jane, sara] = await ethers.getSigners(); 13 | await nfToken.deployed(); 14 | }); 15 | 16 | it('correctly checks all the supported interfaces', async function() { 17 | expect(await nfToken.supportsInterface('0x80ac58cd')).to.equal(true); 18 | expect(await nfToken.supportsInterface('0x5b5e139f')).to.equal(false); 19 | }); 20 | 21 | it('correctly mints a NFT', async function() { 22 | expect(await nfToken.connect(owner).mint(bob.address, 1)).to.emit(nfToken, 'Transfer'); 23 | expect(await nfToken.balanceOf(bob.address)).to.equal(1); 24 | }); 25 | 26 | it('returns correct balanceOf', async function() { 27 | await nfToken.connect(owner).mint(bob.address, id1); 28 | expect(await nfToken.balanceOf(bob.address)).to.equal(1); 29 | await nfToken.connect(owner).mint(bob.address, id2); 30 | expect(await nfToken.balanceOf(bob.address)).to.equal(2); 31 | }); 32 | 33 | it('throws when trying to get count of NFTs owned by 0x0 address', async function() { 34 | await expect(nfToken.balanceOf(zeroAddress)).to.be.revertedWith('003001'); 35 | }); 36 | 37 | it('throws when trying to mint 2 NFTs with the same ids', async function() { 38 | await nfToken.connect(owner).mint(bob.address, id1); 39 | await expect(nfToken.connect(owner).mint(bob.address, id1)).to.be.revertedWith('003006'); 40 | }); 41 | 42 | it('throws when trying to mint NFT to 0x0 address', async function() { 43 | await expect(nfToken.connect(owner).mint(zeroAddress, id1)).to.be.revertedWith('003001'); 44 | }); 45 | 46 | it('finds the correct owner of NFToken id', async function() { 47 | await nfToken.connect(owner).mint(bob.address, id1); 48 | expect(await nfToken.ownerOf(id1)).to.equal(bob.address); 49 | }); 50 | 51 | it('throws when trying to find owner od non-existing NFT id', async function() { 52 | await expect(nfToken.ownerOf(id1)).to.be.revertedWith('003002'); 53 | }); 54 | 55 | it('correctly approves account', async function() { 56 | await nfToken.connect(owner).mint(bob.address, id1); 57 | expect(await nfToken.connect(bob).approve(sara.address, id1)).to.emit(nfToken, 'Approval'); 58 | expect(await nfToken.getApproved(id1)).to.equal(sara.address); 59 | }); 60 | 61 | it('correctly cancels approval', async function() { 62 | await nfToken.connect(owner).mint(bob.address, id1); 63 | await nfToken.connect(bob).approve(sara.address, id1); 64 | await nfToken.connect(bob).approve(zeroAddress, id1); 65 | expect(await nfToken.getApproved(id1)).to.equal(zeroAddress); 66 | }); 67 | 68 | it('throws when trying to get approval of non-existing NFT id', async function() { 69 | await expect(nfToken.getApproved(id1)).to.be.revertedWith('003002'); 70 | }); 71 | 72 | it('throws when trying to approve NFT ID from a third party', async function() { 73 | await nfToken.connect(owner).mint(bob.address, id1); 74 | await expect(nfToken.connect(sara).approve(sara.address, id1)).to.be.revertedWith('003003'); 75 | }); 76 | 77 | it('correctly sets an operator', async function() { 78 | await nfToken.connect(owner).mint(bob.address, id1); 79 | expect(await nfToken.connect(bob).setApprovalForAll(sara.address, true)).to.emit(nfToken, 'ApprovalForAll'); 80 | expect(await nfToken.isApprovedForAll(bob.address, sara.address)).to.equal(true); 81 | }); 82 | 83 | it('correctly sets then cancels an operator', async function() { 84 | await nfToken.connect(owner).mint(bob.address, id1); 85 | await nfToken.connect(bob).setApprovalForAll(sara.address, true); 86 | await nfToken.connect(bob).setApprovalForAll(sara.address, false); 87 | expect(await nfToken.isApprovedForAll(bob.address, sara.address)).to.equal(false); 88 | }); 89 | 90 | it('correctly transfers NFT from owner', async function() { 91 | await nfToken.connect(owner).mint(bob.address, id1); 92 | expect(await nfToken.connect(bob).transferFrom(bob.address, sara.address, id1)).to.emit(nfToken, 'Transfer'); 93 | expect(await nfToken.balanceOf(bob.address)).to.equal(0); 94 | expect(await nfToken.balanceOf(sara.address)).to.equal(1); 95 | expect(await nfToken.ownerOf(id1)).to.equal(sara.address); 96 | }); 97 | 98 | it('correctly transfers NFT from approved address', async function() { 99 | await nfToken.connect(owner).mint(bob.address, id1); 100 | await nfToken.connect(bob).approve(sara.address, id1); 101 | await nfToken.connect(sara).transferFrom(bob.address, jane.address, id1); 102 | expect(await nfToken.balanceOf(bob.address)).to.equal(0); 103 | expect(await nfToken.balanceOf(jane.address)).to.equal(1); 104 | expect(await nfToken.ownerOf(id1)).to.equal(jane.address); 105 | }); 106 | 107 | it('correctly transfers NFT as operator', async function() { 108 | await nfToken.connect(owner).mint(bob.address, id1); 109 | await nfToken.connect(bob).setApprovalForAll(sara.address, true); 110 | await nfToken.connect(sara).transferFrom(bob.address, jane.address, id1); 111 | expect(await nfToken.balanceOf(bob.address)).to.equal(0); 112 | expect(await nfToken.balanceOf(jane.address)).to.equal(1); 113 | expect(await nfToken.ownerOf(id1)).to.equal(jane.address); 114 | }); 115 | 116 | it('throws when trying to transfer NFT as an address that is not owner, approved or operator', async function() { 117 | await nfToken.connect(owner).mint(bob.address, id1); 118 | await expect(nfToken.connect(sara).transferFrom(bob.address, jane.address, id1)).to.be.revertedWith('003004'); 119 | }); 120 | 121 | it('throws when trying to transfer NFT to a zero address', async function() { 122 | await nfToken.connect(owner).mint(bob.address, id1); 123 | await expect(nfToken.connect(bob).transferFrom(bob.address, zeroAddress, id1)).to.be.revertedWith('003001'); 124 | }); 125 | 126 | it('throws when trying to transfer an invalid NFT', async function() { 127 | await expect(nfToken.connect(bob).transferFrom(bob.address, sara.address, id1)).to.be.revertedWith('003004'); 128 | }); 129 | 130 | it('throws when trying to transfer an invalid NFT', async function() { 131 | await expect(nfToken.connect(bob).transferFrom(bob.address, sara.address, id1)).to.be.revertedWith('003004'); 132 | }); 133 | 134 | it('correctly safe transfers NFT from owner', async function() { 135 | await nfToken.connect(owner).mint(bob.address, id1); 136 | expect(await nfToken.connect(bob)['safeTransferFrom(address,address,uint256)'](bob.address, sara.address, id1)).to.emit(nfToken, 'Transfer'); 137 | expect(await nfToken.balanceOf(bob.address)).to.equal(0); 138 | expect(await nfToken.balanceOf(sara.address)).to.equal(1); 139 | expect(await nfToken.ownerOf(id1)).to.equal(sara.address); 140 | }); 141 | 142 | it('throws when trying to safe transfers NFT from owner to a smart contract', async function() { 143 | await nfToken.connect(owner).mint(bob.address, id1); 144 | await expect(nfToken.connect(bob)['safeTransferFrom(address,address,uint256)'](bob.address, nfToken.address, id1)).to.be.revertedWith('Transaction reverted: function selector was not recognized and there\'s no fallback function'); 145 | }); 146 | 147 | it('correctly safe transfers NFT from owner to smart contract that can receive NFTs', async function() { 148 | const tokenReceiverContract = await ethers.getContractFactory('NFTokenReceiverTestMock'); 149 | const tokenReceiver = await tokenReceiverContract.deploy(); 150 | await tokenReceiver.deployed(); 151 | 152 | await nfToken.connect(owner).mint(bob.address, id1); 153 | await nfToken.connect(bob)['safeTransferFrom(address,address,uint256)'](bob.address, tokenReceiver.address, id1); 154 | expect(await nfToken.balanceOf(bob.address)).to.equal(0); 155 | expect(await nfToken.balanceOf(tokenReceiver.address)).to.equal(1); 156 | expect(await nfToken.ownerOf(id1)).to.equal(tokenReceiver.address); 157 | }); 158 | 159 | it('correctly safe transfers NFT from owner to smart contract that can receive NFTs with data', async function() { 160 | const tokenReceiverContract = await ethers.getContractFactory('NFTokenReceiverTestMock'); 161 | const tokenReceiver = await tokenReceiverContract.deploy(); 162 | await tokenReceiver.deployed(); 163 | 164 | await nfToken.connect(owner).mint(bob.address, id1); 165 | expect(await nfToken.connect(bob)['safeTransferFrom(address,address,uint256,bytes)'](bob.address, tokenReceiver.address, id1, '0x01')).to.emit(nfToken, 'Transfer'); 166 | expect(await nfToken.balanceOf(bob.address)).to.equal(0); 167 | expect(await nfToken.balanceOf(tokenReceiver.address)).to.equal(1); 168 | expect(await nfToken.ownerOf(id1)).to.equal(tokenReceiver.address); 169 | }); 170 | 171 | it('correctly burns a NFT', async function() { 172 | await nfToken.connect(owner).mint(bob.address, id1); 173 | expect(await nfToken.connect(owner).burn(id1)).to.emit(nfToken, 'Transfer'); 174 | expect(await nfToken.balanceOf(bob.address)).to.equal(0); 175 | await expect(nfToken.ownerOf(id1)).to.be.revertedWith('003002'); 176 | }); 177 | 178 | it('throws when trying to burn non existent NFT', async function() { 179 | await expect(nfToken.connect(owner).burn(id1)).to.be.revertedWith('003002'); 180 | }); 181 | 182 | // it.only('safeTransfer does not call onERC721Received to constructing contract', async function() { 183 | // const sendsToSelfOnConstructContract = await ethers.getContractFactory('SendsToSelfOnConstruct'); 184 | // const sendsToSelfOnConstruct = await sendsToSelfOnConstructContract.deploy(); 185 | // expect(await sendsToSelfOnConstruct.deployed().deployTransaction).to.emit(sendsToSelfOnConstructContract, 'Transfer'); 186 | 187 | // console.log('here'); 188 | // // console.log(log); 189 | // // console.log(sendsToSelfOnConstruct); No Receive event, 2x Transfer 190 | // }); 191 | 192 | }); 193 | -------------------------------------------------------------------------------- /src/tests/utils/address-utils.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); 2 | 3 | describe('address utils', function() { 4 | let addressUtils, owner; 5 | 6 | beforeEach(async () => { 7 | const addressUtilsContract = await ethers.getContractFactory('AddressUtilsMock'); 8 | addressUtils = await addressUtilsContract.deploy(); 9 | [ owner ] = await ethers.getSigners(); 10 | await addressUtils.deployed(); 11 | }); 12 | 13 | it('correctly checks account', async function() { 14 | expect(await addressUtils.isContract(owner.address)).to.equal(false); 15 | }); 16 | 17 | it('correctly checks smart contract', async function() { 18 | const contract = await ethers.getContractFactory('NFTokenTestMock'); 19 | const nfToken = await contract.deploy(); 20 | await nfToken.deployed(); 21 | expect(await addressUtils.isContract(nfToken.address)).to.equal(true); 22 | }); 23 | }); 24 | --------------------------------------------------------------------------------