├── .eslintrc ├── .github └── workflows │ ├── coverage.yml │ ├── deploy_npm.yml │ ├── run_tests.yml │ └── upgradeable.yml ├── .gitignore ├── .prettierrc ├── CODEOWNERS ├── LICENSE.txt ├── README.md ├── contracts ├── ERC721A.sol ├── IERC721A.sol ├── extensions │ ├── ERC4907A.sol │ ├── ERC721ABatchBurnable.sol │ ├── ERC721ABatchTransferable.sol │ ├── ERC721ABurnable.sol │ ├── ERC721AQueryable.sol │ ├── IERC4907A.sol │ ├── IERC721ABatchBurnable.sol │ ├── IERC721ABatchTransferable.sol │ ├── IERC721ABurnable.sol │ └── IERC721AQueryable.sol ├── interfaces │ ├── IERC4907A.sol │ ├── IERC721A.sol │ ├── IERC721ABatchBurnable.sol │ ├── IERC721ABatchTransferable.sol │ ├── IERC721ABurnable.sol │ └── IERC721AQueryable.sol └── mocks │ ├── DirectBurnBitSetterHelper.sol │ ├── ERC4907AMock.sol │ ├── ERC721ABatchBurnableMock.sol │ ├── ERC721ABatchTransferableMock.sol │ ├── ERC721ABurnableMock.sol │ ├── ERC721ABurnableStartTokenIdMock.sol │ ├── ERC721AGasReporterMock.sol │ ├── ERC721AMock.sol │ ├── ERC721AQueryableMock.sol │ ├── ERC721AQueryableStartTokenIdMock.sol │ ├── ERC721ASpotMock.sol │ ├── ERC721AStartTokenIdMock.sol │ ├── ERC721ATransferCounterMock.sol │ ├── ERC721AWithERC2309Mock.sol │ ├── ERC721ReceiverMock.sol │ ├── SequentialUpToHelper.sol │ └── StartTokenIdHelper.sol ├── docs ├── .nojekyll ├── assets │ ├── css │ │ └── prism-theme.css │ ├── fontello │ │ ├── LICENSE.txt │ │ ├── README.txt │ │ ├── css │ │ │ └── fontello.css │ │ └── font │ │ │ ├── fontello.eot │ │ │ ├── fontello.svg │ │ │ ├── fontello.ttf │ │ │ ├── fontello.woff │ │ │ └── fontello.woff2 │ ├── img │ │ ├── favicon.png │ │ └── preview.png │ └── logo │ │ ├── stylesheet.css │ │ ├── subset-StyreneB-Bold.woff │ │ └── subset-StyreneB-Bold.woff2 ├── design.md ├── erc4907a.md ├── erc721a-burnable.md ├── erc721a-queryable.md ├── erc721a.md ├── index.html ├── interfaces.md ├── migration.md ├── overview.md ├── sidebar.md ├── tips.md └── upgradeable.md ├── hardhat.config.js ├── package-lock.json ├── package.json ├── scripts └── release │ └── update-version.js └── test ├── ERC721A.test.js ├── GasUsage.test.js ├── extensions ├── ERC4907A.test.js ├── ERC721ABatchBurnable.test.js ├── ERC721ABatchTransferable.test.js ├── ERC721ABurnable.test.js ├── ERC721AQueryable.test.js ├── ERC721ASpot.test.js └── ERC721ATransferCounter.test.js └── helpers.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "commonjs": true, 5 | "es2021": true, 6 | "mocha": true 7 | }, 8 | "extends": "eslint:recommended", 9 | "parserOptions": { 10 | "ecmaVersion": "latest" 11 | }, 12 | "globals": { 13 | "ethers": "readonly" 14 | }, 15 | "rules": { 16 | "max-len": [ 17 | "error", 18 | { 19 | "code": 120 20 | } 21 | ] 22 | } 23 | } -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: Code Coverage 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths-ignore: 8 | - 'docs/**' 9 | - '**.md' 10 | 11 | jobs: 12 | run-tests: 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | matrix: 17 | node-version: [14.x] 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v2 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | - name: Install dependencies 26 | run: npm ci 27 | - name: Run Coverage and Upload to CodeCov 28 | run: | 29 | npm run coverage; \ 30 | curl -Os https://uploader.codecov.io/latest/linux/codecov; \ 31 | chmod +x codecov; \ 32 | ./codecov; 33 | -------------------------------------------------------------------------------- /.github/workflows/deploy_npm.yml: -------------------------------------------------------------------------------- 1 | name: Publish Package to npmjs 2 | on: 3 | release: 4 | types: [published] 5 | jobs: 6 | build: 7 | environment: production 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v2 12 | # Setup .npmrc file to publish to npm 13 | - uses: actions/setup-node@v2 14 | with: 15 | node-version: "16.x" 16 | registry-url: "https://registry.npmjs.org" 17 | - run: npm ci 18 | - run: npm publish 19 | env: 20 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 21 | -------------------------------------------------------------------------------- /.github/workflows/run_tests.yml: -------------------------------------------------------------------------------- 1 | name: ERC721A CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths-ignore: 7 | - 'docs/**' 8 | - '**.md' 9 | pull_request: 10 | branches: [main] 11 | paths-ignore: 12 | - 'docs/**' 13 | - '**.md' 14 | 15 | jobs: 16 | run-tests: 17 | runs-on: ubuntu-latest 18 | 19 | strategy: 20 | matrix: 21 | node-version: [14.x,16.x,17.x] 22 | 23 | steps: 24 | - uses: actions/checkout@v2 25 | - name: Use Node.js ${{ matrix.node-version }} 26 | uses: actions/setup-node@v2 27 | with: 28 | node-version: ${{ matrix.node-version }} 29 | - name: Install dependencies 30 | run: npm ci 31 | - name: Run linter 32 | run: npm run lint 33 | - name: Run tests 34 | run: npm test 35 | -------------------------------------------------------------------------------- /.github/workflows/upgradeable.yml: -------------------------------------------------------------------------------- 1 | name: Upgradeable Trigger 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - 'v*' 9 | paths-ignore: 10 | - 'docs/**' 11 | - '**.md' 12 | 13 | jobs: 14 | trigger: 15 | environment: production 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Repository Dispatch 19 | uses: peter-evans/repository-dispatch@v2 20 | with: 21 | token: ${{ secrets.UPGRADEABLE_REPO_ACCESS_TOKEN }} 22 | repository: chiru-labs/ERC721A-Upgradeable 23 | event-type: Update 24 | client-payload: '{"ref": "${{ github.ref }}", "sha": "${{ github.sha }}"}' 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .env 3 | coverage 4 | coverage.json 5 | typechain 6 | 7 | #Hardhat files 8 | cache 9 | artifacts 10 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "printWidth": 120, 4 | "overrides": [ 5 | { 6 | "files": "*.sol", 7 | "options": { 8 | "printWidth": 120, 9 | "explicitTypes": "always" 10 | } 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | .github/workflows/ @chiru-labs 2 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Chiru Labs 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 all 13 | 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 THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Docs][docs-shield]][docs-url] 2 | [![NPM][npm-shield]][npm-url] 3 | [![CI][ci-shield]][ci-url] 4 | [![Issues][issues-shield]][issues-url] 5 | [![MIT License][license-shield]][license-url] 6 | [![Coverage][coverage-shield]][coverage-url] 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | > **📢 Version 4.x introduces several breaking changes. [Please refer to the documentation for more details.](https://chiru-labs.github.io/ERC721A/#/migration)** 15 | 16 | _We highly recommend reading the migration guide_, **especially** _the part on [`supportsInterface`](https://chiru-labs.github.io/ERC721A/#/migration?id=supportsinterface) if you are using with OpenZeppelin extensions_ (e.g. ERC2981). 17 | 18 | 19 | 20 | ## About The Project 21 | 22 | The goal of ERC721A is to provide a fully compliant implementation of IERC721 with significant gas savings for minting multiple NFTs in a single transaction. This project and implementation will be updated regularly and will continue to stay up to date with best practices. 23 | 24 | The [Azuki](https://twitter.com/Azuki) team created ERC721A for its sale on 1/12/22. There was significant demand for 8700 tokens made available to the public, and all were minted within minutes. The network BASEFEE remained low despite huge demand, resulting in low gas costs for minters, while minimizing network disruption for the wider ecosystem as well. 25 | 26 | ![Gas Savings](https://pbs.twimg.com/media/FIdILKpVQAEQ_5U?format=jpg&name=medium) 27 | 28 | For more information on how ERC721A works under the hood, please visit our [blog](https://www.azuki.com/erc721a). To find other projects that are using ERC721A, please visit [erc721a.org](https://www.erc721a.org) and our [curated projects list](https://github.com/chiru-labs/ERC721A/blob/main/projects.md). 29 | 30 | **Chiru Labs is not liable for any outcomes as a result of using ERC721A.** DYOR. 31 | 32 | 33 | 34 | ## Docs 35 | 36 | https://chiru-labs.github.io/ERC721A/ 37 | 38 | 39 | 40 | ## Upgradeable Version 41 | 42 | https://github.com/chiru-labs/ERC721A-Upgradeable 43 | 44 | 45 | 46 | ## Installation 47 | 48 | ```sh 49 | 50 | npm install --save-dev erc721a 51 | 52 | ``` 53 | 54 | 55 | 56 | ## Usage 57 | 58 | Once installed, you can use the contracts in the library by importing them: 59 | 60 | ```solidity 61 | pragma solidity ^0.8.4; 62 | 63 | import "erc721a/contracts/ERC721A.sol"; 64 | 65 | contract Azuki is ERC721A { 66 | constructor() ERC721A("Azuki", "AZUKI") {} 67 | 68 | function mint(uint256 quantity) external payable { 69 | // `_mint`'s second argument now takes in a `quantity`, not a `tokenId`. 70 | _mint(msg.sender, quantity); 71 | } 72 | } 73 | 74 | ``` 75 | 76 | 77 | 78 | ## Roadmap 79 | 80 | - [ ] Improve general repo and code quality (workflows, comments, etc.) 81 | - [ ] Add more documentation on benefits of using ERC721A 82 | - [ ] Maintain full test coverage 83 | 84 | See the [open issues](https://github.com/chiru-labs/ERC721A/issues) for a full list of proposed features (and known issues). 85 | 86 | 87 | 88 | ## Contributing 89 | 90 | Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. 91 | 92 | If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement". 93 | 94 | Don't forget to give the project a star! Thanks again! 95 | 96 | 1. Fork the Project 97 | 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) 98 | 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) 99 | 4. Push to the Branch (`git push origin feature/AmazingFeature`) 100 | 5. Open a Pull Request 101 | 102 | 103 | 104 | ### Running tests locally 105 | 106 | 1. `npm install` 107 | 2. `npm run test` 108 | 109 | 110 | 111 | ## License 112 | 113 | Distributed under the MIT License. See `LICENSE.txt` for more information. 114 | 115 | 116 | 117 | ## Contact 118 | 119 | - 2pm.flow (owner) - [@2pmflow](https://twitter.com/2pmflow) 120 | - location tba (owner) - [@locationtba](https://twitter.com/locationtba) 121 | - cygaar (maintainer) - [@0xCygaar](https://twitter.com/0xCygaar) 122 | - vectorized.eth (maintainer) - [@optimizoor](https://twitter.com/optimizoor) 123 | 124 | Project Link: [https://github.com/chiru-labs/ERC721A](https://github.com/chiru-labs/ERC721A) 125 | 126 | 127 | 128 | 129 | 130 | [docs-shield]: https://img.shields.io/badge/docs-%F0%9F%93%84-blue?style=for-the-badge 131 | [docs-url]: https://chiru-labs.github.io/ERC721A/ 132 | [npm-shield]: https://img.shields.io/npm/v/erc721a.svg?style=for-the-badge 133 | [npm-url]: https://www.npmjs.com/package/erc721a 134 | [ci-shield]: https://img.shields.io/github/actions/workflow/status/chiru-labs/ERC721A/run_tests.yml?label=build&style=for-the-badge&branch=main 135 | [ci-url]: https://github.com/chiru-labs/ERC721A/actions/workflows/run_tests.yml 136 | [contributors-shield]: https://img.shields.io/github/contributors/chiru-labs/ERC721A.svg?style=for-the-badge 137 | [contributors-url]: https://github.com/chiru-labs/ERC721A/graphs/contributors 138 | [forks-shield]: https://img.shields.io/github/forks/chiru-labs/ERC721A.svg?style=for-the-badge 139 | [forks-url]: https://github.com/chiru-labs/ERC721A/network/members 140 | [stars-shield]: https://img.shields.io/github/stars/chiru-labs/ERC721A.svg?style=for-the-badge 141 | [stars-url]: https://github.com/chiru-labs/ERC721A/stargazers 142 | [issues-shield]: https://img.shields.io/github/issues/chiru-labs/ERC721A.svg?style=for-the-badge 143 | [issues-url]: https://github.com/chiru-labs/ERC721A/issues 144 | [license-shield]: https://img.shields.io/badge/License-MIT-green.svg?style=for-the-badge 145 | [license-url]: https://github.com/chiru-labs/ERC721A/blob/main/LICENSE.txt 146 | [coverage-shield]: https://img.shields.io/codecov/c/gh/chiru-labs/ERC721A?style=for-the-badge 147 | [coverage-url]: https://codecov.io/gh/chiru-labs/ERC721A 148 | [product-screenshot]: images/screenshot.png 149 | -------------------------------------------------------------------------------- /contracts/IERC721A.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // ERC721A Contracts v4.3.0 3 | // Creator: Chiru Labs 4 | 5 | pragma solidity ^0.8.4; 6 | 7 | /** 8 | * @dev Interface of ERC721A. 9 | */ 10 | interface IERC721A { 11 | /** 12 | * The caller must own the token or be an approved operator. 13 | */ 14 | error ApprovalCallerNotOwnerNorApproved(); 15 | 16 | /** 17 | * The token does not exist. 18 | */ 19 | error ApprovalQueryForNonexistentToken(); 20 | 21 | /** 22 | * Cannot query the balance for the zero address. 23 | */ 24 | error BalanceQueryForZeroAddress(); 25 | 26 | /** 27 | * Cannot mint to the zero address. 28 | */ 29 | error MintToZeroAddress(); 30 | 31 | /** 32 | * The quantity of tokens minted must be more than zero. 33 | */ 34 | error MintZeroQuantity(); 35 | 36 | /** 37 | * The token does not exist. 38 | */ 39 | error OwnerQueryForNonexistentToken(); 40 | 41 | /** 42 | * The caller must own the token or be an approved operator. 43 | */ 44 | error TransferCallerNotOwnerNorApproved(); 45 | 46 | /** 47 | * The token must be owned by `from`. 48 | */ 49 | error TransferFromIncorrectOwner(); 50 | 51 | /** 52 | * Cannot safely transfer to a contract that does not implement the 53 | * ERC721Receiver interface. 54 | */ 55 | error TransferToNonERC721ReceiverImplementer(); 56 | 57 | /** 58 | * Cannot transfer to the zero address. 59 | */ 60 | error TransferToZeroAddress(); 61 | 62 | /** 63 | * The token does not exist. 64 | */ 65 | error URIQueryForNonexistentToken(); 66 | 67 | /** 68 | * The `quantity` minted with ERC2309 exceeds the safety limit. 69 | */ 70 | error MintERC2309QuantityExceedsLimit(); 71 | 72 | /** 73 | * The `extraData` cannot be set on an unintialized ownership slot. 74 | */ 75 | error OwnershipNotInitializedForExtraData(); 76 | 77 | /** 78 | * The `tokenIds` must be strictly ascending. 79 | */ 80 | error TokenIdsNotStrictlyAscending(); 81 | 82 | /** 83 | * `_sequentialUpTo()` must be greater than `_startTokenId()`. 84 | */ 85 | error SequentialUpToTooSmall(); 86 | 87 | /** 88 | * The `tokenId` of a sequential mint exceeds `_sequentialUpTo()`. 89 | */ 90 | error SequentialMintExceedsLimit(); 91 | 92 | /** 93 | * Spot minting requires a `tokenId` greater than `_sequentialUpTo()`. 94 | */ 95 | error SpotMintTokenIdTooSmall(); 96 | 97 | /** 98 | * Cannot mint over a token that already exists. 99 | */ 100 | error TokenAlreadyExists(); 101 | 102 | /** 103 | * The feature is not compatible with spot mints. 104 | */ 105 | error NotCompatibleWithSpotMints(); 106 | 107 | // ============================================================= 108 | // STRUCTS 109 | // ============================================================= 110 | 111 | struct TokenOwnership { 112 | // The address of the owner. 113 | address addr; 114 | // Stores the start time of ownership with minimal overhead for tokenomics. 115 | uint64 startTimestamp; 116 | // Whether the token has been burned. 117 | bool burned; 118 | // Arbitrary data similar to `startTimestamp` that can be set via {_extraData}. 119 | uint24 extraData; 120 | } 121 | 122 | // ============================================================= 123 | // TOKEN COUNTERS 124 | // ============================================================= 125 | 126 | /** 127 | * @dev Returns the total number of tokens in existence. 128 | * Burned tokens will reduce the count. 129 | * To get the total number of tokens minted, please see {_totalMinted}. 130 | */ 131 | function totalSupply() external view returns (uint256); 132 | 133 | // ============================================================= 134 | // IERC165 135 | // ============================================================= 136 | 137 | /** 138 | * @dev Returns true if this contract implements the interface defined by 139 | * `interfaceId`. See the corresponding 140 | * [EIP section](https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified) 141 | * to learn more about how these ids are created. 142 | * 143 | * This function call must use less than 30000 gas. 144 | */ 145 | function supportsInterface(bytes4 interfaceId) external view returns (bool); 146 | 147 | // ============================================================= 148 | // IERC721 149 | // ============================================================= 150 | 151 | /** 152 | * @dev Emitted when `tokenId` token is transferred from `from` to `to`. 153 | */ 154 | event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); 155 | 156 | /** 157 | * @dev Emitted when `owner` enables `approved` to manage the `tokenId` token. 158 | */ 159 | event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId); 160 | 161 | /** 162 | * @dev Emitted when `owner` enables or disables 163 | * (`approved`) `operator` to manage all of its assets. 164 | */ 165 | event ApprovalForAll(address indexed owner, address indexed operator, bool approved); 166 | 167 | /** 168 | * @dev Returns the number of tokens in `owner`'s account. 169 | */ 170 | function balanceOf(address owner) external view returns (uint256 balance); 171 | 172 | /** 173 | * @dev Returns the owner of the `tokenId` token. 174 | * 175 | * Requirements: 176 | * 177 | * - `tokenId` must exist. 178 | */ 179 | function ownerOf(uint256 tokenId) external view returns (address owner); 180 | 181 | /** 182 | * @dev Safely transfers `tokenId` token from `from` to `to`, 183 | * checking first that contract recipients are aware of the ERC721 protocol 184 | * to prevent tokens from being forever locked. 185 | * 186 | * Requirements: 187 | * 188 | * - `from` cannot be the zero address. 189 | * - `to` cannot be the zero address. 190 | * - `tokenId` token must exist and be owned by `from`. 191 | * - If the caller is not `from`, it must be have been allowed to move 192 | * this token by either {approve} or {setApprovalForAll}. 193 | * - If `to` refers to a smart contract, it must implement 194 | * {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. 195 | * 196 | * Emits a {Transfer} event. 197 | */ 198 | function safeTransferFrom( 199 | address from, 200 | address to, 201 | uint256 tokenId, 202 | bytes calldata data 203 | ) external payable; 204 | 205 | /** 206 | * @dev Equivalent to `safeTransferFrom(from, to, tokenId, '')`. 207 | */ 208 | function safeTransferFrom( 209 | address from, 210 | address to, 211 | uint256 tokenId 212 | ) external payable; 213 | 214 | /** 215 | * @dev Transfers `tokenId` from `from` to `to`. 216 | * 217 | * WARNING: Usage of this method is discouraged, use {safeTransferFrom} 218 | * whenever possible. 219 | * 220 | * Requirements: 221 | * 222 | * - `from` cannot be the zero address. 223 | * - `to` cannot be the zero address. 224 | * - `tokenId` token must be owned by `from`. 225 | * - If the caller is not `from`, it must be approved to move this token 226 | * by either {approve} or {setApprovalForAll}. 227 | * 228 | * Emits a {Transfer} event. 229 | */ 230 | function transferFrom( 231 | address from, 232 | address to, 233 | uint256 tokenId 234 | ) external payable; 235 | 236 | /** 237 | * @dev Gives permission to `to` to transfer `tokenId` token to another account. 238 | * The approval is cleared when the token is transferred. 239 | * 240 | * Only a single account can be approved at a time, so approving the 241 | * zero address clears previous approvals. 242 | * 243 | * Requirements: 244 | * 245 | * - The caller must own the token or be an approved operator. 246 | * - `tokenId` must exist. 247 | * 248 | * Emits an {Approval} event. 249 | */ 250 | function approve(address to, uint256 tokenId) external payable; 251 | 252 | /** 253 | * @dev Approve or remove `operator` as an operator for the caller. 254 | * Operators can call {transferFrom} or {safeTransferFrom} 255 | * for any token owned by the caller. 256 | * 257 | * Requirements: 258 | * 259 | * - The `operator` cannot be the caller. 260 | * 261 | * Emits an {ApprovalForAll} event. 262 | */ 263 | function setApprovalForAll(address operator, bool _approved) external; 264 | 265 | /** 266 | * @dev Returns the account approved for `tokenId` token. 267 | * 268 | * Requirements: 269 | * 270 | * - `tokenId` must exist. 271 | */ 272 | function getApproved(uint256 tokenId) external view returns (address operator); 273 | 274 | /** 275 | * @dev Returns if the `operator` is allowed to manage all of the assets of `owner`. 276 | * 277 | * See {setApprovalForAll}. 278 | */ 279 | function isApprovedForAll(address owner, address operator) external view returns (bool); 280 | 281 | // ============================================================= 282 | // IERC721Metadata 283 | // ============================================================= 284 | 285 | /** 286 | * @dev Returns the token collection name. 287 | */ 288 | function name() external view returns (string memory); 289 | 290 | /** 291 | * @dev Returns the token collection symbol. 292 | */ 293 | function symbol() external view returns (string memory); 294 | 295 | /** 296 | * @dev Returns the Uniform Resource Identifier (URI) for `tokenId` token. 297 | */ 298 | function tokenURI(uint256 tokenId) external view returns (string memory); 299 | 300 | // ============================================================= 301 | // IERC2309 302 | // ============================================================= 303 | 304 | /** 305 | * @dev Emitted when tokens in `fromTokenId` to `toTokenId` 306 | * (inclusive) is transferred from `from` to `to`, as defined in the 307 | * [ERC2309](https://eips.ethereum.org/EIPS/eip-2309) standard. 308 | * 309 | * See {_mintERC2309} for more details. 310 | */ 311 | event ConsecutiveTransfer(uint256 indexed fromTokenId, uint256 toTokenId, address indexed from, address indexed to); 312 | } 313 | -------------------------------------------------------------------------------- /contracts/extensions/ERC4907A.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // ERC721A Contracts v4.3.0 3 | // Creator: Chiru Labs 4 | 5 | pragma solidity ^0.8.4; 6 | 7 | import './IERC4907A.sol'; 8 | import '../ERC721A.sol'; 9 | 10 | /** 11 | * @title ERC4907A 12 | * 13 | * @dev [ERC4907](https://eips.ethereum.org/EIPS/eip-4907) compliant 14 | * extension of ERC721A, which allows owners and authorized addresses 15 | * to add a time-limited role with restricted permissions to ERC721 tokens. 16 | */ 17 | abstract contract ERC4907A is ERC721A, IERC4907A { 18 | // The bit position of `expires` in packed user info. 19 | uint256 private constant _BITPOS_EXPIRES = 160; 20 | 21 | // Mapping from token ID to user info. 22 | // 23 | // Bits Layout: 24 | // - [0..159] `user` 25 | // - [160..223] `expires` 26 | mapping(uint256 => uint256) private _packedUserInfo; 27 | 28 | /** 29 | * @dev Sets the `user` and `expires` for `tokenId`. 30 | * The zero address indicates there is no user. 31 | * 32 | * Requirements: 33 | * 34 | * - The caller must own `tokenId` or be an approved operator. 35 | */ 36 | function setUser( 37 | uint256 tokenId, 38 | address user, 39 | uint64 expires 40 | ) public virtual override { 41 | // Require the caller to be either the token owner or an approved operator. 42 | address owner = ownerOf(tokenId); 43 | if (_msgSenderERC721A() != owner) 44 | if (!isApprovedForAll(owner, _msgSenderERC721A())) 45 | if (getApproved(tokenId) != _msgSenderERC721A()) _revert(SetUserCallerNotOwnerNorApproved.selector); 46 | 47 | _packedUserInfo[tokenId] = (uint256(expires) << _BITPOS_EXPIRES) | uint256(uint160(user)); 48 | 49 | emit UpdateUser(tokenId, user, expires); 50 | } 51 | 52 | /** 53 | * @dev Returns the user address for `tokenId`. 54 | * The zero address indicates that there is no user or if the user is expired. 55 | */ 56 | function userOf(uint256 tokenId) public view virtual override returns (address) { 57 | uint256 packed = _packedUserInfo[tokenId]; 58 | assembly { 59 | // Branchless `packed *= (block.timestamp <= expires ? 1 : 0)`. 60 | // If the `block.timestamp == expires`, the `lt` clause will be true 61 | // if there is a non-zero user address in the lower 160 bits of `packed`. 62 | packed := mul( 63 | packed, 64 | // `block.timestamp <= expires ? 1 : 0`. 65 | lt(shl(_BITPOS_EXPIRES, timestamp()), packed) 66 | ) 67 | } 68 | return address(uint160(packed)); 69 | } 70 | 71 | /** 72 | * @dev Returns the user's expires of `tokenId`. 73 | */ 74 | function userExpires(uint256 tokenId) public view virtual override returns (uint256) { 75 | return _packedUserInfo[tokenId] >> _BITPOS_EXPIRES; 76 | } 77 | 78 | /** 79 | * @dev Override of {IERC165-supportsInterface}. 80 | */ 81 | function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721A, IERC721A) returns (bool) { 82 | // The interface ID for ERC4907 is `0xad092b5c`, 83 | // as defined in [ERC4907](https://eips.ethereum.org/EIPS/eip-4907). 84 | return super.supportsInterface(interfaceId) || interfaceId == 0xad092b5c; 85 | } 86 | 87 | /** 88 | * @dev Returns the user address for `tokenId`, ignoring the expiry status. 89 | */ 90 | function _explicitUserOf(uint256 tokenId) internal view virtual returns (address) { 91 | return address(uint160(_packedUserInfo[tokenId])); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /contracts/extensions/ERC721ABatchBurnable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // ERC721A Contracts v4.2.3 3 | // Creator: Chiru Labs 4 | 5 | pragma solidity ^0.8.4; 6 | 7 | import './ERC721ABurnable.sol'; 8 | import './IERC721ABatchBurnable.sol'; 9 | 10 | /** 11 | * @title ERC721ABatchBurnable. 12 | * 13 | * @dev ERC721A token optimized for batch burns. 14 | */ 15 | abstract contract ERC721ABatchBurnable is ERC721ABurnable, IERC721ABatchBurnable { 16 | function batchBurn(uint256[] memory tokenIds) public virtual override { 17 | _batchBurn(_msgSenderERC721A(), tokenIds); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /contracts/extensions/ERC721ABatchTransferable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // ERC721A Contracts v4.2.3 3 | // Creator: Chiru Labs 4 | 5 | pragma solidity ^0.8.4; 6 | 7 | import '../ERC721A.sol'; 8 | import './IERC721ABatchTransferable.sol'; 9 | 10 | /** 11 | * @title ERC721ABatchTransferable. 12 | * 13 | * @dev ERC721A token optimized for batch transfers. 14 | */ 15 | abstract contract ERC721ABatchTransferable is ERC721A, IERC721ABatchTransferable { 16 | function batchTransferFrom( 17 | address from, 18 | address to, 19 | uint256[] memory tokenIds 20 | ) public payable virtual override { 21 | _batchTransferFrom(_msgSenderERC721A(), from, to, tokenIds); 22 | } 23 | 24 | function safeBatchTransferFrom( 25 | address from, 26 | address to, 27 | uint256[] memory tokenIds 28 | ) public payable virtual override { 29 | _safeBatchTransferFrom(_msgSenderERC721A(), from, to, tokenIds, ''); 30 | } 31 | 32 | function safeBatchTransferFrom( 33 | address from, 34 | address to, 35 | uint256[] memory tokenIds, 36 | bytes memory _data 37 | ) public payable virtual override { 38 | _safeBatchTransferFrom(_msgSenderERC721A(), from, to, tokenIds, _data); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /contracts/extensions/ERC721ABurnable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // ERC721A Contracts v4.3.0 3 | // Creator: Chiru Labs 4 | 5 | pragma solidity ^0.8.4; 6 | 7 | import './IERC721ABurnable.sol'; 8 | import '../ERC721A.sol'; 9 | 10 | /** 11 | * @title ERC721ABurnable. 12 | * 13 | * @dev ERC721A token that can be irreversibly burned (destroyed). 14 | */ 15 | abstract contract ERC721ABurnable is ERC721A, IERC721ABurnable { 16 | /** 17 | * @dev Burns `tokenId`. See {ERC721A-_burn}. 18 | * 19 | * Requirements: 20 | * 21 | * - The caller must own `tokenId` or be an approved operator. 22 | */ 23 | function burn(uint256 tokenId) public virtual override { 24 | _burn(tokenId, true); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /contracts/extensions/ERC721AQueryable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // ERC721A Contracts v4.3.0 3 | // Creator: Chiru Labs 4 | 5 | pragma solidity ^0.8.4; 6 | 7 | import './IERC721AQueryable.sol'; 8 | import '../ERC721A.sol'; 9 | 10 | /** 11 | * @title ERC721AQueryable. 12 | * 13 | * @dev ERC721A subclass with convenience query functions. 14 | */ 15 | abstract contract ERC721AQueryable is ERC721A, IERC721AQueryable { 16 | /** 17 | * @dev Returns the `TokenOwnership` struct at `tokenId` without reverting. 18 | * 19 | * If the `tokenId` is out of bounds: 20 | * 21 | * - `addr = address(0)` 22 | * - `startTimestamp = 0` 23 | * - `burned = false` 24 | * - `extraData = 0` 25 | * 26 | * If the `tokenId` is burned: 27 | * 28 | * - `addr =
` 29 | * - `startTimestamp = ` 30 | * - `burned = true` 31 | * - `extraData = ` 32 | * 33 | * Otherwise: 34 | * 35 | * - `addr =
` 36 | * - `startTimestamp = ` 37 | * - `burned = false` 38 | * - `extraData = ` 39 | */ 40 | function explicitOwnershipOf(uint256 tokenId) 41 | public 42 | view 43 | virtual 44 | override 45 | returns (TokenOwnership memory ownership) 46 | { 47 | unchecked { 48 | if (tokenId >= _startTokenId()) { 49 | if (tokenId > _sequentialUpTo()) return _ownershipAt(tokenId); 50 | 51 | if (tokenId < _nextTokenId()) { 52 | // If the `tokenId` is within bounds, 53 | // scan backwards for the initialized ownership slot. 54 | while (!_ownershipIsInitialized(tokenId)) --tokenId; 55 | return _ownershipAt(tokenId); 56 | } 57 | } 58 | } 59 | } 60 | 61 | /** 62 | * @dev Returns an array of `TokenOwnership` structs at `tokenIds` in order. 63 | * See {ERC721AQueryable-explicitOwnershipOf} 64 | */ 65 | function explicitOwnershipsOf(uint256[] calldata tokenIds) 66 | external 67 | view 68 | virtual 69 | override 70 | returns (TokenOwnership[] memory) 71 | { 72 | TokenOwnership[] memory ownerships; 73 | uint256 i = tokenIds.length; 74 | assembly { 75 | // Grab the free memory pointer. 76 | ownerships := mload(0x40) 77 | // Store the length. 78 | mstore(ownerships, i) 79 | // Allocate one word for the length, 80 | // `tokenIds.length` words for the pointers. 81 | i := shl(5, i) // Multiply `i` by 32. 82 | mstore(0x40, add(add(ownerships, 0x20), i)) 83 | } 84 | while (i != 0) { 85 | uint256 tokenId; 86 | assembly { 87 | i := sub(i, 0x20) 88 | tokenId := calldataload(add(tokenIds.offset, i)) 89 | } 90 | TokenOwnership memory ownership = explicitOwnershipOf(tokenId); 91 | assembly { 92 | // Store the pointer of `ownership` in the `ownerships` array. 93 | mstore(add(add(ownerships, 0x20), i), ownership) 94 | } 95 | } 96 | return ownerships; 97 | } 98 | 99 | /** 100 | * @dev Returns an array of token IDs owned by `owner`, 101 | * in the range [`start`, `stop`) 102 | * (i.e. `start <= tokenId < stop`). 103 | * 104 | * This function allows for tokens to be queried if the collection 105 | * grows too big for a single call of {ERC721AQueryable-tokensOfOwner}. 106 | * 107 | * Requirements: 108 | * 109 | * - `start < stop` 110 | */ 111 | function tokensOfOwnerIn( 112 | address owner, 113 | uint256 start, 114 | uint256 stop 115 | ) external view virtual override returns (uint256[] memory) { 116 | return _tokensOfOwnerIn(owner, start, stop); 117 | } 118 | 119 | /** 120 | * @dev Returns an array of token IDs owned by `owner`. 121 | * 122 | * This function scans the ownership mapping and is O(`totalSupply`) in complexity. 123 | * It is meant to be called off-chain. 124 | * 125 | * See {ERC721AQueryable-tokensOfOwnerIn} for splitting the scan into 126 | * multiple smaller scans if the collection is large enough to cause 127 | * an out-of-gas error (10K collections should be fine). 128 | */ 129 | function tokensOfOwner(address owner) external view virtual override returns (uint256[] memory) { 130 | // If spot mints are enabled, full-range scan is disabled. 131 | if (_sequentialUpTo() != type(uint256).max) _revert(NotCompatibleWithSpotMints.selector); 132 | uint256 start = _startTokenId(); 133 | uint256 stop = _nextTokenId(); 134 | uint256[] memory tokenIds; 135 | if (start != stop) tokenIds = _tokensOfOwnerIn(owner, start, stop); 136 | return tokenIds; 137 | } 138 | 139 | /** 140 | * @dev Helper function for returning an array of token IDs owned by `owner`. 141 | * 142 | * Note that this function is optimized for smaller bytecode size over runtime gas, 143 | * since it is meant to be called off-chain. 144 | */ 145 | function _tokensOfOwnerIn( 146 | address owner, 147 | uint256 start, 148 | uint256 stop 149 | ) private view returns (uint256[] memory tokenIds) { 150 | unchecked { 151 | if (start >= stop) _revert(InvalidQueryRange.selector); 152 | // Set `start = max(start, _startTokenId())`. 153 | if (start < _startTokenId()) start = _startTokenId(); 154 | uint256 nextTokenId = _nextTokenId(); 155 | // If spot mints are enabled, scan all the way until the specified `stop`. 156 | uint256 stopLimit = _sequentialUpTo() != type(uint256).max ? stop : nextTokenId; 157 | // Set `stop = min(stop, stopLimit)`. 158 | if (stop >= stopLimit) stop = stopLimit; 159 | // Number of tokens to scan. 160 | uint256 tokenIdsMaxLength = balanceOf(owner); 161 | // Set `tokenIdsMaxLength` to zero if the range contains no tokens. 162 | if (start >= stop) tokenIdsMaxLength = 0; 163 | // If there are one or more tokens to scan. 164 | if (tokenIdsMaxLength != 0) { 165 | // Set `tokenIdsMaxLength = min(balanceOf(owner), tokenIdsMaxLength)`. 166 | if (stop - start <= tokenIdsMaxLength) tokenIdsMaxLength = stop - start; 167 | uint256 m; // Start of available memory. 168 | assembly { 169 | // Grab the free memory pointer. 170 | tokenIds := mload(0x40) 171 | // Allocate one word for the length, and `tokenIdsMaxLength` words 172 | // for the data. `shl(5, x)` is equivalent to `mul(32, x)`. 173 | m := add(tokenIds, shl(5, add(tokenIdsMaxLength, 1))) 174 | mstore(0x40, m) 175 | } 176 | // We need to call `explicitOwnershipOf(start)`, 177 | // because the slot at `start` may not be initialized. 178 | TokenOwnership memory ownership = explicitOwnershipOf(start); 179 | address currOwnershipAddr; 180 | // If the starting slot exists (i.e. not burned), 181 | // initialize `currOwnershipAddr`. 182 | // `ownership.address` will not be zero, 183 | // as `start` is clamped to the valid token ID range. 184 | if (!ownership.burned) currOwnershipAddr = ownership.addr; 185 | uint256 tokenIdsIdx; 186 | // Use a do-while, which is slightly more efficient for this case, 187 | // as the array will at least contain one element. 188 | do { 189 | if (_sequentialUpTo() != type(uint256).max) { 190 | // Skip the remaining unused sequential slots. 191 | if (start == nextTokenId) start = _sequentialUpTo() + 1; 192 | // Reset `currOwnershipAddr`, as each spot-minted token is a batch of one. 193 | if (start > _sequentialUpTo()) currOwnershipAddr = address(0); 194 | } 195 | ownership = _ownershipAt(start); // This implicitly allocates memory. 196 | assembly { 197 | switch mload(add(ownership, 0x40)) 198 | // if `ownership.burned == false`. 199 | case 0 { 200 | // if `ownership.addr != address(0)`. 201 | // The `addr` already has it's upper 96 bits clearned, 202 | // since it is written to memory with regular Solidity. 203 | if mload(ownership) { 204 | currOwnershipAddr := mload(ownership) 205 | } 206 | // if `currOwnershipAddr == owner`. 207 | // The `shl(96, x)` is to make the comparison agnostic to any 208 | // dirty upper 96 bits in `owner`. 209 | if iszero(shl(96, xor(currOwnershipAddr, owner))) { 210 | tokenIdsIdx := add(tokenIdsIdx, 1) 211 | mstore(add(tokenIds, shl(5, tokenIdsIdx)), start) 212 | } 213 | } 214 | // Otherwise, reset `currOwnershipAddr`. 215 | // This handles the case of batch burned tokens 216 | // (burned bit of first slot set, remaining slots left uninitialized). 217 | default { 218 | currOwnershipAddr := 0 219 | } 220 | start := add(start, 1) 221 | // Free temporary memory implicitly allocated for ownership 222 | // to avoid quadratic memory expansion costs. 223 | mstore(0x40, m) 224 | } 225 | } while (!(start == stop || tokenIdsIdx == tokenIdsMaxLength)); 226 | // Store the length of the array. 227 | assembly { 228 | mstore(tokenIds, tokenIdsIdx) 229 | } 230 | } 231 | } 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /contracts/extensions/IERC4907A.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // ERC721A Contracts v4.3.0 3 | // Creator: Chiru Labs 4 | 5 | pragma solidity ^0.8.4; 6 | 7 | import '../IERC721A.sol'; 8 | 9 | /** 10 | * @dev Interface of ERC4907A. 11 | */ 12 | interface IERC4907A is IERC721A { 13 | /** 14 | * The caller must own the token or be an approved operator. 15 | */ 16 | error SetUserCallerNotOwnerNorApproved(); 17 | 18 | /** 19 | * @dev Emitted when the `user` of an NFT or the `expires` of the `user` is changed. 20 | * The zero address for user indicates that there is no user address. 21 | */ 22 | event UpdateUser(uint256 indexed tokenId, address indexed user, uint64 expires); 23 | 24 | /** 25 | * @dev Sets the `user` and `expires` for `tokenId`. 26 | * The zero address indicates there is no user. 27 | * 28 | * Requirements: 29 | * 30 | * - The caller must own `tokenId` or be an approved operator. 31 | */ 32 | function setUser( 33 | uint256 tokenId, 34 | address user, 35 | uint64 expires 36 | ) external; 37 | 38 | /** 39 | * @dev Returns the user address for `tokenId`. 40 | * The zero address indicates that there is no user or if the user is expired. 41 | */ 42 | function userOf(uint256 tokenId) external view returns (address); 43 | 44 | /** 45 | * @dev Returns the user's expires of `tokenId`. 46 | */ 47 | function userExpires(uint256 tokenId) external view returns (uint256); 48 | } 49 | -------------------------------------------------------------------------------- /contracts/extensions/IERC721ABatchBurnable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // ERC721A Contracts v4.2.3 3 | // Creator: Chiru Labs 4 | 5 | pragma solidity ^0.8.4; 6 | 7 | import './IERC721ABurnable.sol'; 8 | 9 | /** 10 | * @dev Interface of ERC721ABatchBurnable. 11 | */ 12 | interface IERC721ABatchBurnable is IERC721ABurnable { 13 | function batchBurn(uint256[] memory tokenIds) external; 14 | } 15 | -------------------------------------------------------------------------------- /contracts/extensions/IERC721ABatchTransferable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // ERC721A Contracts v4.2.3 3 | // Creator: Chiru Labs 4 | 5 | pragma solidity ^0.8.4; 6 | 7 | import '../IERC721A.sol'; 8 | 9 | /** 10 | * @dev Interface of ERC721ABatchTransferable. 11 | */ 12 | interface IERC721ABatchTransferable is IERC721A { 13 | /** 14 | * @dev Transfers `tokenIds` in batch from `from` to `to`. See {ERC721A-_batchTransferFrom}. 15 | * 16 | * Requirements: 17 | * 18 | * - `from` cannot be the zero address. 19 | * - `to` cannot be the zero address. 20 | * - `tokenIds` tokens must be owned by `from`. 21 | * - If the caller is not `from`, it must be approved to move these tokens 22 | * by either {approve} or {setApprovalForAll}. 23 | * 24 | * Emits a {Transfer} event for each transfer. 25 | */ 26 | function batchTransferFrom( 27 | address from, 28 | address to, 29 | uint256[] memory tokenIds 30 | ) external payable; 31 | 32 | /** 33 | * @dev Equivalent to `safeBatchTransferFrom(from, to, tokenIds, '')`. 34 | */ 35 | function safeBatchTransferFrom( 36 | address from, 37 | address to, 38 | uint256[] memory tokenIds 39 | ) external payable; 40 | 41 | /** 42 | * @dev Safely transfers `tokenIds` in batch from `from` to `to`. See {ERC721A-_safeBatchTransferFrom}. 43 | * 44 | * Requirements: 45 | * 46 | * - `from` cannot be the zero address. 47 | * - `to` cannot be the zero address. 48 | * - `tokenIds` tokens must be owned by `from`. 49 | * - If the caller is not `from`, it must be approved to move these tokens 50 | * by either {approve} or {setApprovalForAll}. 51 | * - If `to` refers to a smart contract, it must implement 52 | * {IERC721Receiver-onERC721Received}, which is called for each transferred token. 53 | * 54 | * Emits a {Transfer} event for each transfer. 55 | */ 56 | function safeBatchTransferFrom( 57 | address from, 58 | address to, 59 | uint256[] memory tokenIds, 60 | bytes memory _data 61 | ) external payable; 62 | } 63 | -------------------------------------------------------------------------------- /contracts/extensions/IERC721ABurnable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // ERC721A Contracts v4.3.0 3 | // Creator: Chiru Labs 4 | 5 | pragma solidity ^0.8.4; 6 | 7 | import '../IERC721A.sol'; 8 | 9 | /** 10 | * @dev Interface of ERC721ABurnable. 11 | */ 12 | interface IERC721ABurnable is IERC721A { 13 | /** 14 | * @dev Burns `tokenId`. See {ERC721A-_burn}. 15 | * 16 | * Requirements: 17 | * 18 | * - The caller must own `tokenId` or be an approved operator. 19 | */ 20 | function burn(uint256 tokenId) external; 21 | } 22 | -------------------------------------------------------------------------------- /contracts/extensions/IERC721AQueryable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // ERC721A Contracts v4.3.0 3 | // Creator: Chiru Labs 4 | 5 | pragma solidity ^0.8.4; 6 | 7 | import '../IERC721A.sol'; 8 | 9 | /** 10 | * @dev Interface of ERC721AQueryable. 11 | */ 12 | interface IERC721AQueryable is IERC721A { 13 | /** 14 | * Invalid query range (`start` >= `stop`). 15 | */ 16 | error InvalidQueryRange(); 17 | 18 | /** 19 | * @dev Returns the `TokenOwnership` struct at `tokenId` without reverting. 20 | * 21 | * If the `tokenId` is out of bounds: 22 | * 23 | * - `addr = address(0)` 24 | * - `startTimestamp = 0` 25 | * - `burned = false` 26 | * - `extraData = 0` 27 | * 28 | * If the `tokenId` is burned: 29 | * 30 | * - `addr =
` 31 | * - `startTimestamp = ` 32 | * - `burned = true` 33 | * - `extraData = ` 34 | * 35 | * Otherwise: 36 | * 37 | * - `addr =
` 38 | * - `startTimestamp = ` 39 | * - `burned = false` 40 | * - `extraData = ` 41 | */ 42 | function explicitOwnershipOf(uint256 tokenId) external view returns (TokenOwnership memory); 43 | 44 | /** 45 | * @dev Returns an array of `TokenOwnership` structs at `tokenIds` in order. 46 | * See {ERC721AQueryable-explicitOwnershipOf} 47 | */ 48 | function explicitOwnershipsOf(uint256[] memory tokenIds) external view returns (TokenOwnership[] memory); 49 | 50 | /** 51 | * @dev Returns an array of token IDs owned by `owner`, 52 | * in the range [`start`, `stop`) 53 | * (i.e. `start <= tokenId < stop`). 54 | * 55 | * This function allows for tokens to be queried if the collection 56 | * grows too big for a single call of {ERC721AQueryable-tokensOfOwner}. 57 | * 58 | * Requirements: 59 | * 60 | * - `start < stop` 61 | */ 62 | function tokensOfOwnerIn( 63 | address owner, 64 | uint256 start, 65 | uint256 stop 66 | ) external view returns (uint256[] memory); 67 | 68 | /** 69 | * @dev Returns an array of token IDs owned by `owner`. 70 | * 71 | * This function scans the ownership mapping and is O(`totalSupply`) in complexity. 72 | * It is meant to be called off-chain. 73 | * 74 | * See {ERC721AQueryable-tokensOfOwnerIn} for splitting the scan into 75 | * multiple smaller scans if the collection is large enough to cause 76 | * an out-of-gas error (10K collections should be fine). 77 | */ 78 | function tokensOfOwner(address owner) external view returns (uint256[] memory); 79 | } 80 | -------------------------------------------------------------------------------- /contracts/interfaces/IERC4907A.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // ERC721A Contracts v4.3.0 3 | // Creator: Chiru Labs 4 | 5 | pragma solidity ^0.8.4; 6 | 7 | import '../extensions/IERC4907A.sol'; 8 | -------------------------------------------------------------------------------- /contracts/interfaces/IERC721A.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // ERC721A Contracts v4.3.0 3 | // Creator: Chiru Labs 4 | 5 | pragma solidity ^0.8.4; 6 | 7 | import '../IERC721A.sol'; 8 | -------------------------------------------------------------------------------- /contracts/interfaces/IERC721ABatchBurnable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // ERC721A Contracts v4.2.3 3 | // Creator: Chiru Labs 4 | 5 | pragma solidity ^0.8.4; 6 | 7 | import '../extensions/IERC721ABatchBurnable.sol'; 8 | -------------------------------------------------------------------------------- /contracts/interfaces/IERC721ABatchTransferable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // ERC721A Contracts v4.2.3 3 | // Creator: Chiru Labs 4 | 5 | pragma solidity ^0.8.4; 6 | 7 | import '../extensions/IERC721ABatchTransferable.sol'; 8 | -------------------------------------------------------------------------------- /contracts/interfaces/IERC721ABurnable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // ERC721A Contracts v4.3.0 3 | // Creator: Chiru Labs 4 | 5 | pragma solidity ^0.8.4; 6 | 7 | import '../extensions/IERC721ABurnable.sol'; 8 | -------------------------------------------------------------------------------- /contracts/interfaces/IERC721AQueryable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // ERC721A Contracts v4.3.0 3 | // Creator: Chiru Labs 4 | 5 | pragma solidity ^0.8.4; 6 | 7 | import '../extensions/IERC721AQueryable.sol'; 8 | -------------------------------------------------------------------------------- /contracts/mocks/DirectBurnBitSetterHelper.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // ERC721A Contracts v4.3.0 3 | // Creators: Chiru Labs 4 | 5 | pragma solidity ^0.8.4; 6 | 7 | contract DirectBurnBitSetterHelper { 8 | function directSetBurnBit(uint256 index) public virtual { 9 | bytes32 erc721aDiamondStorageSlot = keccak256('openzepplin.contracts.storage.ERC721A'); 10 | 11 | // This is `_BITMASK_BURNED` from ERC721A. 12 | uint256 bitmaskBurned = 1 << 224; 13 | // We use assembly to directly access the private mapping. 14 | assembly { 15 | // The `_packedOwnerships` mapping is at slot 4. 16 | mstore(0x20, 4) 17 | mstore(0x00, index) 18 | let ownershipStorageSlot := keccak256(0x00, 0x40) 19 | sstore(ownershipStorageSlot, or(sload(ownershipStorageSlot), bitmaskBurned)) 20 | 21 | // For diamond storage, we'll simply add the offset of the layout struct. 22 | mstore(0x20, add(erc721aDiamondStorageSlot, 4)) 23 | mstore(0x00, index) 24 | ownershipStorageSlot := keccak256(0x00, 0x40) 25 | sstore(ownershipStorageSlot, or(sload(ownershipStorageSlot), bitmaskBurned)) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /contracts/mocks/ERC4907AMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // ERC721A Contracts v4.3.0 3 | // Creators: Chiru Labs 4 | 5 | pragma solidity ^0.8.4; 6 | 7 | import '../extensions/ERC4907A.sol'; 8 | 9 | contract ERC4907AMock is ERC721A, ERC4907A { 10 | constructor(string memory name_, string memory symbol_) ERC721A(name_, symbol_) {} 11 | 12 | function mint(address to, uint256 quantity) public { 13 | _mint(to, quantity); 14 | } 15 | 16 | function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721A, ERC4907A) returns (bool) { 17 | return super.supportsInterface(interfaceId); 18 | } 19 | 20 | function explicitUserOf(uint256 tokenId) public view returns (address) { 21 | return _explicitUserOf(tokenId); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /contracts/mocks/ERC721ABatchBurnableMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // ERC721A Contracts v4.2.3 3 | // Creators: Chiru Labs 4 | 5 | pragma solidity ^0.8.4; 6 | 7 | import '../extensions/ERC721ABatchBurnable.sol'; 8 | import './DirectBurnBitSetterHelper.sol'; 9 | 10 | contract ERC721ABatchBurnableMock is ERC721ABatchBurnable, DirectBurnBitSetterHelper { 11 | constructor(string memory name_, string memory symbol_) ERC721A(name_, symbol_) {} 12 | 13 | function exists(uint256 tokenId) public view returns (bool) { 14 | return _exists(tokenId); 15 | } 16 | 17 | function safeMint(address to, uint256 quantity) public { 18 | _safeMint(to, quantity); 19 | } 20 | 21 | function getOwnershipAt(uint256 index) public view returns (TokenOwnership memory) { 22 | return _ownershipAt(index); 23 | } 24 | 25 | function totalMinted() public view returns (uint256) { 26 | return _totalMinted(); 27 | } 28 | 29 | function totalBurned() public view returns (uint256) { 30 | return _totalBurned(); 31 | } 32 | 33 | function numberBurned(address owner) public view returns (uint256) { 34 | return _numberBurned(owner); 35 | } 36 | 37 | function initializeOwnershipAt(uint256 index) public { 38 | _initializeOwnershipAt(index); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /contracts/mocks/ERC721ABatchTransferableMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // ERC721A Contracts v4.2.3 3 | // Creators: Chiru Labs 4 | 5 | pragma solidity ^0.8.4; 6 | 7 | import '../extensions/ERC721ABatchTransferable.sol'; 8 | 9 | contract ERC721ABatchTransferableMock is ERC721ABatchTransferable { 10 | constructor(string memory name_, string memory symbol_) ERC721A(name_, symbol_) {} 11 | 12 | function safeMint(address to, uint256 quantity) public { 13 | _safeMint(to, quantity); 14 | } 15 | 16 | function getOwnershipAt(uint256 index) public view returns (TokenOwnership memory) { 17 | return _ownershipAt(index); 18 | } 19 | 20 | function initializeOwnershipAt(uint256 index) public { 21 | _initializeOwnershipAt(index); 22 | } 23 | 24 | function _extraData( 25 | address, 26 | address, 27 | uint24 previousExtraData 28 | ) internal view virtual override returns (uint24) { 29 | return previousExtraData; 30 | } 31 | 32 | function setExtraDataAt(uint256 index, uint24 extraData) public { 33 | _setExtraDataAt(index, extraData); 34 | } 35 | 36 | function batchTransferFromUnoptimized( 37 | address from, 38 | address to, 39 | uint256[] memory tokenIds 40 | ) public { 41 | unchecked { 42 | for (uint256 i; i != tokenIds.length; ++i) { 43 | transferFrom(from, to, tokenIds[i]); 44 | } 45 | } 46 | } 47 | 48 | function directBatchTransferFrom( 49 | address by, 50 | address from, 51 | address to, 52 | uint256[] memory tokenIds 53 | ) public { 54 | _batchTransferFrom(by, from, to, tokenIds); 55 | } 56 | 57 | function directBatchTransferFrom( 58 | address from, 59 | address to, 60 | uint256[] memory tokenIds 61 | ) public { 62 | _batchTransferFrom(from, to, tokenIds); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /contracts/mocks/ERC721ABurnableMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // ERC721A Contracts v4.3.0 3 | // Creators: Chiru Labs 4 | 5 | pragma solidity ^0.8.4; 6 | 7 | import '../extensions/ERC721ABurnable.sol'; 8 | 9 | contract ERC721ABurnableMock is ERC721A, ERC721ABurnable { 10 | constructor(string memory name_, string memory symbol_) ERC721A(name_, symbol_) {} 11 | 12 | function exists(uint256 tokenId) public view returns (bool) { 13 | return _exists(tokenId); 14 | } 15 | 16 | function safeMint(address to, uint256 quantity) public { 17 | _safeMint(to, quantity); 18 | } 19 | 20 | function getOwnershipAt(uint256 index) public view returns (TokenOwnership memory) { 21 | return _ownershipAt(index); 22 | } 23 | 24 | function totalMinted() public view returns (uint256) { 25 | return _totalMinted(); 26 | } 27 | 28 | function totalBurned() public view returns (uint256) { 29 | return _totalBurned(); 30 | } 31 | 32 | function numberBurned(address owner) public view returns (uint256) { 33 | return _numberBurned(owner); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /contracts/mocks/ERC721ABurnableStartTokenIdMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // ERC721A Contracts v4.3.0 3 | // Creators: Chiru Labs 4 | 5 | pragma solidity ^0.8.4; 6 | 7 | import './ERC721ABurnableMock.sol'; 8 | import './StartTokenIdHelper.sol'; 9 | 10 | contract ERC721ABurnableStartTokenIdMock is StartTokenIdHelper, ERC721ABurnableMock { 11 | constructor( 12 | string memory name_, 13 | string memory symbol_, 14 | uint256 startTokenId_ 15 | ) StartTokenIdHelper(startTokenId_) ERC721ABurnableMock(name_, symbol_) {} 16 | 17 | function _startTokenId() internal view override returns (uint256) { 18 | return startTokenId(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /contracts/mocks/ERC721AGasReporterMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // ERC721A Contracts v4.3.0 3 | // Creators: Chiru Labs 4 | 5 | pragma solidity ^0.8.4; 6 | 7 | import '../ERC721A.sol'; 8 | 9 | contract ERC721AGasReporterMock is ERC721A { 10 | constructor(string memory name_, string memory symbol_) ERC721A(name_, symbol_) {} 11 | 12 | function safeMintOne(address to) public { 13 | _safeMint(to, 1); 14 | } 15 | 16 | function mintOne(address to) public { 17 | _mint(to, 1); 18 | } 19 | 20 | function safeMintTen(address to) public { 21 | _safeMint(to, 10); 22 | } 23 | 24 | function mintTen(address to) public { 25 | _mint(to, 10); 26 | } 27 | 28 | function safeMintHundred(address to) public { 29 | _safeMint(to, 100); 30 | } 31 | 32 | function mintHundred(address to) public { 33 | _mint(to, 100); 34 | } 35 | 36 | function transferTenAsc(address to) public { 37 | unchecked { 38 | transferFrom(msg.sender, to, 0); 39 | transferFrom(msg.sender, to, 1); 40 | transferFrom(msg.sender, to, 2); 41 | transferFrom(msg.sender, to, 3); 42 | transferFrom(msg.sender, to, 4); 43 | transferFrom(msg.sender, to, 5); 44 | transferFrom(msg.sender, to, 6); 45 | transferFrom(msg.sender, to, 7); 46 | transferFrom(msg.sender, to, 8); 47 | transferFrom(msg.sender, to, 9); 48 | } 49 | } 50 | 51 | function transferTenDesc(address to) public { 52 | unchecked { 53 | transferFrom(msg.sender, to, 9); 54 | transferFrom(msg.sender, to, 8); 55 | transferFrom(msg.sender, to, 7); 56 | transferFrom(msg.sender, to, 6); 57 | transferFrom(msg.sender, to, 5); 58 | transferFrom(msg.sender, to, 4); 59 | transferFrom(msg.sender, to, 3); 60 | transferFrom(msg.sender, to, 2); 61 | transferFrom(msg.sender, to, 1); 62 | transferFrom(msg.sender, to, 0); 63 | } 64 | } 65 | 66 | function transferTenAvg(address to) public { 67 | unchecked { 68 | transferFrom(msg.sender, to, 4); 69 | transferFrom(msg.sender, to, 5); 70 | transferFrom(msg.sender, to, 3); 71 | transferFrom(msg.sender, to, 6); 72 | transferFrom(msg.sender, to, 2); 73 | transferFrom(msg.sender, to, 7); 74 | transferFrom(msg.sender, to, 1); 75 | transferFrom(msg.sender, to, 8); 76 | transferFrom(msg.sender, to, 0); 77 | transferFrom(msg.sender, to, 9); 78 | } 79 | } 80 | 81 | function batchTransferHundredUnoptimized(address to) public { 82 | unchecked { 83 | for (uint256 i; i != 100; ++i) { 84 | transferFrom(msg.sender, to, i); 85 | } 86 | } 87 | } 88 | 89 | function batchTransferHundredOptimized(address to) public { 90 | unchecked { 91 | uint256[] memory tokenIds = new uint256[](100); 92 | for (uint256 i; i != 100; ++i) { 93 | tokenIds[i] = i; 94 | } 95 | _batchTransferFrom(msg.sender, msg.sender, to, tokenIds); 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /contracts/mocks/ERC721AMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // ERC721A Contracts v4.3.0 3 | // Creators: Chiru Labs 4 | 5 | pragma solidity ^0.8.4; 6 | 7 | import '../ERC721A.sol'; 8 | import './DirectBurnBitSetterHelper.sol'; 9 | 10 | contract ERC721AMock is ERC721A, DirectBurnBitSetterHelper { 11 | constructor(string memory name_, string memory symbol_) ERC721A(name_, symbol_) {} 12 | 13 | function numberMinted(address owner) public view returns (uint256) { 14 | return _numberMinted(owner); 15 | } 16 | 17 | function totalMinted() public view returns (uint256) { 18 | return _totalMinted(); 19 | } 20 | 21 | function totalBurned() public view returns (uint256) { 22 | return _totalBurned(); 23 | } 24 | 25 | function nextTokenId() public view returns (uint256) { 26 | return _nextTokenId(); 27 | } 28 | 29 | function getAux(address owner) public view returns (uint64) { 30 | return _getAux(owner); 31 | } 32 | 33 | function setAux(address owner, uint64 aux) public { 34 | _setAux(owner, aux); 35 | } 36 | 37 | function directApprove(address to, uint256 tokenId) public { 38 | _approve(to, tokenId); 39 | } 40 | 41 | function baseURI() public view returns (string memory) { 42 | return _baseURI(); 43 | } 44 | 45 | function exists(uint256 tokenId) public view returns (bool) { 46 | return _exists(tokenId); 47 | } 48 | 49 | function safeMint(address to, uint256 quantity) public { 50 | _safeMint(to, quantity); 51 | } 52 | 53 | function safeMint( 54 | address to, 55 | uint256 quantity, 56 | bytes memory _data 57 | ) public { 58 | _safeMint(to, quantity, _data); 59 | } 60 | 61 | function mint(address to, uint256 quantity) public { 62 | _mint(to, quantity); 63 | } 64 | 65 | function burn(uint256 tokenId) public { 66 | _burn(tokenId); 67 | } 68 | 69 | function burn(uint256 tokenId, bool approvalCheck) public { 70 | _burn(tokenId, approvalCheck); 71 | } 72 | 73 | function toString(uint256 x) public pure returns (string memory) { 74 | return _toString(x); 75 | } 76 | 77 | function getOwnershipAt(uint256 index) public view returns (TokenOwnership memory) { 78 | return _ownershipAt(index); 79 | } 80 | 81 | function getOwnershipOf(uint256 index) public view returns (TokenOwnership memory) { 82 | return _ownershipOf(index); 83 | } 84 | 85 | function initializeOwnershipAt(uint256 index) public { 86 | _initializeOwnershipAt(index); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /contracts/mocks/ERC721AQueryableMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // ERC721A Contracts v4.3.0 3 | // Creators: Chiru Labs 4 | 5 | pragma solidity ^0.8.4; 6 | 7 | import '../extensions/ERC721AQueryable.sol'; 8 | import '../extensions/ERC721ABurnable.sol'; 9 | import './DirectBurnBitSetterHelper.sol'; 10 | 11 | contract ERC721AQueryableMock is ERC721AQueryable, ERC721ABurnable, DirectBurnBitSetterHelper { 12 | constructor(string memory name_, string memory symbol_) ERC721A(name_, symbol_) {} 13 | 14 | function safeMint(address to, uint256 quantity) public { 15 | _safeMint(to, quantity); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /contracts/mocks/ERC721AQueryableStartTokenIdMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // ERC721A Contracts v4.3.0 3 | // Creators: Chiru Labs 4 | 5 | pragma solidity ^0.8.4; 6 | 7 | import './ERC721AQueryableMock.sol'; 8 | import './StartTokenIdHelper.sol'; 9 | 10 | contract ERC721AQueryableStartTokenIdMock is StartTokenIdHelper, ERC721AQueryableMock { 11 | constructor( 12 | string memory name_, 13 | string memory symbol_, 14 | uint256 startTokenId_ 15 | ) StartTokenIdHelper(startTokenId_) ERC721AQueryableMock(name_, symbol_) {} 16 | 17 | function _startTokenId() internal view override returns (uint256) { 18 | return startTokenId(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /contracts/mocks/ERC721ASpotMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // ERC721A Contracts v4.3.0 3 | // Creators: Chiru Labs 4 | 5 | pragma solidity ^0.8.4; 6 | 7 | import './ERC721AQueryableMock.sol'; 8 | import './StartTokenIdHelper.sol'; 9 | import './SequentialUpToHelper.sol'; 10 | 11 | contract ERC721ASpotMock is StartTokenIdHelper, SequentialUpToHelper, ERC721AQueryableMock { 12 | constructor( 13 | string memory name_, 14 | string memory symbol_, 15 | uint256 startTokenId_, 16 | uint256 sequentialUpTo_, 17 | uint256 quantity, 18 | bool mintInConstructor 19 | ) StartTokenIdHelper(startTokenId_) SequentialUpToHelper(sequentialUpTo_) ERC721AQueryableMock(name_, symbol_) { 20 | if (mintInConstructor) { 21 | _mintERC2309(msg.sender, quantity); 22 | } 23 | } 24 | 25 | function _startTokenId() internal view override returns (uint256) { 26 | return startTokenId(); 27 | } 28 | 29 | function _sequentialUpTo() internal view override returns (uint256) { 30 | return sequentialUpTo(); 31 | } 32 | 33 | function exists(uint256 tokenId) public view returns (bool) { 34 | return _exists(tokenId); 35 | } 36 | 37 | function getOwnershipOf(uint256 index) public view returns (TokenOwnership memory) { 38 | return _ownershipOf(index); 39 | } 40 | 41 | function safeMintSpot(address to, uint256 tokenId) public { 42 | _safeMintSpot(to, tokenId); 43 | } 44 | 45 | function totalSpotMinted() public view returns (uint256) { 46 | return _totalSpotMinted(); 47 | } 48 | 49 | function totalMinted() public view returns (uint256) { 50 | return _totalMinted(); 51 | } 52 | 53 | function totalBurned() public view returns (uint256) { 54 | return _totalBurned(); 55 | } 56 | 57 | function numberBurned(address owner) public view returns (uint256) { 58 | return _numberBurned(owner); 59 | } 60 | 61 | function setExtraDataAt(uint256 tokenId, uint24 value) public { 62 | _setExtraDataAt(tokenId, value); 63 | } 64 | 65 | function _extraData( 66 | address, 67 | address, 68 | uint24 previousExtraData 69 | ) internal view virtual override returns (uint24) { 70 | return previousExtraData; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /contracts/mocks/ERC721AStartTokenIdMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // ERC721A Contracts v4.3.0 3 | // Creators: Chiru Labs 4 | 5 | pragma solidity ^0.8.4; 6 | 7 | import './ERC721AMock.sol'; 8 | import './StartTokenIdHelper.sol'; 9 | 10 | contract ERC721AStartTokenIdMock is StartTokenIdHelper, ERC721AMock { 11 | constructor( 12 | string memory name_, 13 | string memory symbol_, 14 | uint256 startTokenId_ 15 | ) StartTokenIdHelper(startTokenId_) ERC721AMock(name_, symbol_) {} 16 | 17 | function _startTokenId() internal view override returns (uint256) { 18 | return startTokenId(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /contracts/mocks/ERC721ATransferCounterMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // ERC721A Contracts v4.3.0 3 | // Creators: Chiru Labs 4 | 5 | pragma solidity ^0.8.4; 6 | 7 | import './ERC721AMock.sol'; 8 | 9 | contract ERC721ATransferCounterMock is ERC721AMock { 10 | constructor(string memory name_, string memory symbol_) ERC721AMock(name_, symbol_) {} 11 | 12 | function _extraData( 13 | address from, 14 | address to, 15 | uint24 previousExtraData 16 | ) internal view virtual override returns (uint24) { 17 | if (from == address(0)) { 18 | return 42; 19 | } 20 | if (to == address(0)) { 21 | return 1337; 22 | } 23 | return previousExtraData + 1; 24 | } 25 | 26 | function setExtraDataAt(uint256 index, uint24 extraData) public { 27 | _setExtraDataAt(index, extraData); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /contracts/mocks/ERC721AWithERC2309Mock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // ERC721A Contracts v4.3.0 3 | // Creators: Chiru Labs 4 | 5 | pragma solidity ^0.8.4; 6 | 7 | import '../ERC721A.sol'; 8 | 9 | contract ERC721AWithERC2309Mock is ERC721A { 10 | constructor( 11 | string memory name_, 12 | string memory symbol_, 13 | address to, 14 | uint256 quantity, 15 | bool mintInConstructor 16 | ) ERC721A(name_, symbol_) { 17 | if (mintInConstructor) { 18 | _mintERC2309(to, quantity); 19 | } 20 | } 21 | 22 | /** 23 | * @dev This function is only for gas comparison purposes. 24 | * Calling `_mintERC3201` outside of contract creation is non-compliant 25 | * with the ERC721 standard. 26 | */ 27 | function mintOneERC2309(address to) public { 28 | _mintERC2309(to, 1); 29 | } 30 | 31 | /** 32 | * @dev This function is only for gas comparison purposes. 33 | * Calling `_mintERC3201` outside of contract creation is non-compliant 34 | * with the ERC721 standard. 35 | */ 36 | function mintTenERC2309(address to) public { 37 | _mintERC2309(to, 10); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /contracts/mocks/ERC721ReceiverMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // ERC721A Contracts v4.3.0 3 | // Creators: Chiru Labs 4 | 5 | pragma solidity ^0.8.4; 6 | 7 | import '../ERC721A.sol'; 8 | 9 | interface IERC721AMock { 10 | function safeMint(address to, uint256 quantity) external; 11 | } 12 | 13 | contract ERC721ReceiverMock is ERC721A__IERC721Receiver { 14 | enum Error { 15 | None, 16 | RevertWithMessage, 17 | RevertWithoutMessage, 18 | Panic 19 | } 20 | 21 | bytes4 private immutable _retval; 22 | address private immutable _erc721aMock; 23 | 24 | event Received(address operator, address from, uint256 tokenId, bytes data, uint256 gas); 25 | 26 | constructor(bytes4 retval, address erc721aMock) { 27 | _retval = retval; 28 | _erc721aMock = erc721aMock; 29 | } 30 | 31 | function onERC721Received( 32 | address operator, 33 | address from, 34 | uint256 tokenId, 35 | bytes memory data 36 | ) public override returns (bytes4) { 37 | uint256 dataValue = data.length == 0 ? 0 : uint256(uint8(data[0])); 38 | 39 | // For testing reverts with a message from the receiver contract. 40 | if (dataValue == 0x01) { 41 | revert('reverted in the receiver contract!'); 42 | } 43 | 44 | // For testing with the returned wrong value from the receiver contract. 45 | if (dataValue == 0x02) { 46 | return 0x0; 47 | } 48 | 49 | // For testing the reentrancy protection. 50 | if (dataValue == 0x03) { 51 | IERC721AMock(_erc721aMock).safeMint(address(this), 1); 52 | } 53 | 54 | emit Received(operator, from, tokenId, data, 20000); 55 | return _retval; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /contracts/mocks/SequentialUpToHelper.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // ERC721A Contracts v4.3.0 3 | // Creators: Chiru Labs 4 | 5 | pragma solidity ^0.8.4; 6 | 7 | /** 8 | * This Helper is used to return a dynamic value in the overridden _sequentialUpTo() function. 9 | * Extending this Helper before the ERC721A contract give us access to the herein set `sequentialUpTo` 10 | * to be returned by the overridden `_sequentialUpTo()` function of ERC721A in the ERC721ASpot mocks. 11 | */ 12 | contract SequentialUpToHelper { 13 | // `bytes4(keccak256('sequentialUpTo'))`. 14 | uint256 private constant SEQUENTIAL_UP_TO_STORAGE_SLOT = 0x9638c59e; 15 | 16 | constructor(uint256 sequentialUpTo_) { 17 | _initializeSequentialUpTo(sequentialUpTo_); 18 | } 19 | 20 | function sequentialUpTo() public view returns (uint256 result) { 21 | assembly { 22 | result := sload(SEQUENTIAL_UP_TO_STORAGE_SLOT) 23 | } 24 | } 25 | 26 | function _initializeSequentialUpTo(uint256 value) private { 27 | // We use assembly to directly set the `sequentialUpTo` in storage so that 28 | // inheriting this class won't affect the layout of other storage slots. 29 | assembly { 30 | sstore(SEQUENTIAL_UP_TO_STORAGE_SLOT, value) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /contracts/mocks/StartTokenIdHelper.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // ERC721A Contracts v4.3.0 3 | // Creators: Chiru Labs 4 | 5 | pragma solidity ^0.8.4; 6 | 7 | /** 8 | * This Helper is used to return a dynamic value in the overridden _startTokenId() function. 9 | * Extending this Helper before the ERC721A contract give us access to the herein set `startTokenId` 10 | * to be returned by the overridden `_startTokenId()` function of ERC721A in the ERC721AStartTokenId mocks. 11 | */ 12 | contract StartTokenIdHelper { 13 | // `bytes4(keccak256('startTokenId'))`. 14 | uint256 private constant _START_TOKEN_ID_STORAGE_SLOT = 0x28f75032; 15 | 16 | constructor(uint256 startTokenId_) { 17 | _initializeStartTokenId(startTokenId_); 18 | } 19 | 20 | function startTokenId() public view returns (uint256 result) { 21 | assembly { 22 | result := sload(_START_TOKEN_ID_STORAGE_SLOT) 23 | } 24 | } 25 | 26 | function _initializeStartTokenId(uint256 value) private { 27 | // We use assembly to directly set the `startTokenId` in storage so that 28 | // inheriting this class won't affect the layout of other storage slots. 29 | assembly { 30 | sstore(_START_TOKEN_ID_STORAGE_SLOT, value) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiru-labs/ERC721A/338e327ae4abb18b3f1a37e364894625a6ea2778/docs/.nojekyll -------------------------------------------------------------------------------- /docs/assets/css/prism-theme.css: -------------------------------------------------------------------------------- 1 | /* Generated with http://k88hudson.github.io/syntax-highlighting-theme-generator/www */ 2 | /* http://k88hudson.github.io/react-markdocs */ 3 | /** 4 | * @author k88hudson 5 | * 6 | * Based on prism.js default theme for JavaScript, CSS and HTML 7 | * Based on dabblet (http://dabblet.com) 8 | * @author Lea Verou 9 | */ 10 | /********************************************************* 11 | * General 12 | */ 13 | pre[class*="language-"], 14 | code[class*="language-"] { 15 | color: #465459; 16 | font-size: 13px; 17 | text-shadow: none; 18 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; 19 | direction: ltr; 20 | text-align: left; 21 | white-space: pre; 22 | word-spacing: normal; 23 | word-break: normal; 24 | line-height: 1.5; 25 | -moz-tab-size: 4; 26 | -o-tab-size: 4; 27 | tab-size: 4; 28 | -webkit-hyphens: none; 29 | -moz-hyphens: none; 30 | -ms-hyphens: none; 31 | hyphens: none; 32 | } 33 | pre[class*="language-"]::selection, 34 | code[class*="language-"]::selection, 35 | pre[class*="language-"]::mozselection, 36 | code[class*="language-"]::mozselection { 37 | text-shadow: none; 38 | background: #b3d4fc; 39 | } 40 | @media print { 41 | pre[class*="language-"], 42 | code[class*="language-"] { 43 | text-shadow: none; 44 | } 45 | } 46 | pre[class*="language-"] { 47 | padding: 1em; 48 | margin: .5em 0; 49 | overflow: auto; 50 | background: #f3f2f0; 51 | } 52 | :not(pre) > code[class*="language-"] { 53 | padding: .1em .3em; 54 | border-radius: .3em; 55 | color: #db4c69; 56 | background: #f9f2f4; 57 | } 58 | /********************************************************* 59 | * Tokens 60 | */ 61 | .namespace { 62 | opacity: .7; 63 | } 64 | .token.keyword, 65 | .token.comment { 66 | font-style: italic; 67 | } 68 | .token.comment, 69 | .token.prolog, 70 | .token.doctype, 71 | .token.cdata { 72 | color: #919191; 73 | } 74 | .token.punctuation { 75 | color: #7f7c9c; 76 | } 77 | .token.property, 78 | .token.tag, 79 | .token.boolean, 80 | .token.number, 81 | .token.constant, 82 | .token.symbol, 83 | .token.deleted { 84 | color: #4f8096; 85 | } 86 | .token.selector, 87 | .token.attr-name, 88 | .token.string, 89 | .token.char, 90 | .token.builtin, 91 | .token.inserted { 92 | color: #987b52; 93 | } 94 | .token.operator, 95 | .token.entity, 96 | .token.url, 97 | .language-css .token.string, 98 | .style .token.string { 99 | color: #7f7c9c; 100 | background: #f3f2f0; 101 | } 102 | .token.atrule, 103 | .token.attr-value, 104 | .token.keyword { 105 | color: #c13441; 106 | } 107 | .token.function { 108 | color: #8f5a3d; 109 | } 110 | .token.regex, 111 | .token.important, 112 | .token.variable { 113 | color: #4f8096; 114 | } 115 | .token.important, 116 | .token.bold { 117 | font-weight: bold; 118 | } 119 | .token.italic { 120 | font-style: italic; 121 | } 122 | .token.entity { 123 | cursor: help; 124 | } 125 | /********************************************************* 126 | * Line highlighting 127 | */ 128 | pre[data-line] { 129 | position: relative; 130 | } 131 | pre[class*="language-"] > code[class*="language-"] { 132 | position: relative; 133 | z-index: 1; 134 | } 135 | .line-highlight { 136 | position: absolute; 137 | left: 0; 138 | right: 0; 139 | padding: inherit 0; 140 | margin-top: 1em; 141 | background: #f7ebc6; 142 | box-shadow: inset 5px 0 0 #f7d87c; 143 | z-index: 0; 144 | pointer-events: none; 145 | line-height: inherit; 146 | white-space: pre; 147 | } 148 | -------------------------------------------------------------------------------- /docs/assets/fontello/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Font license info 2 | 3 | 4 | ## Meteocons 5 | 6 | Copyright (C) 2012 by Alessio Atzeni 7 | 8 | Author: Alessio Atzeni 9 | License: SIL (http://scripts.sil.org/OFL) 10 | Homepage: http://www.alessioatzeni.com 11 | 12 | 13 | ## Font Awesome 14 | 15 | Copyright (C) 2016 by Dave Gandy 16 | 17 | Author: Dave Gandy 18 | License: SIL () 19 | Homepage: http://fortawesome.github.com/Font-Awesome/ 20 | 21 | 22 | -------------------------------------------------------------------------------- /docs/assets/fontello/README.txt: -------------------------------------------------------------------------------- 1 | This webfont is generated by https://fontello.com open source project. 2 | 3 | 4 | ================================================================================ 5 | Please, note, that you should obey original font licenses, used to make this 6 | webfont pack. Details available in LICENSE.txt file. 7 | 8 | - Usually, it's enough to publish content of LICENSE.txt file somewhere on your 9 | site in "About" section. 10 | 11 | - If your project is open-source, usually, it will be ok to make LICENSE.txt 12 | file publicly available in your repository. 13 | 14 | - Fonts, used in Fontello, don't require a clickable link on your site. 15 | But any kind of additional authors crediting is welcome. 16 | ================================================================================ 17 | 18 | 19 | Comments on archive content 20 | --------------------------- 21 | 22 | - /font/* - fonts in different formats 23 | 24 | - /css/* - different kinds of css, for all situations. Should be ok with 25 | twitter bootstrap. Also, you can skip style and assign icon classes 26 | directly to text elements, if you don't mind about IE7. 27 | 28 | - demo.html - demo file, to show your webfont content 29 | 30 | - LICENSE.txt - license info about source fonts, used to build your one. 31 | 32 | - config.json - keeps your settings. You can import it back into fontello 33 | anytime, to continue your work 34 | 35 | 36 | Why so many CSS files ? 37 | ----------------------- 38 | 39 | Because we like to fit all your needs :) 40 | 41 | - basic file, .css - is usually enough, it contains @font-face 42 | and character code definitions 43 | 44 | - *-ie7.css - if you need IE7 support, but still don't wish to put char codes 45 | directly into html 46 | 47 | - *-codes.css and *-ie7-codes.css - if you like to use your own @font-face 48 | rules, but still wish to benefit from css generation. That can be very 49 | convenient for automated asset build systems. When you need to update font - 50 | no need to manually edit files, just override old version with archive 51 | content. See fontello source code for examples. 52 | 53 | - *-embedded.css - basic css file, but with embedded WOFF font, to avoid 54 | CORS issues in Firefox and IE9+, when fonts are hosted on the separate domain. 55 | We strongly recommend to resolve this issue by `Access-Control-Allow-Origin` 56 | server headers. But if you ok with dirty hack - this file is for you. Note, 57 | that data url moved to separate @font-face to avoid problems with 2 | 3 | 4 | Copyright (C) 2022 by original authors @ fontello.com 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /docs/assets/fontello/font/fontello.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiru-labs/ERC721A/338e327ae4abb18b3f1a37e364894625a6ea2778/docs/assets/fontello/font/fontello.ttf -------------------------------------------------------------------------------- /docs/assets/fontello/font/fontello.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiru-labs/ERC721A/338e327ae4abb18b3f1a37e364894625a6ea2778/docs/assets/fontello/font/fontello.woff -------------------------------------------------------------------------------- /docs/assets/fontello/font/fontello.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiru-labs/ERC721A/338e327ae4abb18b3f1a37e364894625a6ea2778/docs/assets/fontello/font/fontello.woff2 -------------------------------------------------------------------------------- /docs/assets/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiru-labs/ERC721A/338e327ae4abb18b3f1a37e364894625a6ea2778/docs/assets/img/favicon.png -------------------------------------------------------------------------------- /docs/assets/img/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiru-labs/ERC721A/338e327ae4abb18b3f1a37e364894625a6ea2778/docs/assets/img/preview.png -------------------------------------------------------------------------------- /docs/assets/logo/stylesheet.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Styrene B'; 3 | src: url('subset-StyreneB-Bold.woff2') format('woff2'), 4 | url('subset-StyreneB-Bold.woff') format('woff'); 5 | font-weight: bold; 6 | font-style: normal; 7 | font-display: swap; 8 | } 9 | 10 | -------------------------------------------------------------------------------- /docs/assets/logo/subset-StyreneB-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiru-labs/ERC721A/338e327ae4abb18b3f1a37e364894625a6ea2778/docs/assets/logo/subset-StyreneB-Bold.woff -------------------------------------------------------------------------------- /docs/assets/logo/subset-StyreneB-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiru-labs/ERC721A/338e327ae4abb18b3f1a37e364894625a6ea2778/docs/assets/logo/subset-StyreneB-Bold.woff2 -------------------------------------------------------------------------------- /docs/design.md: -------------------------------------------------------------------------------- 1 | # Design 2 | 3 | ERC721A enables a near constant gas cost for batch minting via a lazy-initialization mechanism. 4 | 5 | Token IDs are minted in sequential order (e.g. 0, 1, 2, 3, ...). 6 | 7 | Regardless of the quantity minted, the `_mint` function only performs 3 `SSTORE` operations: 8 | 9 | - Initialize the ownership slot at the starting token ID with the address. 10 | - Update the address' balance. 11 | - Update the next token ID. 12 | 13 | > A `Transfer` event will still be emitted for each NFT minted. 14 | However, emitting an event is an order of magnitude cheaper than a `SSTORE` operation. 15 | 16 | ## Lower Fees 17 | 18 | ERC721A defers the initialization of token ownership slots from minting to transferring. 19 | 20 | This allows users to batch mint with low, near-constant transaction fees, and pick a good time to transfer when the network's BASEFEE is lower. 21 | 22 | Although this has a higher total gas cost (minting + transfers), **it gives significantly lower overall transaction fees in practice**. 23 | 24 | > Transaction Fee = (BASEFEE + Max Priority Per Gas) * Gas Used 25 | 26 | Gas savings during high BASEFEE periods matter **more**. 27 | 28 | As such, ERC721A prioritizes gas savings for the minting phase. 29 | 30 | ### Benchmark 31 | 32 | To illustrate, we compare OpenZeppelin's ERC721 with ERC721A. 33 | 34 | | | ERC721 | ERC721A | 35 | | -------------------------- | ------------ | -------------- | 36 | | Batch Mint 5 Tokens | 155949 gas | 63748 gas | 37 | | Transfer 5 Tokens | 226655 gas | 334450 gas | 38 | | Mint BASEFEE | 200 gwei | 200 gwei | 39 | | Transfer BASEFEE | 40 gwei | 40 gwei | 40 | | Total Transaction Fees | 0.0403 ether | 0.0261 ether | 41 | 42 | Even for conservatively small batch sizes (e.g. 5), we can observe decent savings over the barebones implementation. 43 | 44 | In practice, the Mint BASEFEE for ERC721 can be much higher. 45 | 46 | When consecutive blocks hit the block gas limit, [the BASEFEE increases exponentially](https://ethereum.org/en/developers/docs/gas/#base-fee). 47 | 48 | #### First Transfer vs Subsequent Transfers 49 | 50 | The main overhead of transferring a token **only occurs during its very first transfer** for an uninitialized slot. 51 | 52 | | | ERC721 | ERC721A | 53 | | -------------------------- | ------------ | -------------- | 54 | | First transfer | 45331 gas | 92822 gas | 55 | | Subsequent transfers | 45331 gas | 44499 gas | 56 | 57 | Here, we bulk mint 10 tokens, and compare the transfer costs of the 5th token in the batch. 58 | 59 | To keep the cost of the `SSTORE` writing to the balance mapping constant, we ensure that the destination addresses have non-zero balances during all transfers. 60 | 61 | The first transfer with ERC721A will incur the storage overheads: 62 | 63 | - 2 extra `SSTORE`s (initialize current slot and next slot, both of which are empty). 64 | - 5 extra `SLOAD`s (read previous slots and next slot). 65 | 66 | ## Balance Mapping 67 | 68 | ERC721A maintains an internal mapping of address balances. This is an important and deliberate design decision: 69 | 70 | - The `balanceOf` function is required by the ERC721 standard. 71 | 72 | We understand that it is tempting to remove the mapping -- it can save a `SSTORE` during mints, and 2 `SSTORE`s during transfers. 73 | 74 | However, this degrades the `balanceOf` function to become O(n) in complexity -- it must bruteforce through the entire mapping of ownerships. This hampers on-chain interoperability and scalability. 75 | 76 | - While it is possible to emulate the `balanceOf` function by listening to emitted events, this requires users to use centralized indexing services. 77 | 78 | In the case of service disruption, the data can get out-of-sync and hard to reconstruct. 79 | 80 | - In the context of saving gas, we are able to allow whitelist minting achieve the same amount of `SSTORE`s when compared to implementations without the mapping. See `ERC721A._getAux` and `ERC721A._setAux`. 81 | 82 | The address balance mapping is also used to store the mint and burn counts per address with negligible overhead, which can be very useful for tokenomics. 83 | 84 | In all, the address balance mapping gives a good balance of features and gas savings, which makes it desirable to keep. 85 | 86 | -------------------------------------------------------------------------------- /docs/erc4907a.md: -------------------------------------------------------------------------------- 1 | # ERC4907A 2 | 3 | [`erc721a/contracts/extensions/ERC4907A.sol`](https://github.com/chiru-labs/ERC721A/blob/main/contracts/extensions/ERC4907A.sol) 4 | 5 | [ERC4907](https://eips.ethereum.org/EIPS/eip-4907) compliant extension of ERC721A, which allows owners and authorized addresses to add a time-limited role with restricted permissions to ERC721 tokens. 6 | 7 | This implementation does **not** reset the user information upon token transfer. 8 | The new owner must call [`setUser`](#setUser) to change or reset the user. 9 | 10 | To reset or alter the user information after each transfer, you will need to override either 11 | [`_beforeTokenTransfers`](erc721a.md#_beforeTokenTransfers) or 12 | [`_afterTokenTransfers`](erc721a.md#_afterTokenTransfers) in ERC721A. 13 | 14 | Inherits: 15 | 16 | - [ERC721A](erc721a.md) 17 | - [IERC4907A](interfaces.md#ierc4907a) 18 | 19 | 20 | ## Functions 21 | 22 | ### setUser 23 | 24 | ```solidity 25 | function setUser(uint256 tokenId, address user, uint64 expires) public virtual 26 | ``` 27 | 28 | Sets the `user` and `expires` for `tokenId`. 29 | 30 | The zero address indicates there is no user. 31 | 32 | Requirements: 33 | 34 | - The caller must own `tokenId` or be an approved operator. 35 | 36 | 37 | ### userOf 38 | 39 | ```solidity 40 | function userOf(uint256 tokenId) public view virtual returns (address) 41 | ``` 42 | 43 | Returns the user address for `tokenId`. 44 | 45 | The zero address indicates that there is no user or if the user is expired. 46 | 47 | ### userExpires 48 | 49 | ```solidity 50 | function userExpires(uint256 tokenId) public view virtual returns (uint256) 51 | ``` 52 | 53 | Returns the user's expires of `tokenId`. 54 | 55 | ### \_explicitUserOf 56 | 57 | ```solidity 58 | function _explicitUserOf(uint256 tokenId) internal view virtual returns (address) 59 | ``` 60 | 61 | Returns the user address for `tokenId`, ignoring the expiry status. 62 | 63 | 64 | ## Events 65 | 66 | ### UpdateUser 67 | 68 | `IERC4907-UpdateUser` 69 | 70 | ```solidity 71 | event UpdateUser(uint256 indexed tokenId, address indexed user, uint64 expires) 72 | ``` 73 | 74 | Emitted when the `user` of an NFT or the `expires` of the `user` is changed. 75 | 76 | The zero address for user indicates that there is no user address. 77 | 78 | -------------------------------------------------------------------------------- /docs/erc721a-burnable.md: -------------------------------------------------------------------------------- 1 | # ERC721ABurnable 2 | 3 | [`erc721a/contracts/extensions/ERC721ABurnable.sol`](https://github.com/chiru-labs/ERC721A/blob/main/contracts/extensions/ERC721ABurnable.sol) 4 | 5 | ERC721A Token that can be irreversibly burned (destroyed). 6 | 7 | Inherits: 8 | 9 | - [ERC721A](erc721a.md) 10 | - [IERC721ABurnable](interfaces.md#ierc721aburnable) 11 | 12 | ## Functions 13 | 14 | ### burn 15 | 16 | ```solidity 17 | function burn(uint256 tokenId) public virtual 18 | ``` 19 | 20 | Burns `tokenId`. See [`ERC721A._burn`](erc721a.md#_burn). 21 | 22 | Requirements: 23 | 24 | - The caller must own `tokenId` or be an approved operator. 25 | 26 | -------------------------------------------------------------------------------- /docs/erc721a-queryable.md: -------------------------------------------------------------------------------- 1 | # ERC721AQueryable 2 | 3 | [`erc721a/contracts/extensions/ERC721AQueryable.sol`](https://github.com/chiru-labs/ERC721A/blob/main/contracts/extensions/ERC721AQueryable.sol) 4 | 5 | ERC721A subclass with convenience query functions. 6 | 7 | Inherits: 8 | 9 | - [ERC721A](erc721a.md) 10 | - [IERC721AQueryable](interfaces.md#ierc721aqueryable) 11 | 12 | ## Functions 13 | 14 | ### explicitOwnershipOf 15 | 16 | ```solidity 17 | function explicitOwnershipOf(uint256 tokenId) public view returns (TokenOwnership memory) 18 | ``` 19 | 20 | Returns the [`TokenOwnership`](erc721a.md#tokenownership) struct at `tokenId` without reverting. 21 | 22 | This is useful for on-chain and off-chain querying of ownership data for tokenomics. 23 | 24 | The returned struct will contain the following values: 25 | 26 | - If the `tokenId` is out of bounds: 27 | - `addr` = `address(0)` 28 | - `startTimestamp` = `0` 29 | - `burned` = `false` 30 | 31 | - If the `tokenId` is burned: 32 | - `addr` = `
` 33 | - `startTimestamp` = `` 34 | - `burned` = `true` 35 | 36 | - Otherwise: 37 | - `addr` = `
` 38 | - `startTimestamp` = `` 39 | - `burned` = `false` 40 | 41 | ### explicitOwnershipsOf 42 | 43 | ```solidity 44 | function explicitOwnershipsOf( 45 | uint256[] memory tokenIds 46 | ) external view returns (TokenOwnership[] memory) 47 | ``` 48 | 49 | Returns an array of `TokenOwnership` structs at `tokenIds` in order. 50 | 51 | See [`explicitOwnershipOf`](#explicitOwnershipOf). 52 | 53 | ### tokensOfOwner 54 | 55 | ```solidity 56 | function tokensOfOwner(address owner) external view returns (uint256[] memory) 57 | ``` 58 | 59 | Returns an array of token IDs owned by `owner`. 60 | 61 | This function scans the ownership mapping and is O(totalSupply) in complexity. 62 | It is meant to be called off-chain. 63 | 64 | See [`tokensOfOwnerIn`](#tokensOfOwnerIn) for splitting the scan into 65 | multiple smaller scans if the collection is large enough to 66 | cause an out-of-gas error. 67 | 68 | This function should work fine for 10k PFP collections. 69 | 70 | ### tokensOfOwnerIn 71 | 72 | ```solidity 73 | function tokensOfOwnerIn( 74 | address owner, 75 | uint256 start, 76 | uint256 stop 77 | ) external view returns (uint256[] memory) 78 | ``` 79 | 80 | Returns an array of token IDs owned by `owner`, 81 | in the range \[`start`, `stop`) 82 | (i.e. `start` ≤ `tokenId` < `stop`). 83 | 84 | This function allows for tokens to be queried if the collection 85 | grows too big for a single call of [`tokensOfOwner`](#tokensOfOwner). 86 | 87 | Requirements: 88 | 89 | - `start` < `stop`. -------------------------------------------------------------------------------- /docs/erc721a.md: -------------------------------------------------------------------------------- 1 | # ERC721A 2 | 3 | [`erc721a/contracts/ERC721A.sol`](https://github.com/chiru-labs/ERC721A/blob/main/contracts/ERC721A.sol) 4 | 5 | Implementation of [ERC721](https://eips.ethereum.org/EIPS/eip-721) Non-Fungible Token Standard, including the Metadata extension. Built to optimize for lower gas during batch mints. 6 | 7 | Token IDs are minted sequentially (e.g. 0, 1, 2, 3...) starting from `_startTokenId()`. 8 | 9 | An owner cannot have more than `2**64 - 1` (max value of `uint64`) tokens. 10 | 11 | Inherits: 12 | 13 | - [IERC721A](interfaces.md#ierc721a) 14 | 15 | ## Structs 16 | 17 | ### TokenOwnership 18 | 19 | ```solidity 20 | struct TokenOwnership { 21 | // The address of the owner. 22 | address addr; 23 | // Keeps track of the start time of ownership with minimal overhead for tokenomics. 24 | uint64 startTimestamp; 25 | // Whether the token has been burned. 26 | bool burned; 27 | } 28 | ``` 29 | 30 | Holds ownership data for each token. 31 | 32 | `startTimestamp` is the timestamp when the token is minted to, transferred to, or burned by `addr`. 33 | 34 | 35 | ## Functions 36 | 37 | ### constructor 38 | 39 | ```solidity 40 | constructor(string memory name_, string memory symbol_) 41 | ``` 42 | 43 | Initializes the contract by setting a `name` and a `symbol` to the token collection. 44 | 45 | ### supportsInterface 46 | 47 | `IERC165-supportsInterface` 48 | 49 | ```solidity 50 | function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) 51 | ``` 52 | 53 | Returns `true` if this contract implements the interface defined by `interfaceId`. 54 | 55 | See the corresponding [EIP section](https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified) to learn more about how these ids are created. 56 | 57 | See [migration](migration.md#supportsInterface) for `supportsInterface`. 58 | 59 | ### totalSupply 60 | 61 | `IERC721Enumerable-totalSupply` 62 | 63 | ```solidity 64 | function totalSupply() public view returns (uint256) 65 | ``` 66 | 67 | Returns the total number of tokens in existence. 68 | 69 | Burned tokens will reduce the count. 70 | 71 | To get the total number of tokens minted, please see [`_totalMinted`](#_totalMinted). 72 | 73 | ### balanceOf 74 | 75 | `IERC721-balanceOf` 76 | 77 | ```solidity 78 | function balanceOf(address owner) public view override returns (uint256) 79 | ``` 80 | 81 | Returns the number of tokens in `owner`'s account. 82 | 83 | ### ownerOf 84 | 85 | `IERC721-ownerOf` 86 | 87 | ```solidity 88 | function ownerOf(uint256 tokenId) public view override returns (address) 89 | ``` 90 | 91 | Returns the owner of the `tokenId` token. 92 | 93 | Requirements: 94 | 95 | - `tokenId` must exist. 96 | 97 | ### name 98 | 99 | `IERC721Metadata-name` 100 | 101 | ```solidity 102 | function name() public view virtual override returns (string memory) 103 | ``` 104 | 105 | Returns the token collection name. 106 | 107 | ### symbol 108 | 109 | `IERC721Metadata-symbol` 110 | 111 | ```solidity 112 | function symbol() public view virtual override returns (string memory) 113 | ``` 114 | 115 | Returns the token collection symbol. 116 | 117 | ### tokenURI 118 | 119 | `IERC721Metadata-tokenURI` 120 | 121 | ```solidity 122 | function tokenURI(uint256 tokenId) public view virtual override returns (string memory) 123 | ``` 124 | 125 | Returns the Uniform Resource Identifier (URI) for `tokenId` token. 126 | 127 | See [`_baseURI`](#_baseURI) and [`_toString`](#_toString). 128 | 129 | 130 | ### approve 131 | 132 | `IERC721-approve` 133 | 134 | ```solidity 135 | function approve(address to, uint256 tokenId) public override 136 | ``` 137 | 138 | Gives permission to `to` to transfer `tokenId` token to another account. The approval is cleared when the token is transferred. 139 | 140 | Only a single account can be approved at a time, so approving the zero address clears previous approvals. 141 | 142 | Requirements: 143 | 144 | - The caller must own the token or be an approved operator. 145 | - `tokenId` must exist. 146 | 147 | Emits an `Approval` event. 148 | 149 | ### getApproved 150 | 151 | ```solidity 152 | function getApproved(uint256 tokenId) public view override returns (address) 153 | ``` 154 | 155 | `IERC721-getApproved` 156 | 157 | Returns the account approved for `tokenId` token. 158 | 159 | Requirements: 160 | 161 | - `tokenId` must exist. 162 | 163 | ### setApprovalForAll 164 | 165 | `IERC721-setApprovalForAll` 166 | 167 | ```solidity 168 | function setApprovalForAll( 169 | address operator, 170 | bool approved 171 | ) public virtual override 172 | ``` 173 | 174 | Approve or remove `operator` as an operator for the caller. Operators can call `transferFrom` or `safeTransferFrom` for any token owned by the caller. 175 | 176 | Requirements: 177 | 178 | - The `operator` cannot be the caller. 179 | 180 | Emits an `ApprovalForAll` event. 181 | 182 | ### isApprovedForAll 183 | 184 | `IERC721-isApprovedForAll` 185 | 186 | ```solidity 187 | function isApprovedForAll( 188 | address owner, 189 | address operator 190 | ) public view virtual override returns (bool) 191 | ``` 192 | 193 | Returns if the `operator` is allowed to manage all of the assets of owner. 194 | 195 | See [`setApprovalForAll`](#setApprovalForAll). 196 | 197 | ### transferFrom 198 | 199 | `IERC721-transferFrom` 200 | 201 | ```solidity 202 | function transferFrom( 203 | address from, 204 | address to, 205 | uint256 tokenId 206 | ) public virtual override 207 | ``` 208 | 209 | Transfers `tokenId` token from `from` to `to`. 210 | 211 | Requirements: 212 | 213 | - `from` cannot be the zero address. 214 | - `to` cannot be the zero address. 215 | - `tokenId` token must be owned by `from`. 216 | - If the caller is not `from`, it must be approved to move this token by either `approve` or `setApprovalForAll`. 217 | 218 | Emits a `Transfer` event. 219 | 220 | ### safeTransferFrom 221 | 222 | `IERC721-safeTransferFrom` 223 | 224 | ```solidity 225 | function safeTransferFrom( 226 | address from, 227 | address to, 228 | uint256 tokenId, 229 | bytes memory _data 230 | ) public virtual override 231 | ``` 232 | 233 | Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients are aware of the ERC721 protocol to prevent tokens from being forever locked. 234 | 235 | The `data` parameter is forwarded in `IERC721Receiver.onERC721Received` to contract recipients (optional, default: `""`). 236 | 237 | Requirements: 238 | 239 | - `from` cannot be the zero address. 240 | - `to` cannot be the zero address. 241 | - `tokenId` token must be owned by `from`. 242 | - If the caller is not `from`, it must be approved to move this token by either `approve` or `setApprovalForAll`. 243 | - If `to` refers to a smart contract, it must implement `IERC721Receiver.onERC721Received`, which is called upon a safe transfer. 244 | 245 | Emits a `Transfer` event. 246 | 247 | 248 | ### \_startTokenId 249 | 250 | ```solidity 251 | function _startTokenId() internal view virtual returns (uint256) 252 | ``` 253 | 254 | Returns the starting token ID (default: `0`). 255 | 256 | To change the starting token ID, override this function to return a different constant. 257 | 258 | 259 | ### \_nextTokenId 260 | 261 | ```solidity 262 | function _nextTokenId() internal view virtual returns (uint256) 263 | ``` 264 | 265 | Returns the next token ID to be minted. 266 | 267 | 268 | ### \_totalMinted 269 | 270 | ```solidity 271 | function _totalMinted() internal view returns (uint256) 272 | ``` 273 | 274 | Returns the total amount of tokens minted. 275 | 276 | ### \_numberMinted 277 | 278 | ```solidity 279 | function _numberMinted(address owner) internal view returns (uint256) 280 | ``` 281 | 282 | Returns the number of tokens minted by or on behalf of `owner`. 283 | 284 | ### \_totalBurned 285 | 286 | ```solidity 287 | function _totalBurned() internal view returns (uint256) 288 | ``` 289 | 290 | Returns the total amount of tokens burned. 291 | 292 | 293 | ### \_numberBurned 294 | 295 | ```solidity 296 | function _numberBurned(address owner) internal view returns (uint256) 297 | ``` 298 | 299 | Returns the number of tokens burned by or on behalf of `owner`. 300 | 301 | ### \_getAux 302 | 303 | ```solidity 304 | function _getAux(address owner) internal view returns (uint64) 305 | ``` 306 | 307 | Returns the auxiliary data for `owner` (e.g. number of whitelist mint slots used). 308 | 309 | ### \_setAux 310 | 311 | ```solidity 312 | function _setAux(address owner, uint64 aux) internal 313 | ``` 314 | 315 | Sets the auxiliary data for `owner` (e.g. number of whitelist mint slots used). 316 | 317 | If there are multiple variables, please pack them into a `uint64`. 318 | 319 | 320 | ### \_ownershipOf 321 | 322 | ```solidity 323 | function _ownershipOf(uint256 tokenId) internal view returns (TokenOwnership memory) 324 | ``` 325 | 326 | Returns the token ownership data for `tokenId`. See [`TokenOwnership`](#TokenOwnership). 327 | 328 | The gas spent here starts off proportional to the maximum mint batch size. 329 | 330 | It gradually moves to O(1) as tokens get transferred around in the collection over time. 331 | 332 | 333 | ### \_ownershipAt 334 | 335 | ```solidity 336 | function _ownershipAt(uint256 index) internal view returns (TokenOwnership memory) 337 | ``` 338 | 339 | Returns the token ownership data at the `index` slot. See [`TokenOwnership`](#TokenOwnership). 340 | 341 | The token ownership data may or may not be initialized. 342 | 343 | 344 | ### \_initializeOwnershipAt 345 | 346 | ```solidity 347 | function _initializeOwnershipAt(uint256 index) internal 348 | ``` 349 | 350 | Initializes the token ownership data at the `index` slot, if it has not been initialized. 351 | 352 | If the batch minted is very large, this function can be used to initialize some tokens to 353 | reduce the first-time transfer costs. 354 | 355 | 356 | ### \_exists 357 | 358 | ```solidity 359 | function _exists(uint256 tokenId) internal view returns (bool) 360 | ``` 361 | 362 | Returns whether `tokenId` exists. 363 | 364 | Tokens can be managed by their owner or approved accounts via `approve` or `setApprovalForAll`. 365 | 366 | Tokens start existing when they are minted via `_mint`. 367 | 368 | ### \_safeMint 369 | 370 | ```solidity 371 | function _safeMint( 372 | address to, 373 | uint256 quantity, 374 | bytes memory _data 375 | ) internal 376 | ``` 377 | 378 | The `data` parameter is forwarded in `IERC721Receiver.onERC721Received` to contract recipients (optional, default: `""`). 379 | 380 | **Safe minting is reentrancy safe since V3.** 381 | 382 | See [`_mint`](#_mint). 383 | 384 | ### \_mint 385 | 386 | ```solidity 387 | function _mint( 388 | address to, 389 | uint256 quantity 390 | ) internal 391 | ``` 392 | 393 | Mints `quantity` tokens and transfers them to `to`. 394 | 395 | > To prevent excessive first-time token transfer costs, please limit the `quantity` to a reasonable number (e.g. 30). 396 | > 397 | > Extremely large `quantity` amounts (e.g. > 5000) may result in some marketplaces and indexers to drop some `Transfer` events, 398 | > and cause some mints to not appear. 399 | 400 | Requirements: 401 | 402 | - `to` cannot be the zero address. 403 | - `quantity` must be greater than `0`. 404 | 405 | Emits a `Transfer` event. 406 | 407 | ### \_mintERC2309 408 | 409 | ```solidity 410 | function _mintERC2309( 411 | address to, 412 | uint256 quantity 413 | ) internal 414 | ``` 415 | 416 | Mints `quantity` tokens and transfers them to `to`. 417 | 418 | This function is intended for efficient minting **only** during contract creation. 419 | 420 | It emits only one `ConsecutiveTransfer` as defined in [ERC2309](https://eips.ethereum.org/EIPS/eip-2309), 421 | instead of a sequence of `Transfer` event(s). 422 | 423 | Calling this function outside of contract creation **will** make your contract non-compliant with the ERC721 standard. 424 | 425 | For full ERC721 compliance, substituting ERC721 `Transfer` event(s) with the ERC2309 426 | `ConsecutiveTransfer` event is only permissible during contract creation. 427 | 428 | > To prevent overflows, the function limits `quantity` to a maximum of 5000. 429 | 430 | Requirements: 431 | 432 | - `to` cannot be the zero address. 433 | - `quantity` must be greater than `0`. 434 | 435 | Emits a `ConsecutiveTransfer` event. 436 | 437 | ### \_burn 438 | 439 | ```solidity 440 | function _burn(uint256 tokenId, bool approvalCheck) internal virtual 441 | ``` 442 | 443 | Destroys `tokenId`. 444 | 445 | The approval is cleared when the token is burned. 446 | 447 | Requirements: 448 | 449 | - `tokenId` must exist. 450 | - If `approvalCheck` is `true`, the caller must own `tokenId` or be an approved operator. 451 | 452 | Emits a `Transfer` event. 453 | 454 | 455 | ### \_baseURI 456 | 457 | ```solidity 458 | function _baseURI() internal view virtual returns (string memory) 459 | ``` 460 | 461 | Base URI for computing `tokenURI`. 462 | 463 | If set, the resulting URI for each token will be the concatenation of the `baseURI` and the `tokenId`. 464 | 465 | Empty by default, it can be overridden in child contracts. 466 | 467 | 468 | ### \_beforeTokenTransfers 469 | 470 | ```solidity 471 | function _beforeTokenTransfers( 472 | address from, 473 | address to, 474 | uint256 startTokenId, 475 | uint256 quantity 476 | ) internal virtual 477 | ``` 478 | 479 | Hook that is called before a set of serially-ordered token IDs are about to be transferred. This includes minting. 480 | 481 | Also called before burning one token. 482 | 483 | `startTokenId` - the first token ID to be transferred. 484 | `quantity` - the amount to be transferred. 485 | 486 | Calling conditions: 487 | 488 | - When `from` and `to` are both non-zero, `from`'s `tokenId` will be transferred to `to`. 489 | - When `from` is zero, `tokenId` will be minted for `to`. 490 | - When `to` is zero, `tokenId` will be burned by `from`. 491 | - `from` and `to` are never both zero. 492 | 493 | 494 | ### \_afterTokenTransfers 495 | 496 | ```solidity 497 | function _afterTokenTransfers( 498 | address from, 499 | address to, 500 | uint256 startTokenId, 501 | uint256 quantity 502 | ) internal virtual 503 | ``` 504 | 505 | Hook that is called after a set of serially-ordered token IDs are about to be transferred. This includes minting. 506 | 507 | Also called after burning one token. 508 | 509 | `startTokenId` - the first token ID to be transferred. 510 | `quantity` - the amount to be transferred. 511 | 512 | Calling conditions: 513 | 514 | - When `from` and `to` are both non-zero, `from`'s `tokenId` will be transferred to `to`. 515 | - When `from` is zero, `tokenId` will be minted for `to`. 516 | - When `to` is zero, `tokenId` will be burned by `from`. 517 | - `from` and `to` are never both zero. 518 | 519 | 520 | ### \_toString 521 | 522 | ```solidity 523 | function _toString(uint256 value) internal pure returns (string memory) 524 | ``` 525 | 526 | Converts a `uint256` to its ASCII `string` decimal representation. 527 | 528 | This function is provided as a drop-in replacement for OpenZeppelin's `Strings.toString(uint256 value)`. 529 | 530 | 531 | ### \_msgSenderERC721A 532 | 533 | ```solidity 534 | function _msgSenderERC721A() internal view virtual returns (address) 535 | ``` 536 | 537 | Returns the message sender (defaults to `msg.sender`). 538 | 539 | If you are writing [GSN compatible contracts](https://docs.openzeppelin.com/contracts/2.x/gsn), 540 | you need to override this function 541 | (to return `_msgSender()` if using with OpenZeppelin). 542 | 543 | ### \_extraData 544 | 545 | ```solidity 546 | function _extraData( 547 | address from, 548 | address to, 549 | uint24 previousExtraData 550 | ) internal view virtual returns (uint24) 551 | ``` 552 | 553 | Called during each token transfer to set the 24bit `extraData` field. 554 | 555 | This is an advanced storage hitchhiking feature for storing token related data. 556 | 557 | Intended to be overridden by the deriving contract to return the value to be stored after transfer. 558 | 559 | `previousExtraData` - the value of `extraData` before transfer. 560 | 561 | Calling conditions: 562 | 563 | - When `from` and `to` are both non-zero, `from`'s `tokenId` will be transferred to `to`. 564 | - When `from` is zero, `tokenId` will be minted for `to`. 565 | - When `to` is zero, `tokenId` will be burned by `from`. 566 | - `from` and `to` are never both zero. 567 | 568 | ### \_setExtraDataAt 569 | 570 | ```solidity 571 | function _setExtraDataAt(uint256 index, uint24 extraData) internal 572 | ``` 573 | 574 | Directly sets the `extraData` for the ownership data at `index`. 575 | 576 | This is an advanced storage hitchhiking feature for storing token related data. 577 | 578 | Requirements: 579 | 580 | - The token at `index` must be initialized. 581 | For bulk mints, `index` is the value of [`_nextTokenId`](#_nextTokenId) before bulk minting. 582 | 583 | 584 | ## Events 585 | 586 | ### Transfer 587 | 588 | `IERC721-Transfer` 589 | 590 | ```solidity 591 | event Transfer(address from, address to, uint256 tokenId) 592 | ``` 593 | 594 | Emitted when `tokenId` token is transferred from `from` to `to`. 595 | 596 | ### Approval 597 | 598 | `IERC721-Approval` 599 | 600 | ```solidity 601 | event Approval(address owner, address approved, uint256 tokenId) 602 | ``` 603 | 604 | Emitted when `owner` enables `approved` to manage the `tokenId` token. 605 | 606 | ### ApprovalForAll 607 | 608 | `IERC721-ApprovalForAll` 609 | 610 | ```solidity 611 | event ApprovalForAll(address owner, address operator, bool approved) 612 | ``` 613 | 614 | Emitted when `owner` enables or disables (`approved`) `operator` to manage all of its assets. 615 | 616 | ### ConsecutiveTransfer 617 | 618 | `IERC2309-ConsecutiveTransfer` 619 | 620 | ```solidity 621 | event ConsecutiveTransfer( 622 | uint256 indexed fromTokenId, 623 | uint256 toTokenId, 624 | address indexed from, 625 | address indexed to 626 | ) 627 | ``` 628 | 629 | Emitted when tokens from `fromTokenId` to `toTokenId` (inclusive) are transferred from `from` to `to`, during contract creation. 630 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ERC721A 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 121 | 122 | 123 | 124 |
125 | 146 | 147 | 148 | 149 | 150 | 293 | 294 | 295 | -------------------------------------------------------------------------------- /docs/interfaces.md: -------------------------------------------------------------------------------- 1 | # Interfaces 2 | 3 | These interfaces are available as `.sol` files. These are useful to interact with third-party contracts that implement them. 4 | Please refer to the API for any details. 5 | 6 | - [IERC721A](erc721a.md) 7 | - [IERC721ABurnable](erc721a-burnable.md) 8 | - [IERC721AQueryable](erc721a-queryable.md) 9 | 10 | ## List of interfaces 11 | 12 | ### IERC721A 13 | 14 | [`erc721a/contracts/IERC721A.sol`](https://github.com/chiru-labs/ERC721A/blob/main/contracts/IERC721A.sol) 15 | 16 | ```solidity 17 | import 'erc721a/contracts/interfaces/IERC721A.sol'; 18 | 19 | ``` 20 | 21 | ### IERC721ABurnable 22 | 23 | [`erc721a/contracts/extensions/IERC721ABurnable.sol`](https://github.com/chiru-labs/ERC721A/blob/main/contracts/extensions/IERC721ABurnable.sol) 24 | 25 | Inherits: 26 | 27 | - [IERC721A](#ierc721a) 28 | 29 | ```solidity 30 | import 'erc721a/contracts/interfaces/IERC721ABurnable.sol'; 31 | 32 | ``` 33 | 34 | ### IERC721AQueryable 35 | 36 | [`erc721a/contracts/extensions/IERC721AQueryable.sol`](https://github.com/chiru-labs/ERC721A/blob/main/contracts/extensions/IERC721AQueryable.sol) 37 | 38 | Inherits: 39 | 40 | - [IERC721A](#ierc721a) 41 | 42 | ```solidity 43 | import 'erc721a/contracts/interfaces/IERC721AQueryable.sol'; 44 | 45 | ``` 46 | 47 | ### IERC4907A 48 | 49 | [`erc721a/contracts/extensions/IERC4907A.sol`](https://github.com/chiru-labs/ERC721A/blob/main/contracts/extensions/IERC4907A.sol) 50 | 51 | Inherits: 52 | 53 | - [IERC721A](#ierc721a) 54 | 55 | ```solidity 56 | import 'erc721a/contracts/interfaces/IERC4907A.sol'; 57 | 58 | ``` 59 | -------------------------------------------------------------------------------- /docs/migration.md: -------------------------------------------------------------------------------- 1 | # Migration to 4.x 2 | 3 | In version 4.x, we have made the following breaking changes: 4 | 5 | - Removed OpenZeppelin 6 | - Made some variables private 7 | - Updated Upgradeable to use Diamond storage 8 | 9 | ## API Changes 10 | 11 | ### \_currentIndex 12 | 13 | The `_currentIndex` variable has been made private. 14 | 15 | Please use [`_nextTokenId`](erc721a.md#_nextTokenId) instead. 16 | 17 | If you need a `tokensOfOwner` function, please use [`ERC721AQueryable.tokensOfOwner`](erc721a-queryable.md#tokensOfOwner). 18 | 19 | ### \_burnCounter 20 | 21 | The `_burnCounter` variable has been made private. 22 | 23 | Please use [`_totalBurned`](erc721a.md#_totalBurned) instead. 24 | 25 | ### \_ownerships 26 | 27 | The `_ownerships` mapping has been made private. 28 | 29 | Please use the following instead: 30 | - [`_ownershipOf`](erc721a.md#_ownershipOf) 31 | - [`ERC721AQueryable.explicitOwnershipOf`](erc721a-queryable.md#explicitOwnershipOf) (non-reverting) 32 | - [`ERC721AQueryable.tokensOfOwner`](erc721a-queryable.md#tokensOfOwner) 33 | - [`_ownershipAt`](erc721a.md#_ownershipAt) 34 | 35 | ### \_msgSender 36 | 37 | The dependency on OpenZeppelin `_msgSender` has been removed. 38 | 39 | Please use [`_msgSenderERC721A`](erc721a.md#_msgSenderERC721A) instead. 40 | 41 | ### Strings.toString 42 | 43 | Due to removal of OpenZeppelin, `Strings.toString` has been removed. 44 | 45 | Please use [`_toString`](erc721a.md#_toString) instead. 46 | 47 | ### supportsInterface 48 | 49 | Due to removal of OpenZeppelin, using `super.supportsInterface` in the function override may not work. 50 | 51 | When using with OpenZeppelin's libraries (e.g. [ERC2981](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/common/ERC2981.sol)), you will have to do the following: 52 | 53 | ```solidity 54 | function supportsInterface( 55 | bytes4 interfaceId 56 | ) public view virtual override(ERC721A, ERC2981) returns (bool) { 57 | // Supports the following `interfaceId`s: 58 | // - IERC165: 0x01ffc9a7 59 | // - IERC721: 0x80ac58cd 60 | // - IERC721Metadata: 0x5b5e139f 61 | // - IERC2981: 0x2a55205a 62 | return 63 | ERC721A.supportsInterface(interfaceId) || 64 | ERC2981.supportsInterface(interfaceId); 65 | } 66 | ``` 67 | 68 | ### ERC721AOwnersExplicit 69 | 70 | The `ERC721AOwnersExplicit` extension has been removed. 71 | 72 | Please use [`_initializeOwnershipAt`](erc721a.md#_initializeOwnershipAt) instead. 73 | 74 | You can make your own public wrapper function to initialize the slots for any desired range in a loop. 75 | 76 | ## Diamond Storage 77 | 78 | If your upgradeable contracts are deployed using version 3.x, 79 | they will **NOT** be compatible with version 4.x. 80 | 81 | Using version 4.x to upgrade upgradeable contracts deployed with 3.x will lead to unintended consequences. 82 | 83 | You will need to either continue using 3.3.0 (the last compatible version), 84 | or redeploy from scratch with 4.x (the redeployed contracts will not have the previous data). 85 | 86 | Version 4.x of ERC721A Upgradeable will be compatible with the non-diamond OpenZeppelin Upgradeable libraries. 87 | 88 | -------------------------------------------------------------------------------- /docs/overview.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | ERC721A is an improved implementation of the [ERC721](https://eips.ethereum.org/EIPS/eip-721) Non-Fungible Token Standard that supports minting multiple tokens for close to the cost of one. 4 | 5 | ## Announcements 6 | 7 | > **📢 Version 4.x introduces several breaking changes. 8 | > [Please refer to the migration guide for more details.](migration.md)** 9 | 10 | _We highly recommend reading the migration guide_, **especially** _the part on [`supportsInterface`](migration.md?id=supportsinterface) if you are using with OpenZeppelin extensions_ (e.g. ERC2981). 11 | 12 | ## Installation 13 | 14 | ``` 15 | npm install --save-dev erc721a 16 | ``` 17 | 18 | ## Usage 19 | 20 | Once installed, you can use the contracts in the library by importing them: 21 | 22 | ```solidity 23 | pragma solidity ^0.8.4; 24 | 25 | import "erc721a/contracts/ERC721A.sol"; 26 | 27 | contract Azuki is ERC721A { 28 | constructor() ERC721A("Azuki", "AZUKI") {} 29 | 30 | function mint(uint256 quantity) external payable { 31 | // `_mint`'s second argument now takes in a `quantity`, not a `tokenId`. 32 | _mint(msg.sender, quantity); 33 | } 34 | } 35 | ``` 36 | -------------------------------------------------------------------------------- /docs/sidebar.md: -------------------------------------------------------------------------------- 1 | - [Overview](/) 2 | - [Design](design.md) 3 | - [Tips](tips.md) 4 | - [Upgradeable](upgradeable.md) 5 | - [Migration to 4.x](migration.md) 6 | - API 7 | - [ERC721A](erc721a.md) 8 | - [ERC721ABurnable](erc721a-burnable.md) 9 | - [ERC721AQueryable](erc721a-queryable.md) 10 | - [ERC4907A](erc4907a.md) 11 | - [Interfaces](interfaces.md) 12 | - **Theme** 13 |
14 |
15 |
16 |
light
17 |
18 |
19 |
auto
20 |
21 |
22 |
dark
23 |
24 | - **Links** 25 | - [
Github
](https://github.com/chiru-labs/ERC721A) 26 | - [
Twitter
](https://twitter.com/Azuki) 27 | 28 | -------------------------------------------------------------------------------- /docs/tips.md: -------------------------------------------------------------------------------- 1 | # Tips 2 | 3 | ## Transfers 4 | 5 | For users, it is more gas optimal to transfer bulk minted tokens in ascending token ID order. 6 | 7 | For example, if you have bulk minted token IDs (33, 34, ..., 99), 8 | you should transfer in the order (33, 34, ..., 99). 9 | 10 | This is due to how the lazy-initialization mechanism works internally: 11 | it scans uninitialized slots in descending order until it finds an initialized slot. 12 | 13 | ## Popularity 14 | 15 | The more popular your NFT collection, the larger the expected savings in transaction fees. 16 | 17 | See [Design: Lower Fees](design.md#lower-fees). 18 | 19 | ## Aux 20 | 21 | Consider using [`ERC721A._getAux`](erc721a.md#_getAux) and 22 | [`ERC721A._setAux`](erc721a.md#_setAux) to get / set per-address variables 23 | (e.g. number of whitelist mints per address). 24 | 25 | This can help remove an extra cold `SLOAD` and `SSTORE` operation. 26 | 27 | ## Minting 28 | 29 | For typical artwork collections, consider using `_mint` over `_safeMint` if you don't expect users to mint to contracts. 30 | 31 | ## Batch Size 32 | 33 | During transfers, ERC721A scans through ownership storage slots until it finds an initialized slot. 34 | 35 | To prevent expensive first-time transfer fees for tokens minted in large batches, either: 36 | 37 | - Restrict the max batch size for public mints to a reasonable number. 38 | 39 | - Break up excessively large batches into mini batches internally when minting. 40 | 41 | - Use [`_initializeOwnershipAt`](erc721a.md#_initializeOwnershipAt) every couple tokens to reduce number of reads during a transfer. 42 | 43 | ## Efficient Tokenomics 44 | 45 | ERC721A keeps track of additional variables in the internal mappings. 46 | 47 | - [`startTimestamp`](erc721a.md#_ownershipOf) (starting time of holding) per token. 48 | - [`numberMinted`](erc721a.md#_numberMinted) per address. 49 | - [`numberBurned`](erc721a.md#_numberBurned) per address. 50 | 51 | These variables hitchhike on the `SLOAD`s and `SSTORE`s at near zero additional gas cost (< 1%). 52 | 53 | You can use them to design tokenomics with very minimal gas overhead. 54 | 55 | > The [`startTimestamp`](erc721a.md#_ownershipOf), is available via the 56 | > [`TokenOwnership`](erc721a.md#TokenOwnership) struct. 57 | > 58 | > You can get it from the 59 | > [`_ownershipOf`](erc721a.md#_ownershipOf) function or the non-reverting 60 | > [`ERC721AQueryable.explicitOwnershipOf`](erc721a-queryable.md#explicitOwnershipOf) function. 61 | 62 | ## ERC721A vs ERC1155 63 | 64 | | | ERC721A | ERC1155 | 65 | | ---------------- | -------------- | ---------------------- | 66 | | O(1) ownerOf | Yes | No ownerOf | 67 | | O(1) balanceOf | For all tokens | Within fungible tokens | 68 | | O(1)\* bulk mint | For all tokens | Within fungible tokens | 69 | | # mint `SSTORE`s | 3 | 1 | 70 | 71 | \* Approximately O(1) for ERC721A. See [Design](design.md). 72 | 73 | For unique collections, ERC1155 needs a counter which needs 1 more `SSTORE`. 74 | 75 | ERC1155 requires centralized indexing services to emulate ERC721-like functionality off-chain. 76 | 77 | ## Other Implementations 78 | 79 | ERC721A is not a one-size-fits-all solution. 80 | 81 | It is heavily optimized for generative artwork NFT collections. 82 | 83 | If your collection does not expect a busy mint phase (e.g. a pure utility NFT), 84 | or does not require bulk minting, 85 | these excellent implementations can be better for lowering overall transaction fees: 86 | 87 | - [OpenZeppelin](https://github.com/OpenZeppelin/openzeppelin-contracts) 88 | - [Solmate](https://github.com/Rari-Capital/solmate) 89 | 90 | Use the right tool for the job. 91 | -------------------------------------------------------------------------------- /docs/upgradeable.md: -------------------------------------------------------------------------------- 1 | # Using with Upgrades 2 | 3 | If you are deploying upgradeable contracts, 4 | such as using [OpenZeppelin Upgrade Plugins](https://docs.openzeppelin.com/upgrades-plugins/1.x/), 5 | you will need to use the upgradeable variant of ERC721A. 6 | 7 | For more information, please refer to 8 | [OpenZeppelin's documentation](https://docs.openzeppelin.com/contracts/4.x/upgradeable). 9 | 10 | Since v4, the upgradeable variant uses the Diamond storage pattern as defined in [EIP-2535](https://eips.ethereum.org/EIPS/eip-2535). 11 | 12 | ## Installation 13 | 14 | ``` 15 | npm install --save-dev erc721a-upgradeable 16 | ``` 17 | 18 | ## Usage 19 | 20 | The package shares the same directory layout as the main ERC721A package, but every file and contract has the suffix `Upgradeable`. 21 | 22 | Constructors are replaced by internal initializer functions following the naming convention `__{ContractName}__init`. 23 | 24 | These functions are internal, and you must define your own public initializer function that calls the parent class' initializer. 25 | 26 | ```solidity 27 | pragma solidity ^0.8.4; 28 | 29 | import 'erc721a-upgradeable/contracts/ERC721AUpgradeable.sol'; 30 | import '@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol'; 31 | 32 | contract Something is ERC721AUpgradeable, OwnableUpgradeable { 33 | // Take note of the initializer modifiers. 34 | // - `initializerERC721A` for `ERC721AUpgradeable`. 35 | // - `initializer` for OpenZeppelin's `OwnableUpgradeable`. 36 | function initialize() initializerERC721A initializer public { 37 | __ERC721A_init('Something', 'SMTH'); 38 | __Ownable_init(); 39 | } 40 | 41 | function mint(uint256 quantity) external payable { 42 | // `_mint`'s second argument now takes in a `quantity`, not a `tokenId`. 43 | _mint(msg.sender, quantity); 44 | } 45 | 46 | function adminMint(uint256 quantity) external payable onlyOwner { 47 | _mint(msg.sender, quantity); 48 | } 49 | } 50 | ``` 51 | 52 | If using with another upgradeable library, please do use their respective initializer modifier on the `initialize()` function, in addition to the `initializerERC721A` modifier. 53 | 54 | ## Deployment 55 | 56 | If you are using hardhat, you can deploy it using 57 | [OpenZeppelin Upgrade Plugins](https://docs.openzeppelin.com/upgrades-plugins/1.x/). 58 | 59 | ``` 60 | npm install --save-dev @openzeppelin/hardhat-upgrades 61 | ``` 62 | 63 | **Deploy Script** 64 | 65 | ```javascript 66 | // scripts/deploy.js 67 | const { ethers, upgrades } = require('hardhat'); 68 | const fs = require('fs'); 69 | 70 | async function main () { 71 | const Something = await ethers.getContractFactory('Something'); 72 | console.log('Deploying...'); 73 | const something = await upgrades.deployProxy( 74 | Something, 75 | [], 76 | { initializer: 'initialize' } 77 | ); 78 | await something.deployed(); 79 | const addresses = { 80 | proxy: something.address, 81 | admin: await upgrades.erc1967.getAdminAddress(something.address), 82 | implementation: await upgrades.erc1967.getImplementationAddress( 83 | something.address) 84 | }; 85 | console.log('Addresses:', addresses); 86 | 87 | try { 88 | await run('verify', { address: addresses.implementation }); 89 | } catch (e) {} 90 | 91 | fs.writeFileSync('deployment-addresses.json', JSON.stringify(addresses)); 92 | } 93 | 94 | main(); 95 | ``` 96 | 97 | **Upgrade Script** 98 | 99 | ```javascript 100 | // scripts/upgrade.js 101 | const { ethers, upgrades } = require('hardhat'); 102 | const fs = require('fs'); 103 | 104 | async function main () { 105 | const Something = await ethers.getContractFactory('Something'); 106 | console.log('Upgrading...'); 107 | let addresses = JSON.parse(fs.readFileSync('deployment-addresses.json')); 108 | await upgrades.upgradeProxy(addresses.proxy, Something); 109 | console.log('Upgraded'); 110 | 111 | addresses = { 112 | proxy: addresses.proxy, 113 | admin: await upgrades.erc1967.getAdminAddress(addresses.proxy), 114 | implementation: await upgrades.erc1967.getImplementationAddress( 115 | addresses.proxy) 116 | }; 117 | console.log('Addresses:', addresses); 118 | 119 | try { 120 | await run('verify', { address: addresses.implementation }); 121 | } catch (e) {} 122 | 123 | fs.writeFileSync('deployment-addresses.json', JSON.stringify(addresses)); 124 | } 125 | 126 | main(); 127 | ``` 128 | 129 | ### Local 130 | 131 | Add the following to your `hardhat.config.js`: 132 | 133 | ```javascript 134 | // hardhat.config.js 135 | require("@nomiclabs/hardhat-waffle"); 136 | require('@openzeppelin/hardhat-upgrades'); 137 | 138 | module.exports = { 139 | solidity: "0.8.11" 140 | }; 141 | ``` 142 | 143 | **Deploy** 144 | 145 | ``` 146 | npx hardhat run --network localhost scripts/deploy.js 147 | ``` 148 | 149 | **Upgrade** 150 | 151 | ``` 152 | npx hardhat run --network localhost scripts/upgrade.js 153 | ``` 154 | 155 | ### Testnet / Mainnet 156 | 157 | We will use the Goerli testnet as an example. 158 | 159 | Install the following packages if they are not already installed: 160 | 161 | ``` 162 | npm install --save-dev @nomiclabs/hardhat-etherscan 163 | npm install --save-dev dotenv 164 | ``` 165 | 166 | Add the following to your environment file `.env`: 167 | 168 | ``` 169 | ETHERSCAN_KEY="Your Etherscan API Key" 170 | PRIVATE_KEY="Your Wallet Private Key" 171 | RPC_URL_GOERLI="https://Infura Or Alchemy URL With API Key" 172 | ``` 173 | 174 | Add the following to your `hardhat.config.js`: 175 | 176 | ```javascript 177 | // hardhat.config.js 178 | require("@nomiclabs/hardhat-waffle"); 179 | require('dotenv').config(); 180 | require('@openzeppelin/hardhat-upgrades'); 181 | require("@nomiclabs/hardhat-etherscan"); 182 | 183 | module.exports = { 184 | solidity: "0.8.11", 185 | networks: { 186 | goerli: { 187 | url: process.env.RPC_URL_GOERLI, 188 | accounts: [process.env.PRIVATE_KEY] 189 | } 190 | }, 191 | etherscan: { 192 | // Your API key for Etherscan 193 | // Obtain one at https://etherscan.io/ 194 | apiKey: process.env.ETHERSCAN_KEY 195 | } 196 | }; 197 | ``` 198 | 199 | **Deploy** 200 | 201 | ``` 202 | npx hardhat run --network goerli scripts/deploy.js 203 | ``` 204 | 205 | **Upgrade** 206 | 207 | ``` 208 | npx hardhat run --network goerli scripts/upgrade.js 209 | ``` 210 | -------------------------------------------------------------------------------- /hardhat.config.js: -------------------------------------------------------------------------------- 1 | require('@nomiclabs/hardhat-waffle'); 2 | require('@nomiclabs/hardhat-ethers'); 3 | 4 | if (process.env.REPORT_GAS) { 5 | require('hardhat-gas-reporter'); 6 | } 7 | 8 | if (process.env.REPORT_COVERAGE) { 9 | require('solidity-coverage'); 10 | } 11 | 12 | /** 13 | * @type import('hardhat/config').HardhatUserConfig 14 | */ 15 | module.exports = { 16 | solidity: { 17 | version: '0.8.11', 18 | settings: { 19 | optimizer: { 20 | enabled: true, 21 | runs: 800, 22 | }, 23 | }, 24 | }, 25 | gasReporter: { 26 | currency: 'USD', 27 | gasPrice: 100, 28 | showTimeSpent: true, 29 | }, 30 | plugins: ['solidity-coverage'], 31 | }; 32 | 33 | // The "ripemd160" algorithm is not available anymore in NodeJS 17+ (because of lib SSL 3). 34 | // The following code replaces it with "sha256" instead. 35 | 36 | const crypto = require('crypto'); 37 | 38 | try { 39 | crypto.createHash('ripemd160'); 40 | } catch (e) { 41 | const origCreateHash = crypto.createHash; 42 | crypto.createHash = (alg, opts) => { 43 | return origCreateHash(alg === 'ripemd160' ? 'sha256' : alg, opts); 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "erc721a", 3 | "version": "4.3.0", 4 | "description": "ERC721A contract for Solidity", 5 | "files": [ 6 | "/contracts/**/*.sol", 7 | "/build/contracts/*.json", 8 | "!/contracts/mocks/**/*" 9 | ], 10 | "scripts": { 11 | "node": "hardhat node", 12 | "test": "hardhat test", 13 | "test:gas": "REPORT_GAS=true npx hardhat test", 14 | "coverage": "REPORT_COVERAGE=true npx hardhat coverage", 15 | "lint": "npm run lint:js && npm run lint:sol", 16 | "lint:js": "eslint --ignore-path .gitignore . --fix", 17 | "lint:sol": "prettier --write \"contracts/**/*.sol\"", 18 | "update-version": "scripts/release/update-version.js" 19 | }, 20 | "devDependencies": { 21 | "@nomiclabs/hardhat-ethers": "^2.0.4", 22 | "@nomiclabs/hardhat-waffle": "^2.0.1", 23 | "@openzeppelin/test-helpers": "^0.5.15", 24 | "chai": "^4.3.4", 25 | "eslint": "^8.7.0", 26 | "eslint-plugin-mocha": "^10.0.3", 27 | "eslint-plugin-node": "^11.1.0", 28 | "ethereum-waffle": "^3.4.0", 29 | "ethers": "^5.5.3", 30 | "hardhat": "^2.8.2", 31 | "hardhat-gas-reporter": "^1.0.7", 32 | "prettier": "^2.5.1", 33 | "prettier-plugin-solidity": "^1.0.0-beta.19", 34 | "solidity-coverage": "^0.7.20" 35 | }, 36 | "repository": { 37 | "type": "git", 38 | "url": "git+https://github.com/chiru-labs/ERC721A.git" 39 | }, 40 | "author": "chiru-labs", 41 | "license": "ISC", 42 | "bugs": { 43 | "url": "https://github.com/chiru-labs/ERC721A/issues" 44 | }, 45 | "homepage": "https://github.com/chiru-labs/ERC721A#readme" 46 | } -------------------------------------------------------------------------------- /scripts/release/update-version.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const fs = require('fs'); 3 | const glob = require('glob'); 4 | 5 | // read version from input arg 6 | const version = process.argv[2]; 7 | // abort if no version is given 8 | if (!version) { 9 | console.error('No version specified'); 10 | process.exit(1); 11 | } 12 | 13 | // update package.json version 14 | const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8')); 15 | packageJson.version = version; 16 | fs.writeFileSync('package.json', JSON.stringify(packageJson, null, 2)); 17 | 18 | // update package-lock.json version 19 | const packageLockJson = JSON.parse(fs.readFileSync('package-lock.json', 'utf8')); 20 | packageLockJson.version = version; 21 | packageLockJson.packages[""].version = version; 22 | fs.writeFileSync('package-lock.json', JSON.stringify(packageLockJson, null, 2)); 23 | 24 | const spdxString = '// SPDX-License-Identifier: MIT'; 25 | const versionPrefix = '// ERC721A Contracts v'; 26 | 27 | // loop through all files with contracts/**/*.sol pattern 28 | glob('contracts/**/*.sol', null, function (err, files) { 29 | files.forEach((file) => { 30 | // read file content 31 | const content = fs.readFileSync(file, 'utf8'); 32 | 33 | const versionStringLine = versionPrefix + version; 34 | 35 | let updatedContent; 36 | if (content.includes(versionPrefix)) { 37 | updatedContent = content.replace(new RegExp(`${versionPrefix}.*`), `${versionStringLine}`); 38 | } else { 39 | updatedContent = content.replace(new RegExp(`${spdxString}`), `${spdxString}\n${versionStringLine}`); 40 | } 41 | 42 | // write updated file 43 | fs.writeFileSync(file, updatedContent); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /test/GasUsage.test.js: -------------------------------------------------------------------------------- 1 | const { deployContract } = require('./helpers.js'); 2 | 3 | describe('ERC721A Gas Usage', function () { 4 | beforeEach(async function () { 5 | this.erc721a = await deployContract('ERC721AGasReporterMock', ['Azuki', 'AZUKI']); 6 | const [owner, addr1] = await ethers.getSigners(); 7 | this.owner = owner; 8 | this.addr1 = addr1; 9 | }); 10 | 11 | context('mintOne', function () { 12 | it('runs mintOne 2 times', async function () { 13 | for (let i = 0; i < 2; i++) { 14 | await this.erc721a.mintOne(this.addr1.address); 15 | } 16 | }); 17 | }); 18 | 19 | context('safeMintOne', function () { 20 | it('runs safeMintOne 2 times', async function () { 21 | for (let i = 0; i < 2; i++) { 22 | await this.erc721a.safeMintOne(this.addr1.address); 23 | } 24 | }); 25 | }); 26 | 27 | context('mintTen', function () { 28 | it('runs mintTen 2 times', async function () { 29 | for (let i = 0; i < 2; i++) { 30 | await this.erc721a.mintTen(this.addr1.address); 31 | } 32 | }); 33 | }); 34 | 35 | context('safeMintTen', function () { 36 | it('runs safeMintTen 2 times', async function () { 37 | for (let i = 0; i < 2; i++) { 38 | await this.erc721a.safeMintTen(this.addr1.address); 39 | } 40 | }); 41 | }); 42 | 43 | context('mintHundred', function () { 44 | it('runs mintHundred 2 times', async function () { 45 | for (let i = 0; i < 2; i++) { 46 | await this.erc721a.mintHundred(this.addr1.address); 47 | } 48 | }); 49 | }); 50 | 51 | context('safeMintHundred', function () { 52 | it('runs safeMintHundred 2 times', async function () { 53 | for (let i = 0; i < 2; i++) { 54 | await this.erc721a.safeMintHundred(this.addr1.address); 55 | } 56 | }); 57 | }); 58 | 59 | context('transferFrom', function () { 60 | beforeEach(async function () { 61 | await this.erc721a.mintTen(this.owner.address); 62 | await this.erc721a.mintOne(this.owner.address); 63 | 64 | await this.erc721a.mintTen(this.addr1.address); 65 | await this.erc721a.mintOne(this.addr1.address); 66 | }); 67 | 68 | it('transfer to and from two addresses', async function () { 69 | for (let i = 0; i < 2; ++i) { 70 | await this.erc721a.connect(this.owner).transferFrom(this.owner.address, this.addr1.address, 1); 71 | await this.erc721a.connect(this.addr1).transferFrom(this.addr1.address, this.owner.address, 1); 72 | } 73 | }); 74 | 75 | it('transferTen ascending order', async function () { 76 | await this.erc721a.connect(this.owner).transferTenAsc(this.addr1.address); 77 | }); 78 | 79 | it('transferTen descending order', async function () { 80 | await this.erc721a.connect(this.owner).transferTenDesc(this.addr1.address); 81 | }); 82 | 83 | it('transferTen average order', async function () { 84 | await this.erc721a.connect(this.owner).transferTenAvg(this.addr1.address); 85 | }); 86 | 87 | it('transferTen average order', async function () { 88 | await this.erc721a.connect(this.owner).transferTenAvg(this.addr1.address); 89 | }); 90 | }); 91 | 92 | context('batchTransferFromHundred', function () { 93 | beforeEach(async function () { 94 | await this.erc721a.mintHundred(this.owner.address); 95 | }); 96 | 97 | it('batchTransferFromHundred unoptimized', async function () { 98 | await this.erc721a.connect(this.owner).batchTransferHundredUnoptimized(this.addr1.address); 99 | }); 100 | 101 | it('batchTransferFromHundred optimized', async function () { 102 | await this.erc721a.connect(this.owner).batchTransferHundredOptimized(this.addr1.address); 103 | }); 104 | }); 105 | 106 | it('mintOneERC2309', async function () { 107 | // The following call `_mintERC3201` outside of contract creation. 108 | // This is non-compliant with the ERC721 standard, 109 | // and is only meant for gas comparisons. 110 | let args = ['Azuki', 'AZUKI', this.owner.address, 0, false]; 111 | let contract = await deployContract('ERC721AWithERC2309Mock', args); 112 | await contract.mintOneERC2309(this.owner.address); 113 | await contract.mintOneERC2309(this.owner.address); 114 | await contract.mintOneERC2309(this.addr1.address); 115 | }); 116 | 117 | it('mintTenERC2309', async function () { 118 | // The following call `_mintERC3201` outside of contract creation. 119 | // This is non-compliant with the ERC721 standard, 120 | // and is only meant for gas comparisons. 121 | let args = ['Azuki', 'AZUKI', this.owner.address, 0, false]; 122 | let contract = await deployContract('ERC721AWithERC2309Mock', args); 123 | await contract.mintTenERC2309(this.owner.address); 124 | await contract.mintTenERC2309(this.owner.address); 125 | await contract.mintTenERC2309(this.addr1.address); 126 | }); 127 | }); 128 | -------------------------------------------------------------------------------- /test/extensions/ERC4907A.test.js: -------------------------------------------------------------------------------- 1 | const { deployContract, getBlockTimestamp, mineBlockTimestamp } = require('../helpers.js'); 2 | const { expect } = require('chai'); 3 | const { constants } = require('@openzeppelin/test-helpers'); 4 | const { ZERO_ADDRESS } = constants; 5 | 6 | const createTestSuite = ({ contract, constructorArgs }) => 7 | function () { 8 | context(`${contract}`, function () { 9 | beforeEach(async function () { 10 | this.erc4097a = await deployContract(contract, constructorArgs); 11 | }); 12 | 13 | describe('EIP-165 support', async function () { 14 | it('supports ERC165', async function () { 15 | expect(await this.erc4097a.supportsInterface('0x01ffc9a7')).to.eq(true); 16 | }); 17 | 18 | it('supports IERC721', async function () { 19 | expect(await this.erc4097a.supportsInterface('0x80ac58cd')).to.eq(true); 20 | }); 21 | 22 | it('supports ERC721Metadata', async function () { 23 | expect(await this.erc4097a.supportsInterface('0x5b5e139f')).to.eq(true); 24 | }); 25 | 26 | it('supports ERC4907', async function () { 27 | expect(await this.erc4097a.supportsInterface('0xad092b5c')).to.eq(true); 28 | }); 29 | 30 | it('does not support random interface', async function () { 31 | expect(await this.erc4097a.supportsInterface('0x00000042')).to.eq(false); 32 | }); 33 | }); 34 | 35 | context('with minted tokens', async function () { 36 | beforeEach(async function () { 37 | const [owner, addr1] = await ethers.getSigners(); 38 | this.owner = owner; 39 | this.addr1 = addr1; 40 | 41 | await this.erc4097a['mint(address,uint256)'](this.owner.address, 1); 42 | await this.erc4097a['mint(address,uint256)'](this.addr1.address, 2); 43 | 44 | this.expires = (await getBlockTimestamp()) + 123; 45 | this.tokenId = 2; 46 | this.user = this.owner; 47 | }); 48 | 49 | it('explicitUserOf returns zero address after minting', async function () { 50 | expect(await this.erc4097a.explicitUserOf(0)).to.equal(ZERO_ADDRESS); 51 | expect(await this.erc4097a.explicitUserOf(1)).to.equal(ZERO_ADDRESS); 52 | expect(await this.erc4097a.explicitUserOf(2)).to.equal(ZERO_ADDRESS); 53 | }); 54 | 55 | it('userOf returns zero address after minting', async function () { 56 | expect(await this.erc4097a.userOf(0)).to.equal(ZERO_ADDRESS); 57 | expect(await this.erc4097a.userOf(1)).to.equal(ZERO_ADDRESS); 58 | expect(await this.erc4097a.userOf(2)).to.equal(ZERO_ADDRESS); 59 | }); 60 | 61 | it('userExpires returns zero timestamp after minting', async function () { 62 | expect(await this.erc4097a.userExpires(0)).to.equal(0); 63 | expect(await this.erc4097a.userExpires(1)).to.equal(0); 64 | expect(await this.erc4097a.userExpires(2)).to.equal(0); 65 | }); 66 | 67 | describe('setUser', async function () { 68 | beforeEach(async function () { 69 | this.setUser = async () => await this.erc4097a.connect(this.addr1) 70 | .setUser(this.tokenId, this.user.address, this.expires); 71 | 72 | this.setupAuthTest = async () => { 73 | this.tokenId = 0; 74 | await expect(this.setUser()).to.be.revertedWith('SetUserCallerNotOwnerNorApproved'); 75 | }; 76 | }); 77 | 78 | it('correctly changes the return value of explicitUserOf', async function () { 79 | await this.setUser(); 80 | expect(await this.erc4097a.explicitUserOf(this.tokenId)).to.equal(this.user.address); 81 | }); 82 | 83 | it('correctly changes the return value of userOf', async function () { 84 | await this.setUser(); 85 | expect(await this.erc4097a.userOf(this.tokenId)).to.equal(this.user.address); 86 | }); 87 | 88 | it('correctly changes the return value of expires', async function () { 89 | await this.setUser(); 90 | expect(await this.erc4097a.userExpires(this.tokenId)).to.equal(this.expires); 91 | }); 92 | 93 | it('emits the UpdateUser event properly', async function () { 94 | await expect(await this.setUser()) 95 | .to.emit(this.erc4097a, 'UpdateUser') 96 | .withArgs(this.tokenId, this.user.address, this.expires); 97 | }); 98 | 99 | it('reverts for an invalid token', async function () { 100 | this.tokenId = 123; 101 | await expect(this.setUser()).to.be.revertedWith('OwnerQueryForNonexistentToken'); 102 | }); 103 | 104 | it('requires token ownership', async function () { 105 | await this.setupAuthTest(); 106 | await this.erc4097a.transferFrom(this.owner.address, this.addr1.address, this.tokenId); 107 | await this.setUser(); 108 | }); 109 | 110 | it('requires token approval', async function () { 111 | await this.setupAuthTest(); 112 | await this.erc4097a.approve(this.addr1.address, this.tokenId); 113 | await this.setUser(); 114 | }); 115 | 116 | it('requires operator approval', async function () { 117 | await this.setupAuthTest(); 118 | await this.erc4097a.setApprovalForAll(this.addr1.address, 1); 119 | await this.setUser(); 120 | }); 121 | }); 122 | 123 | describe('after expiry', async function () { 124 | beforeEach(async function () { 125 | await this.erc4097a.connect(this.addr1) 126 | .setUser(this.tokenId, this.user.address, this.expires); 127 | }); 128 | 129 | it('userOf returns zero address after expires', async function () { 130 | expect(await this.erc4097a.userOf(this.tokenId)).to.equal(this.user.address); 131 | await mineBlockTimestamp(this.expires); 132 | expect(await this.erc4097a.userOf(this.tokenId)).to.equal(this.user.address); 133 | await mineBlockTimestamp(this.expires + 1); 134 | expect(await this.erc4097a.userOf(this.tokenId)).to.equal(ZERO_ADDRESS); 135 | }); 136 | 137 | it('explicitUserOf returns correct address after expiry', async function () { 138 | expect(await this.erc4097a.explicitUserOf(this.tokenId)).to.equal(this.user.address); 139 | await mineBlockTimestamp(this.expires); 140 | expect(await this.erc4097a.explicitUserOf(this.tokenId)).to.equal(this.user.address); 141 | await mineBlockTimestamp(this.expires + 1); 142 | expect(await this.erc4097a.explicitUserOf(this.tokenId)).to.equal(this.user.address); 143 | }); 144 | }); 145 | }); 146 | }); 147 | }; 148 | 149 | describe( 150 | 'ERC4907A', 151 | createTestSuite({ 152 | contract: 'ERC4907AMock', 153 | constructorArgs: ['Azuki', 'AZUKI'], 154 | }) 155 | ); 156 | -------------------------------------------------------------------------------- /test/extensions/ERC721ABurnable.test.js: -------------------------------------------------------------------------------- 1 | const { deployContract, getBlockTimestamp, mineBlockTimestamp, offsettedIndex } = require('../helpers.js'); 2 | const { expect } = require('chai'); 3 | const { constants } = require('@openzeppelin/test-helpers'); 4 | const { ZERO_ADDRESS } = constants; 5 | 6 | const createTestSuite = ({ contract, constructorArgs }) => 7 | function () { 8 | let offsetted; 9 | 10 | context(`${contract}`, function () { 11 | beforeEach(async function () { 12 | this.erc721aBurnable = await deployContract(contract, constructorArgs); 13 | 14 | this.startTokenId = this.erc721aBurnable.startTokenId 15 | ? (await this.erc721aBurnable.startTokenId()).toNumber() 16 | : 0; 17 | 18 | offsetted = (...arr) => offsettedIndex(this.startTokenId, arr); 19 | }); 20 | 21 | beforeEach(async function () { 22 | const [owner, addr1, addr2, spender] = await ethers.getSigners(); 23 | this.owner = owner; 24 | this.addr1 = addr1; 25 | this.addr2 = addr2; 26 | this.spender = spender; 27 | this.numTestTokens = 10; 28 | this.burnedTokenId = 5; 29 | this.notBurnedTokenId = 6; 30 | await this.erc721aBurnable['safeMint(address,uint256)'](this.addr1.address, this.numTestTokens); 31 | await this.erc721aBurnable.connect(this.addr1).burn(this.burnedTokenId); 32 | }); 33 | 34 | context('totalSupply()', function () { 35 | it('has the expected value', async function () { 36 | expect(await this.erc721aBurnable.totalSupply()).to.equal(9); 37 | }); 38 | 39 | it('is reduced by burns', async function () { 40 | const supplyBefore = await this.erc721aBurnable.totalSupply(); 41 | 42 | for (let i = 0; i < offsetted(2); ++i) { 43 | await this.erc721aBurnable.connect(this.addr1).burn(offsetted(i)); 44 | 45 | const supplyNow = await this.erc721aBurnable.totalSupply(); 46 | expect(supplyNow).to.equal(supplyBefore - (i + 1)); 47 | } 48 | }); 49 | }); 50 | 51 | it('changes numberBurned', async function () { 52 | expect(await this.erc721aBurnable.numberBurned(this.addr1.address)).to.equal(1); 53 | await this.erc721aBurnable.connect(this.addr1).burn(offsetted(0)); 54 | expect(await this.erc721aBurnable.numberBurned(this.addr1.address)).to.equal(2); 55 | }); 56 | 57 | it('changes totalBurned', async function () { 58 | const totalBurnedBefore = (await this.erc721aBurnable.totalBurned()).toNumber(); 59 | expect(totalBurnedBefore).to.equal(1); 60 | 61 | for (let i = 0; i < offsetted(2); ++i) { 62 | await this.erc721aBurnable.connect(this.addr1).burn(offsetted(i)); 63 | 64 | const totalBurnedNow = (await this.erc721aBurnable.totalBurned()).toNumber(); 65 | expect(totalBurnedNow).to.equal(totalBurnedBefore + (i + 1)); 66 | } 67 | }); 68 | 69 | it('changes exists', async function () { 70 | expect(await this.erc721aBurnable.exists(this.burnedTokenId)).to.be.false; 71 | expect(await this.erc721aBurnable.exists(this.notBurnedTokenId)).to.be.true; 72 | }); 73 | 74 | it('cannot burn a non-existing token', async function () { 75 | const query = this.erc721aBurnable.connect(this.addr1).burn(offsetted(this.numTestTokens)); 76 | await expect(query).to.be.revertedWith('OwnerQueryForNonexistentToken'); 77 | }); 78 | 79 | it('cannot burn a burned token', async function () { 80 | const query = this.erc721aBurnable.connect(this.addr1).burn(this.burnedTokenId); 81 | await expect(query).to.be.revertedWith('OwnerQueryForNonexistentToken'); 82 | }); 83 | 84 | it('cannot burn with wrong caller or spender', async function () { 85 | const tokenIdToBurn = this.notBurnedTokenId; 86 | 87 | // sanity check 88 | await this.erc721aBurnable.connect(this.addr1).approve(ZERO_ADDRESS, tokenIdToBurn); 89 | await this.erc721aBurnable.connect(this.addr1).setApprovalForAll(this.spender.address, false); 90 | 91 | const query = this.erc721aBurnable.connect(this.spender).burn(tokenIdToBurn); 92 | await expect(query).to.be.revertedWith('TransferCallerNotOwnerNorApproved'); 93 | }); 94 | 95 | it('spender can burn with specific approved tokenId', async function () { 96 | const tokenIdToBurn = this.notBurnedTokenId; 97 | 98 | await this.erc721aBurnable.connect(this.addr1).approve(this.spender.address, tokenIdToBurn); 99 | await this.erc721aBurnable.connect(this.spender).burn(tokenIdToBurn); 100 | expect(await this.erc721aBurnable.exists(tokenIdToBurn)).to.be.false; 101 | }); 102 | 103 | it('spender can burn with one-time approval', async function () { 104 | const tokenIdToBurn = this.notBurnedTokenId; 105 | 106 | await this.erc721aBurnable.connect(this.addr1).setApprovalForAll(this.spender.address, true); 107 | await this.erc721aBurnable.connect(this.spender).burn(tokenIdToBurn); 108 | expect(await this.erc721aBurnable.exists(tokenIdToBurn)).to.be.false; 109 | }); 110 | 111 | it('cannot transfer a burned token', async function () { 112 | const query = this.erc721aBurnable 113 | .connect(this.addr1) 114 | .transferFrom(this.addr1.address, this.addr2.address, this.burnedTokenId); 115 | await expect(query).to.be.revertedWith('OwnerQueryForNonexistentToken'); 116 | }); 117 | 118 | it('does not affect _totalMinted', async function () { 119 | const totalMintedBefore = await this.erc721aBurnable.totalMinted(); 120 | expect(totalMintedBefore).to.equal(this.numTestTokens); 121 | for (let i = 0; i < 2; ++i) { 122 | await this.erc721aBurnable.connect(this.addr1).burn(offsetted(i)); 123 | } 124 | expect(await this.erc721aBurnable.totalMinted()).to.equal(totalMintedBefore); 125 | }); 126 | 127 | it('adjusts owners balances', async function () { 128 | expect(await this.erc721aBurnable.balanceOf(this.addr1.address)).to.be.equal(this.numTestTokens - 1); 129 | }); 130 | 131 | it('startTimestamp updated correctly', async function () { 132 | const tokenIdToBurn = this.burnedTokenId + 1; 133 | const ownershipBefore = await this.erc721aBurnable.getOwnershipAt(tokenIdToBurn); 134 | const timestampBefore = parseInt(ownershipBefore.startTimestamp); 135 | const timestampToMine = (await getBlockTimestamp()) + 12345; 136 | await mineBlockTimestamp(timestampToMine); 137 | const timestampMined = await getBlockTimestamp(); 138 | await this.erc721aBurnable.connect(this.addr1).burn(tokenIdToBurn); 139 | const ownershipAfter = await this.erc721aBurnable.getOwnershipAt(tokenIdToBurn); 140 | const timestampAfter = parseInt(ownershipAfter.startTimestamp); 141 | expect(timestampBefore).to.be.lt(timestampToMine); 142 | expect(timestampAfter).to.be.gte(timestampToMine); 143 | expect(timestampAfter).to.be.lt(timestampToMine + 10); 144 | expect(timestampToMine).to.be.eq(timestampMined); 145 | }); 146 | 147 | describe('ownerships correctly set', async function () { 148 | it('with token before previously burnt token transferred and burned', async function () { 149 | const tokenIdToBurn = this.burnedTokenId - 1; 150 | await this.erc721aBurnable 151 | .connect(this.addr1) 152 | .transferFrom(this.addr1.address, this.addr2.address, tokenIdToBurn); 153 | expect(await this.erc721aBurnable.ownerOf(tokenIdToBurn)).to.be.equal(this.addr2.address); 154 | await this.erc721aBurnable.connect(this.addr2).burn(tokenIdToBurn); 155 | for (let i = offsetted(0); i < offsetted(this.numTestTokens); ++i) { 156 | if (i == tokenIdToBurn || i == this.burnedTokenId) { 157 | await expect(this.erc721aBurnable.ownerOf(i)).to.be.revertedWith('OwnerQueryForNonexistentToken'); 158 | } else { 159 | expect(await this.erc721aBurnable.ownerOf(i)).to.be.equal(this.addr1.address); 160 | } 161 | } 162 | }); 163 | 164 | it('with token after previously burnt token transferred and burned', async function () { 165 | const tokenIdToBurn = this.burnedTokenId + 1; 166 | await this.erc721aBurnable 167 | .connect(this.addr1) 168 | .transferFrom(this.addr1.address, this.addr2.address, tokenIdToBurn); 169 | expect(await this.erc721aBurnable.ownerOf(tokenIdToBurn)).to.be.equal(this.addr2.address); 170 | await this.erc721aBurnable.connect(this.addr2).burn(tokenIdToBurn); 171 | for (let i = offsetted(0); i < offsetted(this.numTestTokens); ++i) { 172 | if (i == tokenIdToBurn || i == this.burnedTokenId) { 173 | await expect(this.erc721aBurnable.ownerOf(i)).to.be.revertedWith('OwnerQueryForNonexistentToken'); 174 | } else { 175 | expect(await this.erc721aBurnable.ownerOf(i)).to.be.equal(this.addr1.address); 176 | } 177 | } 178 | }); 179 | 180 | it('with first token burned', async function () { 181 | await this.erc721aBurnable.connect(this.addr1).burn(offsetted(0)); 182 | for (let i = offsetted(0); i < offsetted(this.numTestTokens); ++i) { 183 | if (i == offsetted(0).toNumber() || i == this.burnedTokenId) { 184 | await expect(this.erc721aBurnable.ownerOf(i)).to.be.revertedWith('OwnerQueryForNonexistentToken'); 185 | } else { 186 | expect(await this.erc721aBurnable.ownerOf(i)).to.be.equal(this.addr1.address); 187 | } 188 | } 189 | }); 190 | 191 | it('with last token burned', async function () { 192 | await expect(this.erc721aBurnable.ownerOf(offsetted(this.numTestTokens))).to.be.revertedWith( 193 | 'OwnerQueryForNonexistentToken' 194 | ); 195 | await this.erc721aBurnable.connect(this.addr1).burn(offsetted(this.numTestTokens - 1)); 196 | await expect(this.erc721aBurnable.ownerOf(offsetted(this.numTestTokens - 1))).to.be.revertedWith( 197 | 'OwnerQueryForNonexistentToken' 198 | ); 199 | }); 200 | }); 201 | }); 202 | }; 203 | 204 | describe('ERC721ABurnable', createTestSuite({ contract: 'ERC721ABurnableMock', constructorArgs: ['Azuki', 'AZUKI'] })); 205 | 206 | describe( 207 | 'ERC721ABurnable override _startTokenId()', 208 | createTestSuite({ contract: 'ERC721ABurnableStartTokenIdMock', constructorArgs: ['Azuki', 'AZUKI', 1] }) 209 | ); 210 | -------------------------------------------------------------------------------- /test/extensions/ERC721AQueryable.test.js: -------------------------------------------------------------------------------- 1 | const { deployContract, offsettedIndex } = require('../helpers.js'); 2 | const { expect } = require('chai'); 3 | const { BigNumber } = require('ethers'); 4 | const { constants } = require('@openzeppelin/test-helpers'); 5 | const { ZERO_ADDRESS } = constants; 6 | 7 | const createTestSuite = ({ contract, constructorArgs }) => 8 | function () { 9 | let offsetted; 10 | 11 | context(`${contract}`, function () { 12 | beforeEach(async function () { 13 | this.erc721aQueryable = await deployContract(contract, constructorArgs); 14 | 15 | this.startTokenId = this.erc721aQueryable.startTokenId 16 | ? (await this.erc721aQueryable.startTokenId()).toNumber() 17 | : 0; 18 | 19 | offsetted = (...arr) => offsettedIndex(this.startTokenId, arr); 20 | }); 21 | 22 | const expectExplicitOwnershipBurned = function (explicitOwnership, address) { 23 | expect(explicitOwnership.burned).to.eql(true); 24 | expect(explicitOwnership.addr).to.eql(address); 25 | expect(explicitOwnership.startTimestamp).to.not.eql(BigNumber.from(0)); 26 | expect(explicitOwnership.extraData).to.equal(BigNumber.from(0)); 27 | }; 28 | 29 | const expectExplicitOwnershipNotExists = function (explicitOwnership) { 30 | expect(explicitOwnership.burned).to.eql(false); 31 | expect(explicitOwnership.addr).to.eql(ZERO_ADDRESS); 32 | expect(explicitOwnership.startTimestamp).to.eql(BigNumber.from(0)); 33 | expect(explicitOwnership.extraData).to.equal(BigNumber.from(0)); 34 | }; 35 | 36 | const expectExplicitOwnershipExists = function (explicitOwnership, address) { 37 | expect(explicitOwnership.burned).to.eql(false); 38 | expect(explicitOwnership.addr).to.eql(address); 39 | expect(explicitOwnership.startTimestamp).to.not.eql(BigNumber.from(0)); 40 | expect(explicitOwnership.extraData).to.equal(BigNumber.from(0)); 41 | }; 42 | 43 | context('with no minted tokens', async function () { 44 | beforeEach(async function () { 45 | const [owner, addr1] = await ethers.getSigners(); 46 | this.owner = owner; 47 | this.addr1 = addr1; 48 | }); 49 | 50 | describe('tokensOfOwner', async function () { 51 | it('returns empty array', async function () { 52 | expect(await this.erc721aQueryable.tokensOfOwner(this.owner.address)).to.eql([]); 53 | expect(await this.erc721aQueryable.tokensOfOwner(this.addr1.address)).to.eql([]); 54 | }); 55 | }); 56 | 57 | describe('tokensOfOwnerIn', async function () { 58 | it('returns empty array', async function () { 59 | expect(await this.erc721aQueryable.tokensOfOwnerIn(this.owner.address, 0, 9)).to.eql([]); 60 | expect(await this.erc721aQueryable.tokensOfOwnerIn(this.addr1.address, 0, 9)).to.eql([]); 61 | }); 62 | }); 63 | 64 | describe('explicitOwnershipOf', async function () { 65 | it('returns empty struct', async function () { 66 | expectExplicitOwnershipNotExists(await this.erc721aQueryable.explicitOwnershipOf(0)); 67 | expectExplicitOwnershipNotExists(await this.erc721aQueryable.explicitOwnershipOf(1)); 68 | }); 69 | }); 70 | 71 | describe('explicitOwnershipsOf', async function () { 72 | it('returns empty structs', async function () { 73 | const tokenIds = [0, 1, 2, 3]; 74 | const explicitOwnerships = await this.erc721aQueryable.explicitOwnershipsOf(tokenIds); 75 | for (let i = 0; i < explicitOwnerships.length; ++i) { 76 | expectExplicitOwnershipNotExists(explicitOwnerships[i]); 77 | } 78 | }); 79 | }); 80 | }); 81 | 82 | context('with minted tokens', async function () { 83 | beforeEach(async function () { 84 | const [owner, addr1, addr2, addr3, addr4] = await ethers.getSigners(); 85 | this.owner = owner; 86 | this.addr1 = addr1; 87 | this.addr2 = addr2; 88 | this.addr3 = addr3; 89 | this.addr4 = addr4; 90 | 91 | this.addr1.expected = { 92 | balance: 1, 93 | tokens: [offsetted(0)], 94 | }; 95 | 96 | this.addr2.expected = { 97 | balance: 2, 98 | tokens: offsetted(1, 2), 99 | }; 100 | 101 | this.addr3.expected = { 102 | balance: 3, 103 | tokens: offsetted(3, 4, 5), 104 | }; 105 | 106 | this.addr4.expected = { 107 | balance: 0, 108 | tokens: [], 109 | }; 110 | 111 | this.owner.expected = { 112 | balance: 3, 113 | tokens: offsetted(6, 7, 8), 114 | }; 115 | 116 | this.lastTokenId = offsetted(8); 117 | this.currentIndex = this.lastTokenId.add(1); 118 | 119 | this.mintOrder = [this.addr1, this.addr2, this.addr3, this.addr4, owner]; 120 | 121 | for (const minter of this.mintOrder) { 122 | const balance = minter.expected.balance; 123 | if (balance > 0) { 124 | await this.erc721aQueryable['safeMint(address,uint256)'](minter.address, balance); 125 | } 126 | // sanity check 127 | expect(await this.erc721aQueryable.balanceOf(minter.address)).to.equal(minter.expected.balance); 128 | } 129 | }); 130 | 131 | describe('tokensOfOwner', async function () { 132 | it('initial', async function () { 133 | for (const minter of this.mintOrder) { 134 | const tokens = await this.erc721aQueryable.tokensOfOwner(minter.address); 135 | expect(tokens).to.eql(minter.expected.tokens); 136 | } 137 | }); 138 | 139 | it('after a transfer', async function () { 140 | // Break sequential order by transfering 7th token from owner to addr4 141 | const tokenIdToTransfer = [offsetted(7)]; 142 | await this.erc721aQueryable.transferFrom(this.owner.address, this.addr4.address, tokenIdToTransfer[0]); 143 | 144 | // Load balances 145 | const ownerTokens = await this.erc721aQueryable.tokensOfOwner(this.owner.address); 146 | const addr4Tokens = await this.erc721aQueryable.tokensOfOwner(this.addr4.address); 147 | 148 | // Verify the function can still read the correct token ids 149 | expect(ownerTokens).to.eql(offsetted(6, 8)); 150 | expect(addr4Tokens).to.eql(tokenIdToTransfer); 151 | }); 152 | 153 | it('after a burn', async function () { 154 | // Burn tokens 155 | const tokenIdToBurn = [offsetted(7)]; 156 | await this.erc721aQueryable.burn(tokenIdToBurn[0]); 157 | 158 | // Load balances 159 | const ownerTokens = await this.erc721aQueryable.tokensOfOwner(this.owner.address); 160 | 161 | // Verify the function can still read the correct token ids 162 | expect(ownerTokens).to.eql(offsetted(6, 8)); 163 | }); 164 | 165 | it('with direct set burn bit', async function () { 166 | await this.erc721aQueryable['safeMint(address,uint256)'](this.addr3.address, 3); 167 | await this.erc721aQueryable['safeMint(address,uint256)'](this.addr3.address, 2); 168 | const nextTokenId = this.owner.expected.tokens[this.owner.expected.tokens.length - 1].add(1); 169 | expect(await this.erc721aQueryable.tokensOfOwner(this.addr3.address)) 170 | .to.eql(this.addr3.expected.tokens.concat(Array.from({length: 5}, (_, i) => nextTokenId.add(i)))); 171 | await this.erc721aQueryable.directSetBurnBit(nextTokenId); 172 | expect(await this.erc721aQueryable.tokensOfOwner(this.addr3.address)) 173 | .to.eql(this.addr3.expected.tokens.concat(Array.from({length: 2}, (_, i) => nextTokenId.add(3 + i)))); 174 | }); 175 | }); 176 | 177 | describe('tokensOfOwnerIn', async function () { 178 | const expectCorrect = async function (addr, start, stop) { 179 | if (BigNumber.from(start).gte(BigNumber.from(stop))) { 180 | await expect(this.erc721aQueryable.tokensOfOwnerIn(addr, start, stop)).to.be.revertedWith( 181 | 'InvalidQueryRange' 182 | ); 183 | } else { 184 | const expectedTokens = (await this.erc721aQueryable.tokensOfOwner(addr)).filter( 185 | (x) => BigNumber.from(start).lte(x) && BigNumber.from(stop).gt(x) 186 | ); 187 | const tokens = await this.erc721aQueryable.tokensOfOwnerIn(addr, start, stop); 188 | expect(tokens).to.eql(expectedTokens); 189 | } 190 | }; 191 | 192 | const subTests = function (description, beforeEachFunction) { 193 | describe(description, async function () { 194 | it('all token ids', async function () { 195 | await beforeEachFunction.call(this); 196 | await expectCorrect.call(this, this.owner.address, offsetted(0), this.currentIndex); 197 | await expectCorrect.call(this, this.owner.address, offsetted(0), this.currentIndex.add(1)); 198 | }); 199 | 200 | it('partial token ids', async function () { 201 | await beforeEachFunction.call(this); 202 | const ownerTokens = this.owner.expected.tokens; 203 | const start = ownerTokens[0]; 204 | const stop = ownerTokens[ownerTokens.length - 1] + 1; 205 | for (let o = 1; o <= ownerTokens.length; ++o) { 206 | // Start truncated. 207 | await expectCorrect.call(this, this.owner.address, start + o, stop); 208 | // End truncated. 209 | await expectCorrect.call(this, this.owner.address, start, stop - o); 210 | // Start and end truncated. This also tests for start + o >= stop - o. 211 | await expectCorrect.call(this, this.owner.address, start + o, stop - o); 212 | } 213 | for (let l = 0; l < ownerTokens.length; ++l) { 214 | for (let o = 0, n = parseInt(this.currentIndex) + 1; o <= n; ++o) { 215 | // Sliding window. 216 | await expectCorrect.call(this, this.owner.address, o, o + l); 217 | } 218 | } 219 | }); 220 | }); 221 | }; 222 | 223 | subTests('initial', async function () {}); 224 | 225 | subTests('after a token tranfer', async function () { 226 | await this.erc721aQueryable.transferFrom(this.owner.address, this.addr4.address, offsetted(7)); 227 | }); 228 | 229 | subTests('after a token burn', async function () { 230 | await this.erc721aQueryable.burn(offsetted(7)); 231 | }); 232 | 233 | it('with direct set burn bit', async function () { 234 | await this.erc721aQueryable['safeMint(address,uint256)'](this.addr3.address, 3); 235 | await this.erc721aQueryable['safeMint(address,uint256)'](this.addr3.address, 2); 236 | const nextTokenId = this.owner.expected.tokens[this.owner.expected.tokens.length - 1].add(1); 237 | expect(await this.erc721aQueryable.tokensOfOwnerIn(this.addr3.address, 0, 99)) 238 | .to.eql(this.addr3.expected.tokens.concat(Array.from({length: 5}, (_, i) => nextTokenId.add(i)))); 239 | await this.erc721aQueryable.directSetBurnBit(nextTokenId); 240 | expect(await this.erc721aQueryable.tokensOfOwnerIn(this.addr3.address, 0, 99)) 241 | .to.eql(this.addr3.expected.tokens.concat(Array.from({length: 2}, (_, i) => nextTokenId.add(3 + i)))); 242 | }) 243 | }); 244 | 245 | describe('explicitOwnershipOf', async function () { 246 | it('token exists', async function () { 247 | const tokenId = this.owner.expected.tokens[0]; 248 | const explicitOwnership = await this.erc721aQueryable.explicitOwnershipOf(tokenId); 249 | expectExplicitOwnershipExists(explicitOwnership, this.owner.address); 250 | }); 251 | 252 | it('after a token burn', async function () { 253 | const tokenId = this.owner.expected.tokens[0]; 254 | await this.erc721aQueryable.burn(tokenId); 255 | const explicitOwnership = await this.erc721aQueryable.explicitOwnershipOf(tokenId); 256 | expectExplicitOwnershipBurned(explicitOwnership, this.owner.address); 257 | }); 258 | 259 | it('after a token transfer', async function () { 260 | const tokenId = this.owner.expected.tokens[0]; 261 | await this.erc721aQueryable.transferFrom(this.owner.address, this.addr4.address, tokenId); 262 | const explicitOwnership = await this.erc721aQueryable.explicitOwnershipOf(tokenId); 263 | expectExplicitOwnershipExists(explicitOwnership, this.addr4.address); 264 | }); 265 | 266 | it('out of bounds', async function () { 267 | const explicitOwnership = await this.erc721aQueryable.explicitOwnershipOf(this.currentIndex); 268 | expectExplicitOwnershipNotExists(explicitOwnership); 269 | }); 270 | 271 | it('with direct set burn bit', async function () { 272 | await this.erc721aQueryable.directSetBurnBit(this.addr3.expected.tokens[0]); 273 | for (let i = 0; i < this.addr3.expected.tokens.length; ++i) { 274 | const explicitOwnership = await this.erc721aQueryable.explicitOwnershipOf(this.addr3.expected.tokens[i]); 275 | expectExplicitOwnershipBurned(explicitOwnership, this.addr3.address); 276 | } 277 | }); 278 | }); 279 | 280 | describe('explicitOwnershipsOf', async function () { 281 | it('tokens exist', async function () { 282 | const tokenIds = [].concat(this.owner.expected.tokens, this.addr3.expected.tokens); 283 | const explicitOwnerships = await this.erc721aQueryable.explicitOwnershipsOf(tokenIds); 284 | for (let i = 0; i < tokenIds.length; ++i) { 285 | const owner = await this.erc721aQueryable.ownerOf(tokenIds[i]); 286 | expectExplicitOwnershipExists(explicitOwnerships[i], owner); 287 | } 288 | }); 289 | 290 | it('after a token burn', async function () { 291 | const tokenIds = [].concat(this.owner.expected.tokens, this.addr3.expected.tokens); 292 | await this.erc721aQueryable.burn(tokenIds[0]); 293 | const explicitOwnerships = await this.erc721aQueryable.explicitOwnershipsOf(tokenIds); 294 | expectExplicitOwnershipBurned(explicitOwnerships[0], this.owner.address); 295 | for (let i = 1; i < tokenIds.length; ++i) { 296 | const owner = await this.erc721aQueryable.ownerOf(tokenIds[i]); 297 | expectExplicitOwnershipExists(explicitOwnerships[i], owner); 298 | } 299 | }); 300 | 301 | it('after a token transfer', async function () { 302 | const tokenIds = [].concat(this.owner.expected.tokens, this.addr3.expected.tokens); 303 | await this.erc721aQueryable.transferFrom(this.owner.address, this.addr4.address, tokenIds[0]); 304 | const explicitOwnerships = await this.erc721aQueryable.explicitOwnershipsOf(tokenIds); 305 | expectExplicitOwnershipExists(explicitOwnerships[0], this.addr4.address); 306 | for (let i = 1; i < tokenIds.length; ++i) { 307 | const owner = await this.erc721aQueryable.ownerOf(tokenIds[i]); 308 | expectExplicitOwnershipExists(explicitOwnerships[i], owner); 309 | } 310 | }); 311 | 312 | it('out of bounds', async function () { 313 | const tokenIds = [].concat([this.currentIndex], this.addr3.expected.tokens); 314 | const explicitOwnerships = await this.erc721aQueryable.explicitOwnershipsOf(tokenIds); 315 | expectExplicitOwnershipNotExists(explicitOwnerships[0]); 316 | for (let i = 1; i < tokenIds.length; ++i) { 317 | const owner = await this.erc721aQueryable.ownerOf(tokenIds[i]); 318 | expectExplicitOwnershipExists(explicitOwnerships[i], owner); 319 | } 320 | }); 321 | }); 322 | }); 323 | }); 324 | }; 325 | 326 | describe( 327 | 'ERC721AQueryable', 328 | createTestSuite({ 329 | contract: 'ERC721AQueryableMock', 330 | constructorArgs: ['Azuki', 'AZUKI'], 331 | }) 332 | ); 333 | 334 | describe( 335 | 'ERC721AQueryable override _startTokenId()', 336 | createTestSuite({ 337 | contract: 'ERC721AQueryableStartTokenIdMock', 338 | constructorArgs: ['Azuki', 'AZUKI', 1], 339 | }) 340 | ); 341 | -------------------------------------------------------------------------------- /test/extensions/ERC721ASpot.test.js: -------------------------------------------------------------------------------- 1 | const { deployContract } = require('../helpers.js'); 2 | const { expect } = require('chai'); 3 | const { BigNumber } = require('ethers'); 4 | const { constants } = require('@openzeppelin/test-helpers'); 5 | const { ZERO_ADDRESS } = constants; 6 | 7 | describe('ERC721ASpot', function () { 8 | 9 | context('constructor', function () { 10 | const testConstructor = async (args, expectedError) => { 11 | const deployment = deployContract('ERC721ASpotMock', args); 12 | if (expectedError) await expect(deployment).to.be.revertedWith(expectedError); 13 | else await deployment; 14 | }; 15 | 16 | it('reverts if _sequentialUpTo is not greater than _startTokenId', async function () { 17 | const t = async (startTokenId, sequentialUpTo, expectSuccess) => { 18 | await testConstructor( 19 | ['Azuki', 'AZUKI', startTokenId, sequentialUpTo, 0, false], 20 | expectSuccess ? false : 'SequentialUpToTooSmall' 21 | ); 22 | }; 23 | await t(0, 0, true); 24 | await t(1, 0, false); 25 | await t(0, 1, true); 26 | await t(100, 99, false); 27 | await t(100, 100, true); 28 | await t(100, 101, true); 29 | await t(100, 999, true); 30 | }); 31 | 32 | it('reverts if ERC2309 mint exceeds limit', async function () { 33 | const t = async (startTokenId, sequentialUpTo, quantity, expectSuccess) => { 34 | await testConstructor( 35 | ['Azuki', 'AZUKI', startTokenId, sequentialUpTo, quantity, true], 36 | expectSuccess ? false : 'SequentialMintExceedsLimit' 37 | ); 38 | }; 39 | await t(0, 1, 1, true); 40 | await t(0, 1, 2, true); 41 | await t(0, 1, 3, false); 42 | await t(100, 101, 1, true); 43 | await t(100, 101, 2, true); 44 | await t(100, 101, 3, false); 45 | await t(100, 109, 2, true); 46 | await t(100, 109, 9, true); 47 | await t(100, 109, 10, true); 48 | await t(100, 109, 11, false); 49 | }); 50 | }); 51 | 52 | context('mint sequential and spot', function () { 53 | beforeEach(async function () { 54 | const [owner, addr1] = await ethers.getSigners(); 55 | this.owner = owner; 56 | this.addr1 = addr1; 57 | this.startTokenId = BigNumber.from(10); 58 | this.sequentialUpTo = BigNumber.from(19); 59 | const args = ['Azuki', 'AZUKI', this.startTokenId, this.sequentialUpTo, 0, false]; 60 | this.erc721aSpot = await deployContract('ERC721ASpotMock', args); 61 | }); 62 | 63 | it('_mintSpot emits a Transfer event', async function () { 64 | await expect(this.erc721aSpot.safeMintSpot(this.addr1.address, 20)) 65 | .to.emit(this.erc721aSpot, 'Transfer') 66 | .withArgs(ZERO_ADDRESS, this.addr1.address, 20); 67 | }); 68 | 69 | it('increases _totalSpotMinted, totalSupply', async function () { 70 | await this.erc721aSpot.safeMint(this.addr1.address, 5); 71 | expect(await this.erc721aSpot.totalSpotMinted()).to.eq(0); 72 | expect(await this.erc721aSpot.totalSupply()).to.eq(5); 73 | 74 | await this.erc721aSpot.safeMintSpot(this.addr1.address, 20); 75 | expect(await this.erc721aSpot.totalSpotMinted()).to.eq(1); 76 | expect(await this.erc721aSpot.totalSupply()).to.eq(6); 77 | 78 | await this.erc721aSpot.safeMintSpot(this.addr1.address, 30); 79 | expect(await this.erc721aSpot.totalSpotMinted()).to.eq(2); 80 | expect(await this.erc721aSpot.totalSupply()).to.eq(7); 81 | }); 82 | 83 | it('tokensOfOwnerIn', async function () { 84 | expect(await this.erc721aSpot.tokensOfOwnerIn(this.addr1.address, 0, 4294967295)).to.eql([]); 85 | 86 | await this.erc721aSpot.safeMint(this.addr1.address, 5); 87 | expect(await this.erc721aSpot.tokensOfOwnerIn(this.addr1.address, 0, 4294967295)) 88 | .to.eql([10, 11, 12, 13, 14].map(BigNumber.from)); 89 | 90 | await this.erc721aSpot.safeMintSpot(this.addr1.address, 21); 91 | expect(await this.erc721aSpot.tokensOfOwnerIn(this.addr1.address, 0, 4294967295)) 92 | .to.eql([10, 11, 12, 13, 14, 21].map(BigNumber.from)); 93 | 94 | await this.erc721aSpot.safeMintSpot(this.addr1.address, 31); 95 | expect(await this.erc721aSpot.tokensOfOwnerIn(this.addr1.address, 0, 4294967295)) 96 | .to.eql([10, 11, 12, 13, 14, 21, 31].map(BigNumber.from)); 97 | 98 | await this.erc721aSpot.safeMintSpot(this.addr1.address, 22); 99 | expect(await this.erc721aSpot.tokensOfOwnerIn(this.addr1.address, 0, 4294967295)) 100 | .to.eql([10, 11, 12, 13, 14, 21, 22, 31].map(BigNumber.from)); 101 | 102 | await this.erc721aSpot.safeMint(this.addr1.address, 5); 103 | expect(await this.erc721aSpot.tokensOfOwnerIn(this.addr1.address, 0, 4294967295)) 104 | .to.eql([10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 21, 22, 31].map(BigNumber.from)); 105 | 106 | await this.erc721aSpot.safeMintSpot(this.addr1.address, 20); 107 | expect(await this.erc721aSpot.tokensOfOwnerIn(this.addr1.address, 0, 4294967295)) 108 | .to.eql([10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 31].map(BigNumber.from)); 109 | 110 | expect(await this.erc721aSpot.tokensOfOwnerIn(this.addr1.address, 0, 32)) 111 | .to.eql([10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 31].map(BigNumber.from)); 112 | 113 | expect(await this.erc721aSpot.tokensOfOwnerIn(this.addr1.address, 0, 31)) 114 | .to.eql([10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22].map(BigNumber.from)); 115 | }); 116 | 117 | it('explicitOwnershipOf', async function () { 118 | let explicitOwnership = await this.erc721aSpot.explicitOwnershipOf(10); 119 | expect(explicitOwnership.addr).to.eq(ZERO_ADDRESS); 120 | expect(explicitOwnership.burned).to.eq(false); 121 | 122 | await this.erc721aSpot.safeMint(this.addr1.address, 1); 123 | explicitOwnership = await this.erc721aSpot.explicitOwnershipOf(10); 124 | expect(explicitOwnership.addr).to.eq(this.addr1.address); 125 | expect(explicitOwnership.burned).to.eq(false); 126 | 127 | explicitOwnership = await this.erc721aSpot.explicitOwnershipOf(11); 128 | expect(explicitOwnership.addr).to.eq(ZERO_ADDRESS); 129 | expect(explicitOwnership.burned).to.eq(false); 130 | 131 | explicitOwnership = await this.erc721aSpot.explicitOwnershipOf(20); 132 | expect(explicitOwnership.addr).to.eq(ZERO_ADDRESS); 133 | expect(explicitOwnership.burned).to.eq(false); 134 | 135 | await this.erc721aSpot.safeMintSpot(this.addr1.address, 20); 136 | explicitOwnership = await this.erc721aSpot.explicitOwnershipOf(20); 137 | expect(explicitOwnership.addr).to.eq(this.addr1.address); 138 | expect(explicitOwnership.burned).to.eq(false); 139 | }); 140 | 141 | it('tokensOfOwner reverts', async function () { 142 | await expect(this.erc721aSpot.tokensOfOwner(this.addr1.address)).to.be.revertedWith( 143 | 'NotCompatibleWithSpotMints' 144 | ); 145 | }); 146 | 147 | it('spot minting to an existing token reverts', async function () { 148 | await this.erc721aSpot.safeMintSpot(this.addr1.address, 20); 149 | await expect(this.erc721aSpot.safeMintSpot(this.addr1.address, 20)).to.be.revertedWith( 150 | 'TokenAlreadyExists' 151 | ); 152 | }); 153 | 154 | it('reverts if sequential mint exceeds limit', async function () { 155 | await expect(this.erc721aSpot.safeMint(this.addr1.address, 11)).to.be.revertedWith( 156 | 'SequentialMintExceedsLimit' 157 | ); 158 | await this.erc721aSpot.safeMint(this.addr1.address, 10); 159 | }); 160 | 161 | it('reverts if _mintSpot tokenId is too small', async function () { 162 | await expect(this.erc721aSpot.safeMintSpot(this.addr1.address, 19)).to.be.revertedWith( 163 | 'SpotMintTokenIdTooSmall' 164 | ); 165 | }); 166 | 167 | context('with transfers', function () { 168 | it('reverts if token is not minted', async function () { 169 | await this.erc721aSpot.safeMint(this.addr1.address, 10); 170 | await expect(this.erc721aSpot 171 | .connect(this.addr1) 172 | .transferFrom(this.addr1.address, this.owner.address, 21)).to.be.revertedWith( 173 | 'OwnerQueryForNonexistentToken' 174 | ); 175 | await this.erc721aSpot.safeMintSpot(this.addr1.address, 21); 176 | await this.erc721aSpot 177 | .connect(this.addr1) 178 | .transferFrom(this.addr1.address, this.owner.address, 21); 179 | }); 180 | 181 | it('edge case 1', async function () { 182 | await this.erc721aSpot.safeMintSpot(this.addr1.address, 20); 183 | await this.erc721aSpot.safeMint(this.addr1.address, 10); 184 | await this.erc721aSpot.connect(this.addr1).transferFrom(this.addr1.address, this.owner.address, 20); 185 | expect(await this.erc721aSpot.ownerOf(20)).to.eq(this.owner.address); 186 | expect(await this.erc721aSpot.ownerOf(19)).to.eq(this.addr1.address); 187 | expect(await this.erc721aSpot.ownerOf(18)).to.eq(this.addr1.address); 188 | await this.erc721aSpot.connect(this.addr1).transferFrom(this.addr1.address, this.owner.address, 19); 189 | expect(await this.erc721aSpot.ownerOf(20)).to.eq(this.owner.address); 190 | expect(await this.erc721aSpot.ownerOf(19)).to.eq(this.owner.address); 191 | expect(await this.erc721aSpot.ownerOf(18)).to.eq(this.addr1.address); 192 | }); 193 | 194 | it('edge case 2', async function () { 195 | await this.erc721aSpot.safeMintSpot(this.addr1.address, 20); 196 | await this.erc721aSpot.safeMint(this.addr1.address, 10); 197 | await this.erc721aSpot.connect(this.addr1).transferFrom(this.addr1.address, this.owner.address, 19); 198 | expect(await this.erc721aSpot.ownerOf(20)).to.eq(this.addr1.address); 199 | expect(await this.erc721aSpot.ownerOf(19)).to.eq(this.owner.address); 200 | expect(await this.erc721aSpot.ownerOf(18)).to.eq(this.addr1.address); 201 | await this.erc721aSpot.connect(this.addr1).transferFrom(this.addr1.address, this.owner.address, 20); 202 | expect(await this.erc721aSpot.ownerOf(20)).to.eq(this.owner.address); 203 | expect(await this.erc721aSpot.ownerOf(19)).to.eq(this.owner.address); 204 | expect(await this.erc721aSpot.ownerOf(18)).to.eq(this.addr1.address); 205 | }); 206 | }); 207 | 208 | context('with burns', function () { 209 | beforeEach(async function () { 210 | await this.erc721aSpot.safeMint(this.addr1.address, 5); 211 | await this.erc721aSpot.safeMintSpot(this.addr1.address, 20); 212 | await this.erc721aSpot.safeMintSpot(this.addr1.address, 30); 213 | }); 214 | 215 | it('sets ownership correctly', async function () { 216 | const t = async (tokenIds) => { 217 | for (let i = 0; i < 35; ++i) { 218 | const tx = this.erc721aSpot.getOwnershipOf(i); 219 | if (tokenIds.includes(i)) await tx; 220 | else await expect(tx).to.be.revertedWith('OwnerQueryForNonexistentToken'); 221 | } 222 | }; 223 | await t([10, 11, 12, 13, 14, 20, 30]); 224 | await this.erc721aSpot.connect(this.addr1).burn(20); 225 | await t([10, 11, 12, 13, 14, 30]); 226 | }); 227 | 228 | it('reduces balanceOf, totalSupply', async function () { 229 | expect(await this.erc721aSpot.balanceOf(this.addr1.address)).to.eq(7); 230 | await this.erc721aSpot.connect(this.addr1).burn(10); 231 | expect(await this.erc721aSpot.balanceOf(this.addr1.address)).to.eq(6); 232 | expect(await this.erc721aSpot.totalSupply()).to.eq(6); 233 | 234 | await this.erc721aSpot.connect(this.addr1).burn(20); 235 | expect(await this.erc721aSpot.balanceOf(this.addr1.address)).to.eq(5); 236 | expect(await this.erc721aSpot.totalSupply()).to.eq(5); 237 | 238 | await this.erc721aSpot.connect(this.addr1).burn(30); 239 | expect(await this.erc721aSpot.balanceOf(this.addr1.address)).to.eq(4); 240 | expect(await this.erc721aSpot.totalSupply()).to.eq(4); 241 | 242 | await this.erc721aSpot.connect(this.addr1).burn(11); 243 | await this.erc721aSpot.connect(this.addr1).burn(12); 244 | await this.erc721aSpot.connect(this.addr1).burn(13); 245 | await this.erc721aSpot.connect(this.addr1).burn(14); 246 | expect(await this.erc721aSpot.balanceOf(this.addr1.address)).to.eq(0); 247 | expect(await this.erc721aSpot.totalSupply()).to.eq(0); 248 | }); 249 | 250 | it('does not reduce totalMinted', async function () { 251 | expect(await this.erc721aSpot.balanceOf(this.addr1.address)).to.eq(7); 252 | await this.erc721aSpot.connect(this.addr1).burn(10); 253 | expect(await this.erc721aSpot.totalMinted()).to.eq(7); 254 | 255 | await this.erc721aSpot.connect(this.addr1).burn(20); 256 | expect(await this.erc721aSpot.totalMinted()).to.eq(7); 257 | 258 | await this.erc721aSpot.connect(this.addr1).burn(30); 259 | expect(await this.erc721aSpot.totalMinted()).to.eq(7); 260 | }); 261 | 262 | it('increases _numberBurned, totalBurned', async function () { 263 | expect(await this.erc721aSpot.balanceOf(this.addr1.address)).to.eq(7); 264 | await this.erc721aSpot.connect(this.addr1).burn(10); 265 | expect(await this.erc721aSpot.numberBurned(this.addr1.address)).to.eq(1); 266 | expect(await this.erc721aSpot.totalBurned()).to.eq(1); 267 | 268 | await this.erc721aSpot.connect(this.addr1).burn(20); 269 | expect(await this.erc721aSpot.numberBurned(this.addr1.address)).to.eq(2); 270 | expect(await this.erc721aSpot.totalBurned()).to.eq(2); 271 | 272 | await this.erc721aSpot.connect(this.addr1).burn(30); 273 | expect(await this.erc721aSpot.numberBurned(this.addr1.address)).to.eq(3); 274 | expect(await this.erc721aSpot.totalBurned()).to.eq(3); 275 | 276 | await this.erc721aSpot.connect(this.addr1).burn(11); 277 | await this.erc721aSpot.connect(this.addr1).burn(12); 278 | await this.erc721aSpot.connect(this.addr1).burn(13); 279 | await this.erc721aSpot.connect(this.addr1).burn(14); 280 | expect(await this.erc721aSpot.numberBurned(this.addr1.address)).to.eq(7); 281 | expect(await this.erc721aSpot.totalBurned()).to.eq(7); 282 | }); 283 | 284 | it('affects _exists', async function () { 285 | expect(await this.erc721aSpot.exists(0)).to.eq(false); 286 | expect(await this.erc721aSpot.exists(9)).to.eq(false); 287 | expect(await this.erc721aSpot.exists(10)).to.eq(true); 288 | 289 | expect(await this.erc721aSpot.exists(20)).to.eq(true); 290 | 291 | await this.erc721aSpot.connect(this.addr1).burn(20); 292 | expect(await this.erc721aSpot.exists(20)).to.eq(false); 293 | 294 | this.erc721aSpot.safeMintSpot(this.owner.address, 20); 295 | expect(await this.erc721aSpot.exists(20)).to.eq(true); 296 | }); 297 | 298 | it('forwards extraData after burn and re-mint', async function () { 299 | await this.erc721aSpot.setExtraDataAt(20, 123); 300 | let explicitOwnership = await this.erc721aSpot.explicitOwnershipOf(20); 301 | expect(explicitOwnership.addr).to.eq(this.addr1.address); 302 | expect(explicitOwnership.burned).to.eq(false); 303 | expect(explicitOwnership.extraData).to.eq(123); 304 | 305 | await this.erc721aSpot.connect(this.addr1).burn(20); 306 | explicitOwnership = await this.erc721aSpot.explicitOwnershipOf(20); 307 | expect(explicitOwnership.addr).to.eq(this.addr1.address); 308 | expect(explicitOwnership.burned).to.eq(true); 309 | expect(explicitOwnership.extraData).to.eq(123); 310 | 311 | this.erc721aSpot.safeMintSpot(this.owner.address, 20); 312 | explicitOwnership = await this.erc721aSpot.explicitOwnershipOf(20); 313 | expect(explicitOwnership.addr).to.eq(this.owner.address); 314 | expect(explicitOwnership.burned).to.eq(false); 315 | expect(explicitOwnership.extraData).to.eq(123); 316 | }); 317 | }); 318 | }); 319 | }); 320 | -------------------------------------------------------------------------------- /test/extensions/ERC721ATransferCounter.test.js: -------------------------------------------------------------------------------- 1 | const { deployContract } = require('../helpers.js'); 2 | const { expect } = require('chai'); 3 | 4 | const createTestSuite = ({ contract, constructorArgs }) => 5 | function () { 6 | context(`${contract}`, function () { 7 | beforeEach(async function () { 8 | this.erc721aCounter = await deployContract(contract, constructorArgs); 9 | }); 10 | 11 | context('with minted tokens', async function () { 12 | beforeEach(async function () { 13 | const [owner, addr1] = await ethers.getSigners(); 14 | this.owner = owner; 15 | this.addr1 = addr1; 16 | 17 | this.addr1.expected = { 18 | balance: 1, 19 | tokens: [0], 20 | }; 21 | 22 | this.owner.expected = { 23 | balance: 2, 24 | tokens: [1, 2], 25 | }; 26 | 27 | this.mintOrder = [this.addr1, this.owner]; 28 | 29 | for (const minter of this.mintOrder) { 30 | const balance = minter.expected.balance; 31 | if (balance > 0) { 32 | await this.erc721aCounter['safeMint(address,uint256)'](minter.address, balance); 33 | } 34 | // sanity check 35 | expect(await this.erc721aCounter.balanceOf(minter.address)).to.equal(minter.expected.balance); 36 | } 37 | }); 38 | 39 | describe('_ownershipOf', function () { 40 | it('initial', async function () { 41 | for (const minter of this.mintOrder) { 42 | for (const tokenId in minter.expected.tokens) { 43 | const ownership = await this.erc721aCounter.getOwnershipOf(tokenId); 44 | expect(ownership.extraData).to.equal(42); 45 | } 46 | } 47 | }); 48 | 49 | it('after a transfer', async function () { 50 | await this.erc721aCounter.transferFrom(this.owner.address, this.addr1.address, 1); 51 | 52 | const tests = [ 53 | { tokenId: 0, expectedData: 42 }, 54 | { tokenId: 1, expectedData: 43 }, 55 | { tokenId: 2, expectedData: 42 }, 56 | ]; 57 | 58 | for (const test of tests) { 59 | const ownership = await this.erc721aCounter.getOwnershipOf(test.tokenId); 60 | expect(ownership.extraData).to.equal(test.expectedData); 61 | } 62 | }); 63 | 64 | it('after a burn', async function () { 65 | await this.erc721aCounter['burn(uint256)'](2); 66 | 67 | const tests = [ 68 | { tokenId: 0, expectedData: 42 }, 69 | { tokenId: 1, expectedData: 42 }, 70 | { tokenId: 2, expectedData: 1337 }, 71 | ]; 72 | 73 | for (const test of tests) { 74 | const ownership = await this.erc721aCounter.getOwnershipAt(test.tokenId); 75 | expect(ownership.extraData).to.equal(test.expectedData); 76 | } 77 | }); 78 | }); 79 | 80 | describe('setExtraData', function () { 81 | it('can set and get the extraData directly', async function () { 82 | const extraData = 12345; 83 | await this.erc721aCounter.setExtraDataAt(0, extraData); 84 | const ownership = await this.erc721aCounter.getOwnershipAt(0); 85 | expect(ownership.extraData).to.equal(extraData); 86 | }); 87 | 88 | it('setting the extraData for uninitialized slot reverts', async function () { 89 | const extraData = 12345; 90 | await expect(this.erc721aCounter.setExtraDataAt(2, extraData)) 91 | .to.be.revertedWith('OwnershipNotInitializedForExtraData'); 92 | await this.erc721aCounter.transferFrom(this.owner.address, this.addr1.address, 2); 93 | await this.erc721aCounter.setExtraDataAt(2, extraData); 94 | const ownership = await this.erc721aCounter.getOwnershipAt(2); 95 | expect(ownership.extraData).to.equal(extraData); 96 | }); 97 | }); 98 | }); 99 | }); 100 | }; 101 | 102 | describe( 103 | 'ERC721A override _extraData()', 104 | createTestSuite({ 105 | contract: 'ERC721ATransferCounterMock', 106 | constructorArgs: ['Azuki', 'AZUKI'], 107 | }) 108 | ); 109 | -------------------------------------------------------------------------------- /test/helpers.js: -------------------------------------------------------------------------------- 1 | const { ethers } = require('hardhat'); 2 | const { BigNumber } = require('ethers'); 3 | 4 | const deployContract = async function (contractName, constructorArgs) { 5 | let factory; 6 | try { 7 | factory = await ethers.getContractFactory(contractName); 8 | } catch (e) { 9 | factory = await ethers.getContractFactory(contractName + 'UpgradeableWithInit'); 10 | } 11 | let contract = await factory.deploy(...(constructorArgs || [])); 12 | await contract.deployed(); 13 | return contract; 14 | }; 15 | 16 | const getBlockTimestamp = async function () { 17 | return parseInt((await ethers.provider.getBlock('latest'))['timestamp']); 18 | }; 19 | 20 | const mineBlockTimestamp = async function (timestamp) { 21 | await ethers.provider.send('evm_setNextBlockTimestamp', [timestamp]); 22 | await ethers.provider.send('evm_mine'); 23 | }; 24 | 25 | const offsettedIndex = function (startTokenId, arr) { 26 | // return one item if arr length is 1 27 | if (arr.length === 1) { 28 | return BigNumber.from(startTokenId + arr[0]); 29 | } 30 | return arr.map((num) => BigNumber.from(startTokenId + num)); 31 | }; 32 | 33 | module.exports = { deployContract, getBlockTimestamp, mineBlockTimestamp, offsettedIndex }; 34 | --------------------------------------------------------------------------------