├── .eslintrc ├── .github └── workflows │ ├── deploy-npm.yml │ ├── run-tests.yml │ └── transpile-merge.yml ├── .gitignore ├── .prettierrc ├── CODEOWNERS ├── LICENSE.txt ├── README.md ├── contracts ├── ERC721AStorage.sol ├── ERC721AUpgradeable.sol ├── ERC721A__Initializable.sol ├── ERC721A__InitializableStorage.sol ├── IERC721AUpgradeable.sol ├── extensions │ ├── ERC4907AStorage.sol │ ├── ERC4907AUpgradeable.sol │ ├── ERC721ABurnableUpgradeable.sol │ ├── ERC721AQueryableUpgradeable.sol │ ├── IERC4907AUpgradeable.sol │ ├── IERC721ABurnableUpgradeable.sol │ └── IERC721AQueryableUpgradeable.sol ├── interfaces │ ├── IERC4907AUpgradeable.sol │ ├── IERC721ABurnableUpgradeable.sol │ ├── IERC721AQueryableUpgradeable.sol │ └── IERC721AUpgradeable.sol └── mocks │ ├── DirectBurnBitSetterHelperUpgradeable.sol │ ├── ERC4907AMockUpgradeable.sol │ ├── ERC721ABurnableMockUpgradeable.sol │ ├── ERC721ABurnableStartTokenIdMockUpgradeable.sol │ ├── ERC721AGasReporterMockUpgradeable.sol │ ├── ERC721AMockUpgradeable.sol │ ├── ERC721AQueryableMockUpgradeable.sol │ ├── ERC721AQueryableStartTokenIdMockUpgradeable.sol │ ├── ERC721ASpotMockUpgradeable.sol │ ├── ERC721AStartTokenIdMockUpgradeable.sol │ ├── ERC721ATransferCounterMockUpgradeable.sol │ ├── ERC721AWithERC2309MockUpgradeable.sol │ ├── ERC721ReceiverMockStorage.sol │ ├── ERC721ReceiverMockUpgradeable.sol │ ├── SequentialUpToHelperUpgradeable.sol │ ├── StartTokenIdHelperUpgradeable.sol │ └── WithInit.sol ├── hardhat.config.js ├── package-lock.json ├── package.json ├── scripts ├── replace-imports.js └── transpile-merge.sh └── test ├── ERC721A.test.js ├── GasUsage.test.js ├── extensions ├── ERC4907A.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/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 Upgradeable 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,18.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/transpile-merge.yml: -------------------------------------------------------------------------------- 1 | name: Transpile Merge 2 | 3 | on: 4 | push: 5 | branches: [patches] 6 | workflow_dispatch: {} 7 | repository_dispatch: 8 | types: [Update] 9 | 10 | concurrency: 11 | group: transpile-${{ github.ref_name }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | transpile: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Use Node.js 16.x 20 | uses: actions/setup-node@v2 21 | with: 22 | node-version: 16.x 23 | - name: Install dependencies 24 | run: npm ci 25 | - name: Transpile Merge 26 | run: ./scripts/transpile-merge.sh 27 | -------------------------------------------------------------------------------- /.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 | } -------------------------------------------------------------------------------- /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 | 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 | This repository hosts the Upgradeable variant of [ERC721A](https://github.com/chiru-labs/ERC721A), meant for use in upgradeable contracts. This variant is available as separate package called `erc721a-upgradeable`. 23 | 24 | This version uses the [diamond storage layout pattern](https://eips.ethereum.org/EIPS/eip-2535). 25 | 26 | It follows all of the rules for [Writing Upgradeable Contracts]: constructors are replaced by initializer functions, state variables are initialized in initializer functions, and we additionally check for storage incompatibilities across minor versions. 27 | 28 | [writing upgradeable contracts]: https://docs.openzeppelin.com/upgrades-plugins/writing-upgradeable 29 | 30 | > **Warning** 31 | > 32 | > There will be storage incompatibilities across major versions of this package, which makes it unsafe to upgrade a deployed contract from one major version to another, for example from 3.4.0 to 4.0.0. 33 | > 34 | > **It is strongly encouraged to use these contracts together with a tool that can simplify the deployment of upgradeable contracts, such as [OpenZeppelin Upgrades Plugins](https://github.com/OpenZeppelin/openzeppelin-upgrades).** 35 | 36 | This repository is generated by a transpiler. 37 | 38 | **Chiru Labs is not liable for any outcomes as a result of using ERC721A and ERC721A-Upgradeable.** DYOR. 39 | 40 | 41 | 42 | ## Docs 43 | 44 | https://chiru-labs.github.io/ERC721A/ 45 | 46 | 47 | 48 | ## Installation 49 | 50 | ```sh 51 | 52 | npm install --save-dev erc721a-upgradeable 53 | 54 | ``` 55 | 56 | 57 | 58 | ## Usage 59 | 60 | Once installed, you can use the contracts in the library by importing them: 61 | 62 | ```solidity 63 | pragma solidity ^0.8.4; 64 | 65 | import 'erc721a-upgradeable/contracts/ERC721AUpgradeable.sol'; 66 | import '@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol'; 67 | 68 | contract Something is ERC721AUpgradeable, OwnableUpgradeable { 69 | // Take note of the initializer modifiers. 70 | // - `initializerERC721A` for `ERC721AUpgradeable`. 71 | // - `initializer` for OpenZeppelin's `OwnableUpgradeable`. 72 | function initialize() initializerERC721A initializer public { 73 | __ERC721A_init('Something', 'SMTH'); 74 | __Ownable_init(); 75 | } 76 | 77 | function mint(uint256 quantity) external payable { 78 | // `_mint`'s second argument now takes in a `quantity`, not a `tokenId`. 79 | _mint(msg.sender, quantity); 80 | } 81 | 82 | function adminMint(uint256 quantity) external payable onlyOwner { 83 | _mint(msg.sender, quantity); 84 | } 85 | } 86 | 87 | ``` 88 | 89 | 90 | 91 | ## Contributing 92 | 93 | This repository is automatically transpiled from the main [ERC721A](https://github.com/chiru-labs/ERC721A) repository by a workflow. 94 | 95 | Any changes to the `contracts` and `test` directories will be overwritten. 96 | 97 | If you want to make a contribution to the transpiler workflow: 98 | 99 | 1. Fork the Project 100 | 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) 101 | 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) 102 | 4. Push to the Branch (`git push origin feature/AmazingFeature`) 103 | 5. Open a Pull Request 104 | 105 | ### Running tests locally 106 | 107 | 1. `npm install` 108 | 2. `npm run test` 109 | 110 | 111 | 112 | ## License 113 | 114 | Distributed under the MIT License. See `LICENSE.txt` for more information. 115 | 116 | 117 | 118 | ## Contact 119 | 120 | - 2pm.flow (owner) - [@2pmflow](https://twitter.com/2pmflow) 121 | - location tba (owner) - [@locationtba](https://twitter.com/locationtba) 122 | - cygaar (maintainer) - [@cygaar_dev](https://twitter.com/cygaar_dev) 123 | - vectorized.eth (maintainer) - [@optimizoor](https://twitter.com/optimizoor) 124 | 125 | Project Link: [https://github.com/chiru-labs/ERC721A-Upgradeable](https://github.com/chiru-labs/ERC721A-Upgradeable) 126 | 127 | 128 | 129 | 130 | 131 | [docs-shield]: https://img.shields.io/badge/docs-%F0%9F%93%84-blue?style=for-the-badge 132 | [docs-url]: https://chiru-labs.github.io/ERC721A/#/upgradeable 133 | [npm-shield]: https://img.shields.io/npm/v/erc721a-upgradeable.svg?style=for-the-badge 134 | [npm-url]: https://www.npmjs.com/package/erc721a-upgradeable 135 | [ci-shield]: https://img.shields.io/github/actions/workflow/status/chiru-labs/ERC721A-Upgradeable/run-tests.yml?label=build&style=for-the-badge&branch=main 136 | [ci-url]: https://github.com/chiru-labs/ERC721A-Upgradeable/actions/workflows/run-tests.yml 137 | [issues-shield]: https://img.shields.io/github/issues/chiru-labs/ERC721A-Upgradeable.svg?style=for-the-badge 138 | [issues-url]: https://github.com/chiru-labs/ERC721A-Upgradeable/issues 139 | [license-shield]: https://img.shields.io/badge/License-MIT-green.svg?style=for-the-badge 140 | [license-url]: https://github.com/chiru-labs/ERC721A-Upgradeable/blob/main/LICENSE.txt 141 | -------------------------------------------------------------------------------- /contracts/ERC721AStorage.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | library ERC721AStorage { 6 | // Bypass for a `--via-ir` bug (https://github.com/chiru-labs/ERC721A/pull/364). 7 | struct TokenApprovalRef { 8 | address value; 9 | } 10 | 11 | struct Layout { 12 | // ============================================================= 13 | // STORAGE 14 | // ============================================================= 15 | 16 | // The next token ID to be minted. 17 | uint256 _currentIndex; 18 | // The number of tokens burned. 19 | uint256 _burnCounter; 20 | // Token name 21 | string _name; 22 | // Token symbol 23 | string _symbol; 24 | // Mapping from token ID to ownership details 25 | // An empty struct value does not necessarily mean the token is unowned. 26 | // See {_packedOwnershipOf} implementation for details. 27 | // 28 | // Bits Layout: 29 | // - [0..159] `addr` 30 | // - [160..223] `startTimestamp` 31 | // - [224] `burned` 32 | // - [225] `nextInitialized` 33 | // - [232..255] `extraData` 34 | mapping(uint256 => uint256) _packedOwnerships; 35 | // Mapping owner address to address data. 36 | // 37 | // Bits Layout: 38 | // - [0..63] `balance` 39 | // - [64..127] `numberMinted` 40 | // - [128..191] `numberBurned` 41 | // - [192..255] `aux` 42 | mapping(address => uint256) _packedAddressData; 43 | // Mapping from token ID to approved address. 44 | mapping(uint256 => ERC721AStorage.TokenApprovalRef) _tokenApprovals; 45 | // Mapping from owner to operator approvals 46 | mapping(address => mapping(address => bool)) _operatorApprovals; 47 | // The amount of tokens minted above `_sequentialUpTo()`. 48 | // We call these spot mints (i.e. non-sequential mints). 49 | uint256 _spotMinted; 50 | } 51 | 52 | bytes32 internal constant STORAGE_SLOT = keccak256('ERC721A.contracts.storage.ERC721A'); 53 | 54 | function layout() internal pure returns (Layout storage l) { 55 | bytes32 slot = STORAGE_SLOT; 56 | assembly { 57 | l.slot := slot 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /contracts/ERC721A__Initializable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | /** 5 | * @dev This is a base contract to aid in writing upgradeable diamond facet contracts, or any kind of contract that will be deployed 6 | * behind a proxy. Since proxied contracts do not make use of a constructor, it's common to move constructor logic to an 7 | * external initializer function, usually called `initialize`. It then becomes necessary to protect this initializer 8 | * function so it can only be called once. The {initializer} modifier provided by this contract will have this effect. 9 | * 10 | * TIP: To avoid leaving the proxy in an uninitialized state, the initializer function should be called as early as 11 | * possible by providing the encoded function call as the `_data` argument to {ERC1967Proxy-constructor}. 12 | * 13 | * CAUTION: When used with inheritance, manual care must be taken to not invoke a parent initializer twice, or to ensure 14 | * that all initializers are idempotent. This is not verified automatically as constructors are by Solidity. 15 | */ 16 | 17 | import {ERC721A__InitializableStorage} from './ERC721A__InitializableStorage.sol'; 18 | 19 | abstract contract ERC721A__Initializable { 20 | using ERC721A__InitializableStorage for ERC721A__InitializableStorage.Layout; 21 | 22 | /** 23 | * @dev Modifier to protect an initializer function from being invoked twice. 24 | */ 25 | modifier initializerERC721A() { 26 | // If the contract is initializing we ignore whether _initialized is set in order to support multiple 27 | // inheritance patterns, but we only do this in the context of a constructor, because in other contexts the 28 | // contract may have been reentered. 29 | require( 30 | ERC721A__InitializableStorage.layout()._initializing 31 | ? _isConstructor() 32 | : !ERC721A__InitializableStorage.layout()._initialized, 33 | 'ERC721A__Initializable: contract is already initialized' 34 | ); 35 | 36 | bool isTopLevelCall = !ERC721A__InitializableStorage.layout()._initializing; 37 | if (isTopLevelCall) { 38 | ERC721A__InitializableStorage.layout()._initializing = true; 39 | ERC721A__InitializableStorage.layout()._initialized = true; 40 | } 41 | 42 | _; 43 | 44 | if (isTopLevelCall) { 45 | ERC721A__InitializableStorage.layout()._initializing = false; 46 | } 47 | } 48 | 49 | /** 50 | * @dev Modifier to protect an initialization function so that it can only be invoked by functions with the 51 | * {initializer} modifier, directly or indirectly. 52 | */ 53 | modifier onlyInitializingERC721A() { 54 | require( 55 | ERC721A__InitializableStorage.layout()._initializing, 56 | 'ERC721A__Initializable: contract is not initializing' 57 | ); 58 | _; 59 | } 60 | 61 | /// @dev Returns true if and only if the function is running in the constructor 62 | function _isConstructor() private view returns (bool) { 63 | // extcodesize checks the size of the code stored in an address, and 64 | // address returns the current address. Since the code is still not 65 | // deployed when running a constructor, any checks on its code size will 66 | // yield zero, making it an effective way to detect if a contract is 67 | // under construction or not. 68 | address self = address(this); 69 | uint256 cs; 70 | assembly { 71 | cs := extcodesize(self) 72 | } 73 | return cs == 0; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /contracts/ERC721A__InitializableStorage.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | /** 6 | * @dev This is a base storage for the initialization function for upgradeable diamond facet contracts 7 | **/ 8 | 9 | library ERC721A__InitializableStorage { 10 | struct Layout { 11 | /* 12 | * Indicates that the contract has been initialized. 13 | */ 14 | bool _initialized; 15 | /* 16 | * Indicates that the contract is in the process of being initialized. 17 | */ 18 | bool _initializing; 19 | } 20 | 21 | bytes32 internal constant STORAGE_SLOT = keccak256('ERC721A.contracts.storage.initializable.facet'); 22 | 23 | function layout() internal pure returns (Layout storage l) { 24 | bytes32 slot = STORAGE_SLOT; 25 | assembly { 26 | l.slot := slot 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /contracts/IERC721AUpgradeable.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 IERC721AUpgradeable { 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 | * `_sequentialUpTo()` must be greater than `_startTokenId()`. 79 | */ 80 | error SequentialUpToTooSmall(); 81 | 82 | /** 83 | * The `tokenId` of a sequential mint exceeds `_sequentialUpTo()`. 84 | */ 85 | error SequentialMintExceedsLimit(); 86 | 87 | /** 88 | * Spot minting requires a `tokenId` greater than `_sequentialUpTo()`. 89 | */ 90 | error SpotMintTokenIdTooSmall(); 91 | 92 | /** 93 | * Cannot mint over a token that already exists. 94 | */ 95 | error TokenAlreadyExists(); 96 | 97 | /** 98 | * The feature is not compatible with spot mints. 99 | */ 100 | error NotCompatibleWithSpotMints(); 101 | 102 | // ============================================================= 103 | // STRUCTS 104 | // ============================================================= 105 | 106 | struct TokenOwnership { 107 | // The address of the owner. 108 | address addr; 109 | // Stores the start time of ownership with minimal overhead for tokenomics. 110 | uint64 startTimestamp; 111 | // Whether the token has been burned. 112 | bool burned; 113 | // Arbitrary data similar to `startTimestamp` that can be set via {_extraData}. 114 | uint24 extraData; 115 | } 116 | 117 | // ============================================================= 118 | // TOKEN COUNTERS 119 | // ============================================================= 120 | 121 | /** 122 | * @dev Returns the total number of tokens in existence. 123 | * Burned tokens will reduce the count. 124 | * To get the total number of tokens minted, please see {_totalMinted}. 125 | */ 126 | function totalSupply() external view returns (uint256); 127 | 128 | // ============================================================= 129 | // IERC165 130 | // ============================================================= 131 | 132 | /** 133 | * @dev Returns true if this contract implements the interface defined by 134 | * `interfaceId`. See the corresponding 135 | * [EIP section](https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified) 136 | * to learn more about how these ids are created. 137 | * 138 | * This function call must use less than 30000 gas. 139 | */ 140 | function supportsInterface(bytes4 interfaceId) external view returns (bool); 141 | 142 | // ============================================================= 143 | // IERC721 144 | // ============================================================= 145 | 146 | /** 147 | * @dev Emitted when `tokenId` token is transferred from `from` to `to`. 148 | */ 149 | event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); 150 | 151 | /** 152 | * @dev Emitted when `owner` enables `approved` to manage the `tokenId` token. 153 | */ 154 | event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId); 155 | 156 | /** 157 | * @dev Emitted when `owner` enables or disables 158 | * (`approved`) `operator` to manage all of its assets. 159 | */ 160 | event ApprovalForAll(address indexed owner, address indexed operator, bool approved); 161 | 162 | /** 163 | * @dev Returns the number of tokens in `owner`'s account. 164 | */ 165 | function balanceOf(address owner) external view returns (uint256 balance); 166 | 167 | /** 168 | * @dev Returns the owner of the `tokenId` token. 169 | * 170 | * Requirements: 171 | * 172 | * - `tokenId` must exist. 173 | */ 174 | function ownerOf(uint256 tokenId) external view returns (address owner); 175 | 176 | /** 177 | * @dev Safely transfers `tokenId` token from `from` to `to`, 178 | * checking first that contract recipients are aware of the ERC721 protocol 179 | * to prevent tokens from being forever locked. 180 | * 181 | * Requirements: 182 | * 183 | * - `from` cannot be the zero address. 184 | * - `to` cannot be the zero address. 185 | * - `tokenId` token must exist and be owned by `from`. 186 | * - If the caller is not `from`, it must be have been allowed to move 187 | * this token by either {approve} or {setApprovalForAll}. 188 | * - If `to` refers to a smart contract, it must implement 189 | * {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. 190 | * 191 | * Emits a {Transfer} event. 192 | */ 193 | function safeTransferFrom( 194 | address from, 195 | address to, 196 | uint256 tokenId, 197 | bytes calldata data 198 | ) external payable; 199 | 200 | /** 201 | * @dev Equivalent to `safeTransferFrom(from, to, tokenId, '')`. 202 | */ 203 | function safeTransferFrom( 204 | address from, 205 | address to, 206 | uint256 tokenId 207 | ) external payable; 208 | 209 | /** 210 | * @dev Transfers `tokenId` from `from` to `to`. 211 | * 212 | * WARNING: Usage of this method is discouraged, use {safeTransferFrom} 213 | * whenever possible. 214 | * 215 | * Requirements: 216 | * 217 | * - `from` cannot be the zero address. 218 | * - `to` cannot be the zero address. 219 | * - `tokenId` token must be owned by `from`. 220 | * - If the caller is not `from`, it must be approved to move this token 221 | * by either {approve} or {setApprovalForAll}. 222 | * 223 | * Emits a {Transfer} event. 224 | */ 225 | function transferFrom( 226 | address from, 227 | address to, 228 | uint256 tokenId 229 | ) external payable; 230 | 231 | /** 232 | * @dev Gives permission to `to` to transfer `tokenId` token to another account. 233 | * The approval is cleared when the token is transferred. 234 | * 235 | * Only a single account can be approved at a time, so approving the 236 | * zero address clears previous approvals. 237 | * 238 | * Requirements: 239 | * 240 | * - The caller must own the token or be an approved operator. 241 | * - `tokenId` must exist. 242 | * 243 | * Emits an {Approval} event. 244 | */ 245 | function approve(address to, uint256 tokenId) external payable; 246 | 247 | /** 248 | * @dev Approve or remove `operator` as an operator for the caller. 249 | * Operators can call {transferFrom} or {safeTransferFrom} 250 | * for any token owned by the caller. 251 | * 252 | * Requirements: 253 | * 254 | * - The `operator` cannot be the caller. 255 | * 256 | * Emits an {ApprovalForAll} event. 257 | */ 258 | function setApprovalForAll(address operator, bool _approved) external; 259 | 260 | /** 261 | * @dev Returns the account approved for `tokenId` token. 262 | * 263 | * Requirements: 264 | * 265 | * - `tokenId` must exist. 266 | */ 267 | function getApproved(uint256 tokenId) external view returns (address operator); 268 | 269 | /** 270 | * @dev Returns if the `operator` is allowed to manage all of the assets of `owner`. 271 | * 272 | * See {setApprovalForAll}. 273 | */ 274 | function isApprovedForAll(address owner, address operator) external view returns (bool); 275 | 276 | // ============================================================= 277 | // IERC721Metadata 278 | // ============================================================= 279 | 280 | /** 281 | * @dev Returns the token collection name. 282 | */ 283 | function name() external view returns (string memory); 284 | 285 | /** 286 | * @dev Returns the token collection symbol. 287 | */ 288 | function symbol() external view returns (string memory); 289 | 290 | /** 291 | * @dev Returns the Uniform Resource Identifier (URI) for `tokenId` token. 292 | */ 293 | function tokenURI(uint256 tokenId) external view returns (string memory); 294 | 295 | // ============================================================= 296 | // IERC2309 297 | // ============================================================= 298 | 299 | /** 300 | * @dev Emitted when tokens in `fromTokenId` to `toTokenId` 301 | * (inclusive) is transferred from `from` to `to`, as defined in the 302 | * [ERC2309](https://eips.ethereum.org/EIPS/eip-2309) standard. 303 | * 304 | * See {_mintERC2309} for more details. 305 | */ 306 | event ConsecutiveTransfer(uint256 indexed fromTokenId, uint256 toTokenId, address indexed from, address indexed to); 307 | } 308 | -------------------------------------------------------------------------------- /contracts/extensions/ERC4907AStorage.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import {ERC4907AUpgradeable} from './ERC4907AUpgradeable.sol'; 6 | 7 | library ERC4907AStorage { 8 | struct Layout { 9 | // Mapping from token ID to user info. 10 | // 11 | // Bits Layout: 12 | // - [0..159] `user` 13 | // - [160..223] `expires` 14 | mapping(uint256 => uint256) _packedUserInfo; 15 | } 16 | 17 | bytes32 internal constant STORAGE_SLOT = keccak256('ERC721A.contracts.storage.ERC4907A'); 18 | 19 | function layout() internal pure returns (Layout storage l) { 20 | bytes32 slot = STORAGE_SLOT; 21 | assembly { 22 | l.slot := slot 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /contracts/extensions/ERC4907AUpgradeable.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 './IERC4907AUpgradeable.sol'; 8 | import '../ERC721AUpgradeable.sol'; 9 | import {ERC4907AStorage} from './ERC4907AStorage.sol'; 10 | import '../ERC721A__Initializable.sol'; 11 | 12 | /** 13 | * @title ERC4907A 14 | * 15 | * @dev [ERC4907](https://eips.ethereum.org/EIPS/eip-4907) compliant 16 | * extension of ERC721A, which allows owners and authorized addresses 17 | * to add a time-limited role with restricted permissions to ERC721 tokens. 18 | */ 19 | abstract contract ERC4907AUpgradeable is ERC721A__Initializable, ERC721AUpgradeable, IERC4907AUpgradeable { 20 | using ERC4907AStorage for ERC4907AStorage.Layout; 21 | 22 | function __ERC4907A_init() internal onlyInitializingERC721A { 23 | __ERC4907A_init_unchained(); 24 | } 25 | 26 | function __ERC4907A_init_unchained() internal onlyInitializingERC721A {} 27 | 28 | // The bit position of `expires` in packed user info. 29 | uint256 private constant _BITPOS_EXPIRES = 160; 30 | 31 | /** 32 | * @dev Sets the `user` and `expires` for `tokenId`. 33 | * The zero address indicates there is no user. 34 | * 35 | * Requirements: 36 | * 37 | * - The caller must own `tokenId` or be an approved operator. 38 | */ 39 | function setUser( 40 | uint256 tokenId, 41 | address user, 42 | uint64 expires 43 | ) public virtual override { 44 | // Require the caller to be either the token owner or an approved operator. 45 | address owner = ownerOf(tokenId); 46 | if (_msgSenderERC721A() != owner) 47 | if (!isApprovedForAll(owner, _msgSenderERC721A())) 48 | if (getApproved(tokenId) != _msgSenderERC721A()) _revert(SetUserCallerNotOwnerNorApproved.selector); 49 | 50 | ERC4907AStorage.layout()._packedUserInfo[tokenId] = 51 | (uint256(expires) << _BITPOS_EXPIRES) | 52 | uint256(uint160(user)); 53 | 54 | emit UpdateUser(tokenId, user, expires); 55 | } 56 | 57 | /** 58 | * @dev Returns the user address for `tokenId`. 59 | * The zero address indicates that there is no user or if the user is expired. 60 | */ 61 | function userOf(uint256 tokenId) public view virtual override returns (address) { 62 | uint256 packed = ERC4907AStorage.layout()._packedUserInfo[tokenId]; 63 | assembly { 64 | // Branchless `packed *= (block.timestamp <= expires ? 1 : 0)`. 65 | // If the `block.timestamp == expires`, the `lt` clause will be true 66 | // if there is a non-zero user address in the lower 160 bits of `packed`. 67 | packed := mul( 68 | packed, 69 | // `block.timestamp <= expires ? 1 : 0`. 70 | lt(shl(_BITPOS_EXPIRES, timestamp()), packed) 71 | ) 72 | } 73 | return address(uint160(packed)); 74 | } 75 | 76 | /** 77 | * @dev Returns the user's expires of `tokenId`. 78 | */ 79 | function userExpires(uint256 tokenId) public view virtual override returns (uint256) { 80 | return ERC4907AStorage.layout()._packedUserInfo[tokenId] >> _BITPOS_EXPIRES; 81 | } 82 | 83 | /** 84 | * @dev Override of {IERC165-supportsInterface}. 85 | */ 86 | function supportsInterface(bytes4 interfaceId) 87 | public 88 | view 89 | virtual 90 | override(ERC721AUpgradeable, IERC721AUpgradeable) 91 | returns (bool) 92 | { 93 | // The interface ID for ERC4907 is `0xad092b5c`, 94 | // as defined in [ERC4907](https://eips.ethereum.org/EIPS/eip-4907). 95 | return super.supportsInterface(interfaceId) || interfaceId == 0xad092b5c; 96 | } 97 | 98 | /** 99 | * @dev Returns the user address for `tokenId`, ignoring the expiry status. 100 | */ 101 | function _explicitUserOf(uint256 tokenId) internal view virtual returns (address) { 102 | return address(uint160(ERC4907AStorage.layout()._packedUserInfo[tokenId])); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /contracts/extensions/ERC721ABurnableUpgradeable.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 './IERC721ABurnableUpgradeable.sol'; 8 | import '../ERC721AUpgradeable.sol'; 9 | import '../ERC721A__Initializable.sol'; 10 | 11 | /** 12 | * @title ERC721ABurnable. 13 | * 14 | * @dev ERC721A token that can be irreversibly burned (destroyed). 15 | */ 16 | abstract contract ERC721ABurnableUpgradeable is 17 | ERC721A__Initializable, 18 | ERC721AUpgradeable, 19 | IERC721ABurnableUpgradeable 20 | { 21 | function __ERC721ABurnable_init() internal onlyInitializingERC721A { 22 | __ERC721ABurnable_init_unchained(); 23 | } 24 | 25 | function __ERC721ABurnable_init_unchained() internal onlyInitializingERC721A {} 26 | 27 | /** 28 | * @dev Burns `tokenId`. See {ERC721A-_burn}. 29 | * 30 | * Requirements: 31 | * 32 | * - The caller must own `tokenId` or be an approved operator. 33 | */ 34 | function burn(uint256 tokenId) public virtual override { 35 | _burn(tokenId, true); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /contracts/extensions/ERC721AQueryableUpgradeable.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 './IERC721AQueryableUpgradeable.sol'; 8 | import '../ERC721AUpgradeable.sol'; 9 | import '../ERC721A__Initializable.sol'; 10 | 11 | /** 12 | * @title ERC721AQueryable. 13 | * 14 | * @dev ERC721A subclass with convenience query functions. 15 | */ 16 | abstract contract ERC721AQueryableUpgradeable is 17 | ERC721A__Initializable, 18 | ERC721AUpgradeable, 19 | IERC721AQueryableUpgradeable 20 | { 21 | function __ERC721AQueryable_init() internal onlyInitializingERC721A { 22 | __ERC721AQueryable_init_unchained(); 23 | } 24 | 25 | function __ERC721AQueryable_init_unchained() internal onlyInitializingERC721A {} 26 | 27 | /** 28 | * @dev Returns the `TokenOwnership` struct at `tokenId` without reverting. 29 | * 30 | * If the `tokenId` is out of bounds: 31 | * 32 | * - `addr = address(0)` 33 | * - `startTimestamp = 0` 34 | * - `burned = false` 35 | * - `extraData = 0` 36 | * 37 | * If the `tokenId` is burned: 38 | * 39 | * - `addr =
` 40 | * - `startTimestamp = ` 41 | * - `burned = true` 42 | * - `extraData = ` 43 | * 44 | * Otherwise: 45 | * 46 | * - `addr =
` 47 | * - `startTimestamp = ` 48 | * - `burned = false` 49 | * - `extraData = ` 50 | */ 51 | function explicitOwnershipOf(uint256 tokenId) 52 | public 53 | view 54 | virtual 55 | override 56 | returns (TokenOwnership memory ownership) 57 | { 58 | unchecked { 59 | if (tokenId >= _startTokenId()) { 60 | if (tokenId > _sequentialUpTo()) return _ownershipAt(tokenId); 61 | 62 | if (tokenId < _nextTokenId()) { 63 | // If the `tokenId` is within bounds, 64 | // scan backwards for the initialized ownership slot. 65 | while (!_ownershipIsInitialized(tokenId)) --tokenId; 66 | return _ownershipAt(tokenId); 67 | } 68 | } 69 | } 70 | } 71 | 72 | /** 73 | * @dev Returns an array of `TokenOwnership` structs at `tokenIds` in order. 74 | * See {ERC721AQueryable-explicitOwnershipOf} 75 | */ 76 | function explicitOwnershipsOf(uint256[] calldata tokenIds) 77 | external 78 | view 79 | virtual 80 | override 81 | returns (TokenOwnership[] memory) 82 | { 83 | TokenOwnership[] memory ownerships; 84 | uint256 i = tokenIds.length; 85 | assembly { 86 | // Grab the free memory pointer. 87 | ownerships := mload(0x40) 88 | // Store the length. 89 | mstore(ownerships, i) 90 | // Allocate one word for the length, 91 | // `tokenIds.length` words for the pointers. 92 | i := shl(5, i) // Multiply `i` by 32. 93 | mstore(0x40, add(add(ownerships, 0x20), i)) 94 | } 95 | while (i != 0) { 96 | uint256 tokenId; 97 | assembly { 98 | i := sub(i, 0x20) 99 | tokenId := calldataload(add(tokenIds.offset, i)) 100 | } 101 | TokenOwnership memory ownership = explicitOwnershipOf(tokenId); 102 | assembly { 103 | // Store the pointer of `ownership` in the `ownerships` array. 104 | mstore(add(add(ownerships, 0x20), i), ownership) 105 | } 106 | } 107 | return ownerships; 108 | } 109 | 110 | /** 111 | * @dev Returns an array of token IDs owned by `owner`, 112 | * in the range [`start`, `stop`) 113 | * (i.e. `start <= tokenId < stop`). 114 | * 115 | * This function allows for tokens to be queried if the collection 116 | * grows too big for a single call of {ERC721AQueryable-tokensOfOwner}. 117 | * 118 | * Requirements: 119 | * 120 | * - `start < stop` 121 | */ 122 | function tokensOfOwnerIn( 123 | address owner, 124 | uint256 start, 125 | uint256 stop 126 | ) external view virtual override returns (uint256[] memory) { 127 | return _tokensOfOwnerIn(owner, start, stop); 128 | } 129 | 130 | /** 131 | * @dev Returns an array of token IDs owned by `owner`. 132 | * 133 | * This function scans the ownership mapping and is O(`totalSupply`) in complexity. 134 | * It is meant to be called off-chain. 135 | * 136 | * See {ERC721AQueryable-tokensOfOwnerIn} for splitting the scan into 137 | * multiple smaller scans if the collection is large enough to cause 138 | * an out-of-gas error (10K collections should be fine). 139 | */ 140 | function tokensOfOwner(address owner) external view virtual override returns (uint256[] memory) { 141 | // If spot mints are enabled, full-range scan is disabled. 142 | if (_sequentialUpTo() != type(uint256).max) _revert(NotCompatibleWithSpotMints.selector); 143 | uint256 start = _startTokenId(); 144 | uint256 stop = _nextTokenId(); 145 | uint256[] memory tokenIds; 146 | if (start != stop) tokenIds = _tokensOfOwnerIn(owner, start, stop); 147 | return tokenIds; 148 | } 149 | 150 | /** 151 | * @dev Helper function for returning an array of token IDs owned by `owner`. 152 | * 153 | * Note that this function is optimized for smaller bytecode size over runtime gas, 154 | * since it is meant to be called off-chain. 155 | */ 156 | function _tokensOfOwnerIn( 157 | address owner, 158 | uint256 start, 159 | uint256 stop 160 | ) private view returns (uint256[] memory tokenIds) { 161 | unchecked { 162 | if (start >= stop) _revert(InvalidQueryRange.selector); 163 | // Set `start = max(start, _startTokenId())`. 164 | if (start < _startTokenId()) start = _startTokenId(); 165 | uint256 nextTokenId = _nextTokenId(); 166 | // If spot mints are enabled, scan all the way until the specified `stop`. 167 | uint256 stopLimit = _sequentialUpTo() != type(uint256).max ? stop : nextTokenId; 168 | // Set `stop = min(stop, stopLimit)`. 169 | if (stop >= stopLimit) stop = stopLimit; 170 | // Number of tokens to scan. 171 | uint256 tokenIdsMaxLength = balanceOf(owner); 172 | // Set `tokenIdsMaxLength` to zero if the range contains no tokens. 173 | if (start >= stop) tokenIdsMaxLength = 0; 174 | // If there are one or more tokens to scan. 175 | if (tokenIdsMaxLength != 0) { 176 | // Set `tokenIdsMaxLength = min(balanceOf(owner), tokenIdsMaxLength)`. 177 | if (stop - start <= tokenIdsMaxLength) tokenIdsMaxLength = stop - start; 178 | uint256 m; // Start of available memory. 179 | assembly { 180 | // Grab the free memory pointer. 181 | tokenIds := mload(0x40) 182 | // Allocate one word for the length, and `tokenIdsMaxLength` words 183 | // for the data. `shl(5, x)` is equivalent to `mul(32, x)`. 184 | m := add(tokenIds, shl(5, add(tokenIdsMaxLength, 1))) 185 | mstore(0x40, m) 186 | } 187 | // We need to call `explicitOwnershipOf(start)`, 188 | // because the slot at `start` may not be initialized. 189 | TokenOwnership memory ownership = explicitOwnershipOf(start); 190 | address currOwnershipAddr; 191 | // If the starting slot exists (i.e. not burned), 192 | // initialize `currOwnershipAddr`. 193 | // `ownership.address` will not be zero, 194 | // as `start` is clamped to the valid token ID range. 195 | if (!ownership.burned) currOwnershipAddr = ownership.addr; 196 | uint256 tokenIdsIdx; 197 | // Use a do-while, which is slightly more efficient for this case, 198 | // as the array will at least contain one element. 199 | do { 200 | if (_sequentialUpTo() != type(uint256).max) { 201 | // Skip the remaining unused sequential slots. 202 | if (start == nextTokenId) start = _sequentialUpTo() + 1; 203 | // Reset `currOwnershipAddr`, as each spot-minted token is a batch of one. 204 | if (start > _sequentialUpTo()) currOwnershipAddr = address(0); 205 | } 206 | ownership = _ownershipAt(start); // This implicitly allocates memory. 207 | assembly { 208 | switch mload(add(ownership, 0x40)) 209 | // if `ownership.burned == false`. 210 | case 0 { 211 | // if `ownership.addr != address(0)`. 212 | // The `addr` already has it's upper 96 bits clearned, 213 | // since it is written to memory with regular Solidity. 214 | if mload(ownership) { 215 | currOwnershipAddr := mload(ownership) 216 | } 217 | // if `currOwnershipAddr == owner`. 218 | // The `shl(96, x)` is to make the comparison agnostic to any 219 | // dirty upper 96 bits in `owner`. 220 | if iszero(shl(96, xor(currOwnershipAddr, owner))) { 221 | tokenIdsIdx := add(tokenIdsIdx, 1) 222 | mstore(add(tokenIds, shl(5, tokenIdsIdx)), start) 223 | } 224 | } 225 | // Otherwise, reset `currOwnershipAddr`. 226 | // This handles the case of batch burned tokens 227 | // (burned bit of first slot set, remaining slots left uninitialized). 228 | default { 229 | currOwnershipAddr := 0 230 | } 231 | start := add(start, 1) 232 | // Free temporary memory implicitly allocated for ownership 233 | // to avoid quadratic memory expansion costs. 234 | mstore(0x40, m) 235 | } 236 | } while (!(start == stop || tokenIdsIdx == tokenIdsMaxLength)); 237 | // Store the length of the array. 238 | assembly { 239 | mstore(tokenIds, tokenIdsIdx) 240 | } 241 | } 242 | } 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /contracts/extensions/IERC4907AUpgradeable.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 '../IERC721AUpgradeable.sol'; 8 | 9 | /** 10 | * @dev Interface of ERC4907A. 11 | */ 12 | interface IERC4907AUpgradeable is IERC721AUpgradeable { 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/IERC721ABurnableUpgradeable.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 '../IERC721AUpgradeable.sol'; 8 | 9 | /** 10 | * @dev Interface of ERC721ABurnable. 11 | */ 12 | interface IERC721ABurnableUpgradeable is IERC721AUpgradeable { 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/IERC721AQueryableUpgradeable.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 '../IERC721AUpgradeable.sol'; 8 | 9 | /** 10 | * @dev Interface of ERC721AQueryable. 11 | */ 12 | interface IERC721AQueryableUpgradeable is IERC721AUpgradeable { 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/IERC4907AUpgradeable.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/IERC4907AUpgradeable.sol'; 8 | -------------------------------------------------------------------------------- /contracts/interfaces/IERC721ABurnableUpgradeable.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/IERC721ABurnableUpgradeable.sol'; 8 | -------------------------------------------------------------------------------- /contracts/interfaces/IERC721AQueryableUpgradeable.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/IERC721AQueryableUpgradeable.sol'; 8 | -------------------------------------------------------------------------------- /contracts/interfaces/IERC721AUpgradeable.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 '../IERC721AUpgradeable.sol'; 8 | -------------------------------------------------------------------------------- /contracts/mocks/DirectBurnBitSetterHelperUpgradeable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // ERC721A Contracts v4.3.0 3 | // Creators: Chiru Labs 4 | 5 | pragma solidity ^0.8.4; 6 | import '../ERC721A__Initializable.sol'; 7 | 8 | contract DirectBurnBitSetterHelperUpgradeable is ERC721A__Initializable { 9 | function __DirectBurnBitSetterHelper_init() internal onlyInitializingERC721A { 10 | __DirectBurnBitSetterHelper_init_unchained(); 11 | } 12 | 13 | function __DirectBurnBitSetterHelper_init_unchained() internal onlyInitializingERC721A {} 14 | 15 | function directSetBurnBit(uint256 index) public virtual { 16 | bytes32 erc721aDiamondStorageSlot = keccak256('ERC721A.contracts.storage.ERC721A'); 17 | 18 | // This is `_BITMASK_BURNED` from ERC721A. 19 | uint256 bitmaskBurned = 1 << 224; 20 | // We use assembly to directly access the private mapping. 21 | assembly { 22 | // The `_packedOwnerships` mapping is at slot 4. 23 | mstore(0x20, 4) 24 | mstore(0x00, index) 25 | let ownershipStorageSlot := keccak256(0x00, 0x40) 26 | sstore(ownershipStorageSlot, or(sload(ownershipStorageSlot), bitmaskBurned)) 27 | 28 | // For diamond storage, we'll simply add the offset of the layout struct. 29 | mstore(0x20, add(erc721aDiamondStorageSlot, 4)) 30 | mstore(0x00, index) 31 | ownershipStorageSlot := keccak256(0x00, 0x40) 32 | sstore(ownershipStorageSlot, or(sload(ownershipStorageSlot), bitmaskBurned)) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /contracts/mocks/ERC4907AMockUpgradeable.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/ERC4907AUpgradeable.sol'; 8 | import '../ERC721A__Initializable.sol'; 9 | 10 | contract ERC4907AMockUpgradeable is ERC721A__Initializable, ERC721AUpgradeable, ERC4907AUpgradeable { 11 | function __ERC4907AMock_init(string memory name_, string memory symbol_) internal onlyInitializingERC721A { 12 | __ERC721A_init_unchained(name_, symbol_); 13 | __ERC4907A_init_unchained(); 14 | __ERC4907AMock_init_unchained(name_, symbol_); 15 | } 16 | 17 | function __ERC4907AMock_init_unchained(string memory, string memory) internal onlyInitializingERC721A {} 18 | 19 | function mint(address to, uint256 quantity) public { 20 | _mint(to, quantity); 21 | } 22 | 23 | function supportsInterface(bytes4 interfaceId) 24 | public 25 | view 26 | virtual 27 | override(ERC721AUpgradeable, ERC4907AUpgradeable) 28 | returns (bool) 29 | { 30 | return super.supportsInterface(interfaceId); 31 | } 32 | 33 | function explicitUserOf(uint256 tokenId) public view returns (address) { 34 | return _explicitUserOf(tokenId); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /contracts/mocks/ERC721ABurnableMockUpgradeable.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/ERC721ABurnableUpgradeable.sol'; 8 | import '../ERC721A__Initializable.sol'; 9 | 10 | contract ERC721ABurnableMockUpgradeable is ERC721A__Initializable, ERC721AUpgradeable, ERC721ABurnableUpgradeable { 11 | function __ERC721ABurnableMock_init(string memory name_, string memory symbol_) internal onlyInitializingERC721A { 12 | __ERC721A_init_unchained(name_, symbol_); 13 | __ERC721ABurnable_init_unchained(); 14 | __ERC721ABurnableMock_init_unchained(name_, symbol_); 15 | } 16 | 17 | function __ERC721ABurnableMock_init_unchained(string memory, string memory) internal onlyInitializingERC721A {} 18 | 19 | function exists(uint256 tokenId) public view returns (bool) { 20 | return _exists(tokenId); 21 | } 22 | 23 | function safeMint(address to, uint256 quantity) public { 24 | _safeMint(to, quantity); 25 | } 26 | 27 | function getOwnershipAt(uint256 index) public view returns (TokenOwnership memory) { 28 | return _ownershipAt(index); 29 | } 30 | 31 | function totalMinted() public view returns (uint256) { 32 | return _totalMinted(); 33 | } 34 | 35 | function totalBurned() public view returns (uint256) { 36 | return _totalBurned(); 37 | } 38 | 39 | function numberBurned(address owner) public view returns (uint256) { 40 | return _numberBurned(owner); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /contracts/mocks/ERC721ABurnableStartTokenIdMockUpgradeable.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 './ERC721ABurnableMockUpgradeable.sol'; 8 | import './StartTokenIdHelperUpgradeable.sol'; 9 | import '../ERC721A__Initializable.sol'; 10 | 11 | contract ERC721ABurnableStartTokenIdMockUpgradeable is 12 | ERC721A__Initializable, 13 | StartTokenIdHelperUpgradeable, 14 | ERC721ABurnableMockUpgradeable 15 | { 16 | function __ERC721ABurnableStartTokenIdMock_init( 17 | string memory name_, 18 | string memory symbol_, 19 | uint256 startTokenId_ 20 | ) internal onlyInitializingERC721A { 21 | __StartTokenIdHelper_init_unchained(startTokenId_); 22 | __ERC721A_init_unchained(name_, symbol_); 23 | __ERC721ABurnable_init_unchained(); 24 | __ERC721ABurnableMock_init_unchained(name_, symbol_); 25 | __ERC721ABurnableStartTokenIdMock_init_unchained(name_, symbol_, startTokenId_); 26 | } 27 | 28 | function __ERC721ABurnableStartTokenIdMock_init_unchained( 29 | string memory, 30 | string memory, 31 | uint256 32 | ) internal onlyInitializingERC721A {} 33 | 34 | function _startTokenId() internal view override returns (uint256) { 35 | return startTokenId(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /contracts/mocks/ERC721AGasReporterMockUpgradeable.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 '../ERC721AUpgradeable.sol'; 8 | import '../ERC721A__Initializable.sol'; 9 | 10 | contract ERC721AGasReporterMockUpgradeable is ERC721A__Initializable, ERC721AUpgradeable { 11 | function __ERC721AGasReporterMock_init(string memory name_, string memory symbol_) 12 | internal 13 | onlyInitializingERC721A 14 | { 15 | __ERC721A_init_unchained(name_, symbol_); 16 | __ERC721AGasReporterMock_init_unchained(name_, symbol_); 17 | } 18 | 19 | function __ERC721AGasReporterMock_init_unchained(string memory, string memory) internal onlyInitializingERC721A {} 20 | 21 | function safeMintOne(address to) public { 22 | _safeMint(to, 1); 23 | } 24 | 25 | function mintOne(address to) public { 26 | _mint(to, 1); 27 | } 28 | 29 | function safeMintTen(address to) public { 30 | _safeMint(to, 10); 31 | } 32 | 33 | function mintTen(address to) public { 34 | _mint(to, 10); 35 | } 36 | 37 | function transferTenAsc(address to) public { 38 | unchecked { 39 | transferFrom(msg.sender, to, 0); 40 | transferFrom(msg.sender, to, 1); 41 | transferFrom(msg.sender, to, 2); 42 | transferFrom(msg.sender, to, 3); 43 | transferFrom(msg.sender, to, 4); 44 | transferFrom(msg.sender, to, 5); 45 | transferFrom(msg.sender, to, 6); 46 | transferFrom(msg.sender, to, 7); 47 | transferFrom(msg.sender, to, 8); 48 | transferFrom(msg.sender, to, 9); 49 | } 50 | } 51 | 52 | function transferTenDesc(address to) public { 53 | unchecked { 54 | transferFrom(msg.sender, to, 9); 55 | transferFrom(msg.sender, to, 8); 56 | transferFrom(msg.sender, to, 7); 57 | transferFrom(msg.sender, to, 6); 58 | transferFrom(msg.sender, to, 5); 59 | transferFrom(msg.sender, to, 4); 60 | transferFrom(msg.sender, to, 3); 61 | transferFrom(msg.sender, to, 2); 62 | transferFrom(msg.sender, to, 1); 63 | transferFrom(msg.sender, to, 0); 64 | } 65 | } 66 | 67 | function transferTenAvg(address to) public { 68 | unchecked { 69 | transferFrom(msg.sender, to, 4); 70 | transferFrom(msg.sender, to, 5); 71 | transferFrom(msg.sender, to, 3); 72 | transferFrom(msg.sender, to, 6); 73 | transferFrom(msg.sender, to, 2); 74 | transferFrom(msg.sender, to, 7); 75 | transferFrom(msg.sender, to, 1); 76 | transferFrom(msg.sender, to, 8); 77 | transferFrom(msg.sender, to, 0); 78 | transferFrom(msg.sender, to, 9); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /contracts/mocks/ERC721AMockUpgradeable.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 '../ERC721AUpgradeable.sol'; 8 | import './DirectBurnBitSetterHelperUpgradeable.sol'; 9 | import '../ERC721A__Initializable.sol'; 10 | 11 | contract ERC721AMockUpgradeable is ERC721A__Initializable, ERC721AUpgradeable, DirectBurnBitSetterHelperUpgradeable { 12 | function __ERC721AMock_init(string memory name_, string memory symbol_) internal onlyInitializingERC721A { 13 | __ERC721A_init_unchained(name_, symbol_); 14 | __DirectBurnBitSetterHelper_init_unchained(); 15 | __ERC721AMock_init_unchained(name_, symbol_); 16 | } 17 | 18 | function __ERC721AMock_init_unchained(string memory, string memory) internal onlyInitializingERC721A {} 19 | 20 | function numberMinted(address owner) public view returns (uint256) { 21 | return _numberMinted(owner); 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 nextTokenId() public view returns (uint256) { 33 | return _nextTokenId(); 34 | } 35 | 36 | function getAux(address owner) public view returns (uint64) { 37 | return _getAux(owner); 38 | } 39 | 40 | function setAux(address owner, uint64 aux) public { 41 | _setAux(owner, aux); 42 | } 43 | 44 | function directApprove(address to, uint256 tokenId) public { 45 | _approve(to, tokenId); 46 | } 47 | 48 | function baseURI() public view returns (string memory) { 49 | return _baseURI(); 50 | } 51 | 52 | function exists(uint256 tokenId) public view returns (bool) { 53 | return _exists(tokenId); 54 | } 55 | 56 | function safeMint(address to, uint256 quantity) public { 57 | _safeMint(to, quantity); 58 | } 59 | 60 | function safeMint( 61 | address to, 62 | uint256 quantity, 63 | bytes memory _data 64 | ) public { 65 | _safeMint(to, quantity, _data); 66 | } 67 | 68 | function mint(address to, uint256 quantity) public { 69 | _mint(to, quantity); 70 | } 71 | 72 | function burn(uint256 tokenId) public { 73 | _burn(tokenId); 74 | } 75 | 76 | function burn(uint256 tokenId, bool approvalCheck) public { 77 | _burn(tokenId, approvalCheck); 78 | } 79 | 80 | function toString(uint256 x) public pure returns (string memory) { 81 | return _toString(x); 82 | } 83 | 84 | function getOwnershipAt(uint256 index) public view returns (TokenOwnership memory) { 85 | return _ownershipAt(index); 86 | } 87 | 88 | function getOwnershipOf(uint256 index) public view returns (TokenOwnership memory) { 89 | return _ownershipOf(index); 90 | } 91 | 92 | function initializeOwnershipAt(uint256 index) public { 93 | _initializeOwnershipAt(index); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /contracts/mocks/ERC721AQueryableMockUpgradeable.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/ERC721AQueryableUpgradeable.sol'; 8 | import '../extensions/ERC721ABurnableUpgradeable.sol'; 9 | import './DirectBurnBitSetterHelperUpgradeable.sol'; 10 | import '../ERC721A__Initializable.sol'; 11 | 12 | contract ERC721AQueryableMockUpgradeable is 13 | ERC721A__Initializable, 14 | ERC721AQueryableUpgradeable, 15 | ERC721ABurnableUpgradeable, 16 | DirectBurnBitSetterHelperUpgradeable 17 | { 18 | function __ERC721AQueryableMock_init(string memory name_, string memory symbol_) internal onlyInitializingERC721A { 19 | __ERC721A_init_unchained(name_, symbol_); 20 | __ERC721AQueryable_init_unchained(); 21 | __ERC721ABurnable_init_unchained(); 22 | __DirectBurnBitSetterHelper_init_unchained(); 23 | __ERC721AQueryableMock_init_unchained(name_, symbol_); 24 | } 25 | 26 | function __ERC721AQueryableMock_init_unchained(string memory, string memory) internal onlyInitializingERC721A {} 27 | 28 | function safeMint(address to, uint256 quantity) public { 29 | _safeMint(to, quantity); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /contracts/mocks/ERC721AQueryableStartTokenIdMockUpgradeable.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 './ERC721AQueryableMockUpgradeable.sol'; 8 | import './StartTokenIdHelperUpgradeable.sol'; 9 | import '../ERC721A__Initializable.sol'; 10 | 11 | contract ERC721AQueryableStartTokenIdMockUpgradeable is 12 | ERC721A__Initializable, 13 | StartTokenIdHelperUpgradeable, 14 | ERC721AQueryableMockUpgradeable 15 | { 16 | function __ERC721AQueryableStartTokenIdMock_init( 17 | string memory name_, 18 | string memory symbol_, 19 | uint256 startTokenId_ 20 | ) internal onlyInitializingERC721A { 21 | __StartTokenIdHelper_init_unchained(startTokenId_); 22 | __ERC721A_init_unchained(name_, symbol_); 23 | __ERC721AQueryable_init_unchained(); 24 | __ERC721ABurnable_init_unchained(); 25 | __DirectBurnBitSetterHelper_init_unchained(); 26 | __ERC721AQueryableMock_init_unchained(name_, symbol_); 27 | __ERC721AQueryableStartTokenIdMock_init_unchained(name_, symbol_, startTokenId_); 28 | } 29 | 30 | function __ERC721AQueryableStartTokenIdMock_init_unchained( 31 | string memory, 32 | string memory, 33 | uint256 34 | ) internal onlyInitializingERC721A {} 35 | 36 | function _startTokenId() internal view override returns (uint256) { 37 | return startTokenId(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /contracts/mocks/ERC721ASpotMockUpgradeable.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 './ERC721AQueryableMockUpgradeable.sol'; 8 | import './StartTokenIdHelperUpgradeable.sol'; 9 | import './SequentialUpToHelperUpgradeable.sol'; 10 | import '../ERC721A__Initializable.sol'; 11 | 12 | contract ERC721ASpotMockUpgradeable is 13 | ERC721A__Initializable, 14 | StartTokenIdHelperUpgradeable, 15 | SequentialUpToHelperUpgradeable, 16 | ERC721AQueryableMockUpgradeable 17 | { 18 | function __ERC721ASpotMock_init( 19 | string memory name_, 20 | string memory symbol_, 21 | uint256 startTokenId_, 22 | uint256 sequentialUpTo_, 23 | uint256 quantity, 24 | bool mintInConstructor 25 | ) internal onlyInitializingERC721A { 26 | __StartTokenIdHelper_init_unchained(startTokenId_); 27 | __SequentialUpToHelper_init_unchained(sequentialUpTo_); 28 | __ERC721A_init_unchained(name_, symbol_); 29 | __ERC721AQueryable_init_unchained(); 30 | __ERC721ABurnable_init_unchained(); 31 | __DirectBurnBitSetterHelper_init_unchained(); 32 | __ERC721AQueryableMock_init_unchained(name_, symbol_); 33 | __ERC721ASpotMock_init_unchained(name_, symbol_, startTokenId_, sequentialUpTo_, quantity, mintInConstructor); 34 | } 35 | 36 | function __ERC721ASpotMock_init_unchained( 37 | string memory, 38 | string memory, 39 | uint256, 40 | uint256, 41 | uint256 quantity, 42 | bool mintInConstructor 43 | ) internal onlyInitializingERC721A { 44 | if (mintInConstructor) { 45 | _mintERC2309(msg.sender, quantity); 46 | } 47 | } 48 | 49 | function _startTokenId() internal view override returns (uint256) { 50 | return startTokenId(); 51 | } 52 | 53 | function _sequentialUpTo() internal view override returns (uint256) { 54 | return sequentialUpTo(); 55 | } 56 | 57 | function exists(uint256 tokenId) public view returns (bool) { 58 | return _exists(tokenId); 59 | } 60 | 61 | function getOwnershipOf(uint256 index) public view returns (TokenOwnership memory) { 62 | return _ownershipOf(index); 63 | } 64 | 65 | function safeMintSpot(address to, uint256 tokenId) public { 66 | _safeMintSpot(to, tokenId); 67 | } 68 | 69 | function totalSpotMinted() public view returns (uint256) { 70 | return _totalSpotMinted(); 71 | } 72 | 73 | function totalMinted() public view returns (uint256) { 74 | return _totalMinted(); 75 | } 76 | 77 | function totalBurned() public view returns (uint256) { 78 | return _totalBurned(); 79 | } 80 | 81 | function numberBurned(address owner) public view returns (uint256) { 82 | return _numberBurned(owner); 83 | } 84 | 85 | function setExtraDataAt(uint256 tokenId, uint24 value) public { 86 | _setExtraDataAt(tokenId, value); 87 | } 88 | 89 | function _extraData( 90 | address, 91 | address, 92 | uint24 previousExtraData 93 | ) internal view virtual override returns (uint24) { 94 | return previousExtraData; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /contracts/mocks/ERC721AStartTokenIdMockUpgradeable.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 './ERC721AMockUpgradeable.sol'; 8 | import './StartTokenIdHelperUpgradeable.sol'; 9 | import '../ERC721A__Initializable.sol'; 10 | 11 | contract ERC721AStartTokenIdMockUpgradeable is 12 | ERC721A__Initializable, 13 | StartTokenIdHelperUpgradeable, 14 | ERC721AMockUpgradeable 15 | { 16 | function __ERC721AStartTokenIdMock_init( 17 | string memory name_, 18 | string memory symbol_, 19 | uint256 startTokenId_ 20 | ) internal onlyInitializingERC721A { 21 | __StartTokenIdHelper_init_unchained(startTokenId_); 22 | __ERC721A_init_unchained(name_, symbol_); 23 | __DirectBurnBitSetterHelper_init_unchained(); 24 | __ERC721AMock_init_unchained(name_, symbol_); 25 | __ERC721AStartTokenIdMock_init_unchained(name_, symbol_, startTokenId_); 26 | } 27 | 28 | function __ERC721AStartTokenIdMock_init_unchained( 29 | string memory, 30 | string memory, 31 | uint256 32 | ) internal onlyInitializingERC721A {} 33 | 34 | function _startTokenId() internal view override returns (uint256) { 35 | return startTokenId(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /contracts/mocks/ERC721ATransferCounterMockUpgradeable.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 './ERC721AMockUpgradeable.sol'; 8 | import '../ERC721A__Initializable.sol'; 9 | 10 | contract ERC721ATransferCounterMockUpgradeable is ERC721A__Initializable, ERC721AMockUpgradeable { 11 | function __ERC721ATransferCounterMock_init(string memory name_, string memory symbol_) 12 | internal 13 | onlyInitializingERC721A 14 | { 15 | __ERC721A_init_unchained(name_, symbol_); 16 | __DirectBurnBitSetterHelper_init_unchained(); 17 | __ERC721AMock_init_unchained(name_, symbol_); 18 | __ERC721ATransferCounterMock_init_unchained(name_, symbol_); 19 | } 20 | 21 | function __ERC721ATransferCounterMock_init_unchained(string memory, string memory) 22 | internal 23 | onlyInitializingERC721A 24 | {} 25 | 26 | function _extraData( 27 | address from, 28 | address to, 29 | uint24 previousExtraData 30 | ) internal view virtual override returns (uint24) { 31 | if (from == address(0)) { 32 | return 42; 33 | } 34 | if (to == address(0)) { 35 | return 1337; 36 | } 37 | return previousExtraData + 1; 38 | } 39 | 40 | function setExtraDataAt(uint256 index, uint24 extraData) public { 41 | _setExtraDataAt(index, extraData); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /contracts/mocks/ERC721AWithERC2309MockUpgradeable.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 '../ERC721AUpgradeable.sol'; 8 | import '../ERC721A__Initializable.sol'; 9 | 10 | contract ERC721AWithERC2309MockUpgradeable is ERC721A__Initializable, ERC721AUpgradeable { 11 | function __ERC721AWithERC2309Mock_init( 12 | string memory name_, 13 | string memory symbol_, 14 | address to, 15 | uint256 quantity, 16 | bool mintInConstructor 17 | ) internal onlyInitializingERC721A { 18 | __ERC721A_init_unchained(name_, symbol_); 19 | __ERC721AWithERC2309Mock_init_unchained(name_, symbol_, to, quantity, mintInConstructor); 20 | } 21 | 22 | function __ERC721AWithERC2309Mock_init_unchained( 23 | string memory, 24 | string memory, 25 | address to, 26 | uint256 quantity, 27 | bool mintInConstructor 28 | ) internal onlyInitializingERC721A { 29 | if (mintInConstructor) { 30 | _mintERC2309(to, quantity); 31 | } 32 | } 33 | 34 | /** 35 | * @dev This function is only for gas comparison purposes. 36 | * Calling `_mintERC3201` outside of contract creation is non-compliant 37 | * with the ERC721 standard. 38 | */ 39 | function mintOneERC2309(address to) public { 40 | _mintERC2309(to, 1); 41 | } 42 | 43 | /** 44 | * @dev This function is only for gas comparison purposes. 45 | * Calling `_mintERC3201` outside of contract creation is non-compliant 46 | * with the ERC721 standard. 47 | */ 48 | function mintTenERC2309(address to) public { 49 | _mintERC2309(to, 10); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /contracts/mocks/ERC721ReceiverMockStorage.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import {ERC721ReceiverMockUpgradeable} from './ERC721ReceiverMockUpgradeable.sol'; 6 | 7 | library ERC721ReceiverMockStorage { 8 | struct Layout { 9 | bytes4 _retval; 10 | address _erc721aMock; 11 | } 12 | 13 | bytes32 internal constant STORAGE_SLOT = keccak256('ERC721A.contracts.storage.ERC721ReceiverMock'); 14 | 15 | function layout() internal pure returns (Layout storage l) { 16 | bytes32 slot = STORAGE_SLOT; 17 | assembly { 18 | l.slot := slot 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /contracts/mocks/ERC721ReceiverMockUpgradeable.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 '../ERC721AUpgradeable.sol'; 8 | import {ERC721ReceiverMockStorage} from './ERC721ReceiverMockStorage.sol'; 9 | import '../ERC721A__Initializable.sol'; 10 | 11 | interface IERC721AMockUpgradeable { 12 | function safeMint(address to, uint256 quantity) external; 13 | } 14 | 15 | contract ERC721ReceiverMockUpgradeable is ERC721A__Initializable, ERC721A__IERC721ReceiverUpgradeable { 16 | using ERC721ReceiverMockStorage for ERC721ReceiverMockStorage.Layout; 17 | enum Error { 18 | None, 19 | RevertWithMessage, 20 | RevertWithoutMessage, 21 | Panic 22 | } 23 | 24 | event Received(address operator, address from, uint256 tokenId, bytes data, uint256 gas); 25 | 26 | function __ERC721ReceiverMock_init(bytes4 retval, address erc721aMock) internal onlyInitializingERC721A { 27 | __ERC721ReceiverMock_init_unchained(retval, erc721aMock); 28 | } 29 | 30 | function __ERC721ReceiverMock_init_unchained(bytes4 retval, address erc721aMock) internal onlyInitializingERC721A { 31 | ERC721ReceiverMockStorage.layout()._retval = retval; 32 | ERC721ReceiverMockStorage.layout()._erc721aMock = erc721aMock; 33 | } 34 | 35 | function onERC721Received( 36 | address operator, 37 | address from, 38 | uint256 tokenId, 39 | bytes memory data 40 | ) public override returns (bytes4) { 41 | uint256 dataValue = data.length == 0 ? 0 : uint256(uint8(data[0])); 42 | 43 | // For testing reverts with a message from the receiver contract. 44 | if (dataValue == 0x01) { 45 | revert('reverted in the receiver contract!'); 46 | } 47 | 48 | // For testing with the returned wrong value from the receiver contract. 49 | if (dataValue == 0x02) { 50 | return 0x0; 51 | } 52 | 53 | // For testing the reentrancy protection. 54 | if (dataValue == 0x03) { 55 | IERC721AMockUpgradeable(ERC721ReceiverMockStorage.layout()._erc721aMock).safeMint(address(this), 1); 56 | } 57 | 58 | emit Received(operator, from, tokenId, data, 20000); 59 | return ERC721ReceiverMockStorage.layout()._retval; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /contracts/mocks/SequentialUpToHelperUpgradeable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // ERC721A Contracts v4.3.0 3 | // Creators: Chiru Labs 4 | 5 | pragma solidity ^0.8.4; 6 | import '../ERC721A__Initializable.sol'; 7 | 8 | /** 9 | * This Helper is used to return a dynamic value in the overridden _sequentialUpTo() function. 10 | * Extending this Helper before the ERC721A contract give us access to the herein set `sequentialUpTo` 11 | * to be returned by the overridden `_sequentialUpTo()` function of ERC721A in the ERC721ASpot mocks. 12 | */ 13 | contract SequentialUpToHelperUpgradeable is ERC721A__Initializable { 14 | // `bytes4(keccak256('sequentialUpTo'))`. 15 | uint256 private constant SEQUENTIAL_UP_TO_STORAGE_SLOT = 0x9638c59e; 16 | 17 | function __SequentialUpToHelper_init(uint256 sequentialUpTo_) internal onlyInitializingERC721A { 18 | __SequentialUpToHelper_init_unchained(sequentialUpTo_); 19 | } 20 | 21 | function __SequentialUpToHelper_init_unchained(uint256 sequentialUpTo_) internal onlyInitializingERC721A { 22 | _initializeSequentialUpTo(sequentialUpTo_); 23 | } 24 | 25 | function sequentialUpTo() public view returns (uint256 result) { 26 | assembly { 27 | result := sload(SEQUENTIAL_UP_TO_STORAGE_SLOT) 28 | } 29 | } 30 | 31 | function _initializeSequentialUpTo(uint256 value) private { 32 | // We use assembly to directly set the `sequentialUpTo` in storage so that 33 | // inheriting this class won't affect the layout of other storage slots. 34 | assembly { 35 | sstore(SEQUENTIAL_UP_TO_STORAGE_SLOT, value) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /contracts/mocks/StartTokenIdHelperUpgradeable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // ERC721A Contracts v4.3.0 3 | // Creators: Chiru Labs 4 | 5 | pragma solidity ^0.8.4; 6 | import '../ERC721A__Initializable.sol'; 7 | 8 | /** 9 | * This Helper is used to return a dynamic value in the overridden _startTokenId() function. 10 | * Extending this Helper before the ERC721A contract give us access to the herein set `startTokenId` 11 | * to be returned by the overridden `_startTokenId()` function of ERC721A in the ERC721AStartTokenId mocks. 12 | */ 13 | contract StartTokenIdHelperUpgradeable is ERC721A__Initializable { 14 | // `bytes4(keccak256('startTokenId'))`. 15 | uint256 private constant _START_TOKEN_ID_STORAGE_SLOT = 0x28f75032; 16 | 17 | function __StartTokenIdHelper_init(uint256 startTokenId_) internal onlyInitializingERC721A { 18 | __StartTokenIdHelper_init_unchained(startTokenId_); 19 | } 20 | 21 | function __StartTokenIdHelper_init_unchained(uint256 startTokenId_) internal onlyInitializingERC721A { 22 | _initializeStartTokenId(startTokenId_); 23 | } 24 | 25 | function startTokenId() public view returns (uint256 result) { 26 | assembly { 27 | result := sload(_START_TOKEN_ID_STORAGE_SLOT) 28 | } 29 | } 30 | 31 | function _initializeStartTokenId(uint256 value) private { 32 | // We use assembly to directly set the `startTokenId` in storage so that 33 | // inheriting this class won't affect the layout of other storage slots. 34 | assembly { 35 | sstore(_START_TOKEN_ID_STORAGE_SLOT, value) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /contracts/mocks/WithInit.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.7 <0.9; 3 | pragma experimental ABIEncoderV2; 4 | 5 | import '../ERC721AUpgradeable.sol'; 6 | 7 | contract ERC721AUpgradeableWithInit is ERC721AUpgradeable { 8 | constructor(string memory name_, string memory symbol_) payable initializerERC721A { 9 | __ERC721A_init(name_, symbol_); 10 | } 11 | } 12 | import './ERC721ReceiverMockUpgradeable.sol'; 13 | 14 | contract ERC721ReceiverMockUpgradeableWithInit is ERC721ReceiverMockUpgradeable { 15 | constructor(bytes4 retval, address erc721aMock) payable initializerERC721A { 16 | __ERC721ReceiverMock_init(retval, erc721aMock); 17 | } 18 | } 19 | import './ERC721AWithERC2309MockUpgradeable.sol'; 20 | 21 | contract ERC721AWithERC2309MockUpgradeableWithInit is ERC721AWithERC2309MockUpgradeable { 22 | constructor( 23 | string memory name_, 24 | string memory symbol_, 25 | address to, 26 | uint256 quantity, 27 | bool mintInConstructor 28 | ) payable initializerERC721A { 29 | __ERC721AWithERC2309Mock_init(name_, symbol_, to, quantity, mintInConstructor); 30 | } 31 | } 32 | import './ERC721AMockUpgradeable.sol'; 33 | 34 | contract ERC721AMockUpgradeableWithInit is ERC721AMockUpgradeable { 35 | constructor(string memory name_, string memory symbol_) payable initializerERC721A { 36 | __ERC721AMock_init(name_, symbol_); 37 | } 38 | } 39 | import './DirectBurnBitSetterHelperUpgradeable.sol'; 40 | 41 | contract DirectBurnBitSetterHelperUpgradeableWithInit is DirectBurnBitSetterHelperUpgradeable { 42 | constructor() payable initializerERC721A { 43 | __DirectBurnBitSetterHelper_init(); 44 | } 45 | } 46 | import './ERC721ATransferCounterMockUpgradeable.sol'; 47 | 48 | contract ERC721ATransferCounterMockUpgradeableWithInit is ERC721ATransferCounterMockUpgradeable { 49 | constructor(string memory name_, string memory symbol_) payable initializerERC721A { 50 | __ERC721ATransferCounterMock_init(name_, symbol_); 51 | } 52 | } 53 | import './ERC721AStartTokenIdMockUpgradeable.sol'; 54 | 55 | contract ERC721AStartTokenIdMockUpgradeableWithInit is ERC721AStartTokenIdMockUpgradeable { 56 | constructor( 57 | string memory name_, 58 | string memory symbol_, 59 | uint256 startTokenId_ 60 | ) payable initializerERC721A { 61 | __ERC721AStartTokenIdMock_init(name_, symbol_, startTokenId_); 62 | } 63 | } 64 | import './StartTokenIdHelperUpgradeable.sol'; 65 | 66 | contract StartTokenIdHelperUpgradeableWithInit is StartTokenIdHelperUpgradeable { 67 | constructor(uint256 startTokenId_) payable initializerERC721A { 68 | __StartTokenIdHelper_init(startTokenId_); 69 | } 70 | } 71 | import './ERC721ASpotMockUpgradeable.sol'; 72 | 73 | contract ERC721ASpotMockUpgradeableWithInit is ERC721ASpotMockUpgradeable { 74 | constructor( 75 | string memory name_, 76 | string memory symbol_, 77 | uint256 startTokenId_, 78 | uint256 sequentialUpTo_, 79 | uint256 quantity, 80 | bool mintInConstructor 81 | ) payable initializerERC721A { 82 | __ERC721ASpotMock_init(name_, symbol_, startTokenId_, sequentialUpTo_, quantity, mintInConstructor); 83 | } 84 | } 85 | import './ERC721AQueryableMockUpgradeable.sol'; 86 | 87 | contract ERC721AQueryableMockUpgradeableWithInit is ERC721AQueryableMockUpgradeable { 88 | constructor(string memory name_, string memory symbol_) payable initializerERC721A { 89 | __ERC721AQueryableMock_init(name_, symbol_); 90 | } 91 | } 92 | import './SequentialUpToHelperUpgradeable.sol'; 93 | 94 | contract SequentialUpToHelperUpgradeableWithInit is SequentialUpToHelperUpgradeable { 95 | constructor(uint256 sequentialUpTo_) payable initializerERC721A { 96 | __SequentialUpToHelper_init(sequentialUpTo_); 97 | } 98 | } 99 | import './ERC721AQueryableStartTokenIdMockUpgradeable.sol'; 100 | 101 | contract ERC721AQueryableStartTokenIdMockUpgradeableWithInit is ERC721AQueryableStartTokenIdMockUpgradeable { 102 | constructor( 103 | string memory name_, 104 | string memory symbol_, 105 | uint256 startTokenId_ 106 | ) payable initializerERC721A { 107 | __ERC721AQueryableStartTokenIdMock_init(name_, symbol_, startTokenId_); 108 | } 109 | } 110 | import './ERC721ABurnableMockUpgradeable.sol'; 111 | 112 | contract ERC721ABurnableMockUpgradeableWithInit is ERC721ABurnableMockUpgradeable { 113 | constructor(string memory name_, string memory symbol_) payable initializerERC721A { 114 | __ERC721ABurnableMock_init(name_, symbol_); 115 | } 116 | } 117 | import './ERC721ABurnableStartTokenIdMockUpgradeable.sol'; 118 | 119 | contract ERC721ABurnableStartTokenIdMockUpgradeableWithInit is ERC721ABurnableStartTokenIdMockUpgradeable { 120 | constructor( 121 | string memory name_, 122 | string memory symbol_, 123 | uint256 startTokenId_ 124 | ) payable initializerERC721A { 125 | __ERC721ABurnableStartTokenIdMock_init(name_, symbol_, startTokenId_); 126 | } 127 | } 128 | import './ERC4907AMockUpgradeable.sol'; 129 | 130 | contract ERC4907AMockUpgradeableWithInit is ERC4907AMockUpgradeable { 131 | constructor(string memory name_, string memory symbol_) payable initializerERC721A { 132 | __ERC4907AMock_init(name_, symbol_); 133 | } 134 | } 135 | import './ERC721AGasReporterMockUpgradeable.sol'; 136 | 137 | contract ERC721AGasReporterMockUpgradeableWithInit is ERC721AGasReporterMockUpgradeable { 138 | constructor(string memory name_, string memory symbol_) payable initializerERC721A { 139 | __ERC721AGasReporterMock_init(name_, symbol_); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /hardhat.config.js: -------------------------------------------------------------------------------- 1 | require('@nomiclabs/hardhat-waffle'); 2 | require('@nomiclabs/hardhat-ethers'); 3 | 4 | const { internalTask } = require('hardhat/config'); 5 | const { TASK_COMPILE_SOLIDITY_GET_COMPILER_INPUT } = require('hardhat/builtin-tasks/task-names'); 6 | 7 | if (process.env.REPORT_GAS) { 8 | require('hardhat-gas-reporter'); 9 | } 10 | 11 | if (process.env.REPORT_COVERAGE) { 12 | require('solidity-coverage'); 13 | } 14 | 15 | internalTask(TASK_COMPILE_SOLIDITY_GET_COMPILER_INPUT, async (args, hre, runSuper) => { 16 | const input = await runSuper(); 17 | input.settings.outputSelection['*']['*'].push('storageLayout'); 18 | return input; 19 | }); 20 | 21 | /** 22 | * @type import('hardhat/config').HardhatUserConfig 23 | */ 24 | module.exports = { 25 | solidity: { 26 | version: '0.8.11', 27 | settings: { 28 | optimizer: { 29 | enabled: true, 30 | runs: 800, 31 | }, 32 | }, 33 | }, 34 | gasReporter: { 35 | currency: 'USD', 36 | gasPrice: 100, 37 | showTimeSpent: true, 38 | }, 39 | plugins: ['solidity-coverage'], 40 | }; 41 | 42 | // The "ripemd160" algorithm is not available anymore in NodeJS 17+ (because of lib SSL 3). 43 | // The following code replaces it with "sha256" instead. 44 | 45 | const crypto = require('crypto'); 46 | 47 | try { 48 | crypto.createHash('ripemd160'); 49 | } catch (e) { 50 | const origCreateHash = crypto.createHash; 51 | crypto.createHash = (alg, opts) => { 52 | return origCreateHash(alg === 'ripemd160' ? 'sha256' : alg, opts); 53 | }; 54 | } 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "erc721a-upgradeable", 3 | "version": "4.3.0", 4 | "description": "ERC721A Upgradeable 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 | }, 19 | "devDependencies": { 20 | "@openzeppelin/contracts": "^4.4.2", 21 | "@nomiclabs/hardhat-ethers": "^2.0.4", 22 | "@nomiclabs/hardhat-waffle": "^2.0.1", 23 | "@openzeppelin/test-helpers": "^0.5.15", 24 | "@gnus.ai/upgrade-safe-transpiler-diamond": "latest", 25 | "chai": "^4.3.4", 26 | "eslint": "^8.7.0", 27 | "eslint-plugin-mocha": "^10.0.3", 28 | "eslint-plugin-node": "^11.1.0", 29 | "ethereum-waffle": "^3.4.0", 30 | "ethers": "^5.5.3", 31 | "hardhat": "^2.8.2", 32 | "hardhat-gas-reporter": "^1.0.7", 33 | "prettier": "^2.5.1", 34 | "prettier-plugin-solidity": "^1.0.0-beta.19", 35 | "solidity-coverage": "^0.7.20" 36 | }, 37 | "repository": { 38 | "type": "git", 39 | "url": "git+https://github.com/chiru-labs/ERC721A-Upgradeable.git" 40 | }, 41 | "author": "chiru-labs", 42 | "license": "ISC", 43 | "bugs": { 44 | "url": "https://github.com/chiru-labs/ERC721A-Upgradeable/issues" 45 | }, 46 | "homepage": "https://github.com/chiru-labs/ERC721A-Upgradeable#readme" 47 | } 48 | -------------------------------------------------------------------------------- /scripts/replace-imports.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const fs = require('fs'); 3 | const glob = require('glob'); 4 | 5 | // Rename files. 6 | fs.renameSync('contracts/Initializable.sol', 'contracts/ERC721A__Initializable.sol'); 7 | fs.renameSync('contracts/InitializableStorage.sol', 'contracts/ERC721A__InitializableStorage.sol'); 8 | 9 | // Loop through all files with contracts/**/*.sol pattern. 10 | glob('contracts/**/*.sol', null, function (err, files) { 11 | files.forEach((file) => { 12 | // Read file content. 13 | const content = fs.readFileSync(file, 'utf8'); 14 | 15 | const updatedContent = content 16 | .replace(/open.*?torage\./g, 'ERC721A.contracts.storage.') 17 | .replace(/modifier initializer/g, 'modifier initializerERC721A') 18 | .replace(/initializer\s*?\{/g, 'initializerERC721A {') 19 | .replace(/modifier onlyInitializing/g, 'modifier onlyInitializingERC721A') 20 | .replace(/onlyInitializing\s*?\{/g, 'onlyInitializingERC721A {') 21 | .replace(/Initializable/g, 'ERC721A__Initializable'); 22 | 23 | // Write updated file. 24 | fs.writeFileSync(file, updatedContent); 25 | }); 26 | }); 27 | 28 | // Replace the TokenApprovalRef to break cyclic importing. 29 | let erc721aFilepath = 'contracts/ERC721AUpgradeable.sol'; 30 | let erc721aContents = fs.readFileSync(erc721aFilepath, 'utf8'); 31 | let tokenApprovalRefRe = /\/\/.*?\n\r?\s*struct TokenApprovalRef\s*\{[^}]+\}/; 32 | let tokenApprovalRefMatch = erc721aContents.match(tokenApprovalRefRe); 33 | if (tokenApprovalRefMatch) { 34 | erc721aContents = erc721aContents 35 | .replace(tokenApprovalRefMatch[0], '') 36 | .replace(/TokenApprovalRef/g, 'ERC721AStorage.TokenApprovalRef'); 37 | fs.writeFileSync(erc721aFilepath, erc721aContents); 38 | 39 | let erc721aStorageFilepath = 'contracts/ERC721AStorage.sol'; 40 | let erc721aStorageContents = fs.readFileSync(erc721aStorageFilepath, 'utf8'); 41 | erc721aStorageContents = erc721aStorageContents 42 | .replace(/struct Layout\s*\{/, tokenApprovalRefMatch[0] + '\n\n struct Layout {') 43 | .replace(/ERC721AUpgradeable.TokenApprovalRef/g, 'ERC721AStorage.TokenApprovalRef') 44 | .replace(/import.*?\.\/ERC721AUpgradeable.sol[^;]+;/, ''); 45 | 46 | fs.writeFileSync(erc721aStorageFilepath, erc721aStorageContents); 47 | } 48 | -------------------------------------------------------------------------------- /scripts/transpile-merge.sh: -------------------------------------------------------------------------------- 1 | git checkout main; 2 | 3 | # Download the latest ERC721A. 4 | echo "Getting latest ERC721A"; 5 | if [[ -f "ERC721A/package.json" ]]; then 6 | cd ERC721A; 7 | git fetch --all; 8 | git reset --hard origin/main; 9 | cd ..; 10 | else 11 | git clone https://github.com/chiru-labs/ERC721A.git; 12 | fi 13 | 14 | # Get the last commit hash of ERC721A 15 | cd ./ERC721A; 16 | commit="$(git rev-parse HEAD)"; 17 | cd ..; 18 | 19 | # Replace the contracts and test folder with the latest copy. 20 | rm -r ./contracts; 21 | rm -r ./test; 22 | rsync -av --progress ERC721A/ ./ \ 23 | --exclude README.md \ 24 | --exclude projects.md \ 25 | --exclude hardhat.config.js \ 26 | --exclude .github/ \ 27 | --exclude .git/ \ 28 | --exclude docs/ \ 29 | --exclude scripts/ \ 30 | --exclude package.json \ 31 | --exclude package-lock.json; 32 | rm -rf ./ERC721A; 33 | 34 | # Recompile the contracts. 35 | npx hardhat clean; 36 | npx hardhat compile; 37 | 38 | # Transpile. 39 | echo "Transpiling"; 40 | # -D: delete original and excluded files 41 | # -E: extract storage for Diamond Pattern 42 | npx @gnus.ai/upgrade-safe-transpiler-diamond -D -E; 43 | node scripts/replace-imports.js; 44 | (npm run lint:sol) || true; 45 | 46 | # Commit and push 47 | echo "Committing latest code"; 48 | git config user.name 'github-actions'; 49 | git config user.email '41898282+github-actions[bot]@users.noreply.github.com'; 50 | git add -A; 51 | (git commit -m "Transpile chiru-labs/ERC721A@$commit" && git push origin main) || echo "No changes to commit"; 52 | -------------------------------------------------------------------------------- /test/ERC721A.test.js: -------------------------------------------------------------------------------- 1 | const { deployContract, getBlockTimestamp, mineBlockTimestamp, 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 RECEIVER_MAGIC_VALUE = '0x150b7a02'; 8 | const GAS_MAGIC_VALUE = 20000; 9 | 10 | const createTestSuite = ({ contract, constructorArgs }) => 11 | function () { 12 | let offsetted; 13 | 14 | context(`${contract}`, function () { 15 | beforeEach(async function () { 16 | this.erc721a = await deployContract(contract, constructorArgs); 17 | this.receiver = await deployContract('ERC721ReceiverMock', [RECEIVER_MAGIC_VALUE, this.erc721a.address]); 18 | this.startTokenId = this.erc721a.startTokenId ? (await this.erc721a.startTokenId()).toNumber() : 0; 19 | 20 | offsetted = (...arr) => offsettedIndex(this.startTokenId, arr); 21 | }); 22 | 23 | describe('EIP-165 support', async function () { 24 | it('supports ERC165', async function () { 25 | expect(await this.erc721a.supportsInterface('0x01ffc9a7')).to.eq(true); 26 | }); 27 | 28 | it('supports IERC721', async function () { 29 | expect(await this.erc721a.supportsInterface('0x80ac58cd')).to.eq(true); 30 | }); 31 | 32 | it('supports ERC721Metadata', async function () { 33 | expect(await this.erc721a.supportsInterface('0x5b5e139f')).to.eq(true); 34 | }); 35 | 36 | it('does not support ERC721Enumerable', async function () { 37 | expect(await this.erc721a.supportsInterface('0x780e9d63')).to.eq(false); 38 | }); 39 | 40 | it('does not support random interface', async function () { 41 | expect(await this.erc721a.supportsInterface('0x00000042')).to.eq(false); 42 | }); 43 | }); 44 | 45 | describe('ERC721Metadata support', async function () { 46 | it('name', async function () { 47 | expect(await this.erc721a.name()).to.eq(constructorArgs[0]); 48 | }); 49 | 50 | it('symbol', async function () { 51 | expect(await this.erc721a.symbol()).to.eq(constructorArgs[1]); 52 | }); 53 | 54 | describe('baseURI', async function () { 55 | it('sends an empty URI by default', async function () { 56 | expect(await this.erc721a.baseURI()).to.eq(''); 57 | }); 58 | }); 59 | }); 60 | 61 | context('with no minted tokens', async function () { 62 | it('has 0 totalSupply', async function () { 63 | const supply = await this.erc721a.totalSupply(); 64 | expect(supply).to.equal(0); 65 | }); 66 | 67 | it('has 0 totalMinted', async function () { 68 | const totalMinted = await this.erc721a.totalMinted(); 69 | expect(totalMinted).to.equal(0); 70 | }); 71 | 72 | it('has 0 totalBurned', async function () { 73 | const totalBurned = await this.erc721a.totalBurned(); 74 | expect(totalBurned).to.equal(0); 75 | }); 76 | 77 | it('_nextTokenId must be equal to _startTokenId', async function () { 78 | const nextTokenId = await this.erc721a.nextTokenId(); 79 | expect(nextTokenId).to.equal(offsetted(0)); 80 | }); 81 | }); 82 | 83 | context('with minted tokens', async function () { 84 | beforeEach(async function () { 85 | const [owner, addr1, addr2, addr3, addr4] = await ethers.getSigners(); 86 | this.owner = owner; 87 | this.addr1 = addr1; 88 | this.addr2 = addr2; 89 | this.addr3 = addr3; 90 | this.addr4 = addr4; 91 | this.expectedMintCount = 6; 92 | 93 | this.addr1.expected = { 94 | mintCount: 1, 95 | tokens: [offsetted(0)], 96 | }; 97 | 98 | this.addr2.expected = { 99 | mintCount: 2, 100 | tokens: offsetted(1, 2), 101 | }; 102 | 103 | this.addr3.expected = { 104 | mintCount: 3, 105 | tokens: offsetted(3, 4, 5), 106 | }; 107 | 108 | await this.erc721a['safeMint(address,uint256)'](addr1.address, this.addr1.expected.mintCount); 109 | await this.erc721a['safeMint(address,uint256)'](addr2.address, this.addr2.expected.mintCount); 110 | await this.erc721a['safeMint(address,uint256)'](addr3.address, this.addr3.expected.mintCount); 111 | }); 112 | 113 | describe('tokenURI (ERC721Metadata)', async function () { 114 | describe('tokenURI', async function () { 115 | it('sends an empty uri by default', async function () { 116 | expect(await this.erc721a.tokenURI(offsetted(0))).to.eq(''); 117 | }); 118 | 119 | it('reverts when tokenId does not exist', async function () { 120 | await expect(this.erc721a.tokenURI(offsetted(this.expectedMintCount))).to.be.revertedWith( 121 | 'URIQueryForNonexistentToken' 122 | ); 123 | }); 124 | }); 125 | }); 126 | 127 | describe('exists', async function () { 128 | it('verifies valid tokens', async function () { 129 | for (let tokenId = offsetted(0); tokenId < offsetted(this.expectedMintCount); tokenId++) { 130 | const exists = await this.erc721a.exists(tokenId); 131 | expect(exists).to.be.true; 132 | } 133 | }); 134 | 135 | it('verifies invalid tokens', async function () { 136 | expect(await this.erc721a.exists(offsetted(this.expectedMintCount))).to.be.false; 137 | }); 138 | }); 139 | 140 | describe('balanceOf', async function () { 141 | it('returns the amount for a given address', async function () { 142 | expect(await this.erc721a.balanceOf(this.owner.address)).to.equal('0'); 143 | expect(await this.erc721a.balanceOf(this.addr1.address)).to.equal(this.addr1.expected.mintCount); 144 | expect(await this.erc721a.balanceOf(this.addr2.address)).to.equal(this.addr2.expected.mintCount); 145 | expect(await this.erc721a.balanceOf(this.addr3.address)).to.equal(this.addr3.expected.mintCount); 146 | }); 147 | 148 | it('returns correct amount with transferred tokens', async function () { 149 | const tokenIdToTransfer = this.addr2.expected.tokens[0]; 150 | await this.erc721a 151 | .connect(this.addr2) 152 | .transferFrom(this.addr2.address, this.addr3.address, tokenIdToTransfer); 153 | // sanity check 154 | expect(await this.erc721a.ownerOf(tokenIdToTransfer)).to.equal(this.addr3.address); 155 | 156 | expect(await this.erc721a.balanceOf(this.addr2.address)).to.equal(this.addr2.expected.mintCount - 1); 157 | expect(await this.erc721a.balanceOf(this.addr3.address)).to.equal(this.addr3.expected.mintCount + 1); 158 | }); 159 | 160 | it('throws an exception for the 0 address', async function () { 161 | await expect(this.erc721a.balanceOf(ZERO_ADDRESS)).to.be.revertedWith('BalanceQueryForZeroAddress'); 162 | }); 163 | }); 164 | 165 | describe('_numberMinted', async function () { 166 | it('returns the amount for a given address', async function () { 167 | expect(await this.erc721a.numberMinted(this.owner.address)).to.equal('0'); 168 | expect(await this.erc721a.numberMinted(this.addr1.address)).to.equal(this.addr1.expected.mintCount); 169 | expect(await this.erc721a.numberMinted(this.addr2.address)).to.equal(this.addr2.expected.mintCount); 170 | expect(await this.erc721a.numberMinted(this.addr3.address)).to.equal(this.addr3.expected.mintCount); 171 | }); 172 | 173 | it('returns the same amount with transferred token', async function () { 174 | const tokenIdToTransfer = this.addr2.expected.tokens[0]; 175 | await this.erc721a 176 | .connect(this.addr2) 177 | .transferFrom(this.addr2.address, this.addr3.address, tokenIdToTransfer); 178 | // sanity check 179 | expect(await this.erc721a.ownerOf(tokenIdToTransfer)).to.equal(this.addr3.address); 180 | 181 | expect(await this.erc721a.numberMinted(this.addr2.address)).to.equal(this.addr2.expected.mintCount); 182 | expect(await this.erc721a.numberMinted(this.addr3.address)).to.equal(this.addr3.expected.mintCount); 183 | }); 184 | }); 185 | 186 | context('_totalMinted', async function () { 187 | it('has correct totalMinted', async function () { 188 | const totalMinted = await this.erc721a.totalMinted(); 189 | expect(totalMinted).to.equal(this.expectedMintCount); 190 | }); 191 | }); 192 | 193 | context('_nextTokenId', async function () { 194 | it('has correct nextTokenId', async function () { 195 | const nextTokenId = await this.erc721a.nextTokenId(); 196 | expect(nextTokenId).to.equal(offsetted(this.expectedMintCount)); 197 | }); 198 | }); 199 | 200 | describe('aux', async function () { 201 | it('get and set works correctly', async function () { 202 | const uint64Max = BigNumber.from(2).pow(64).sub(1).toString(); 203 | expect(await this.erc721a.getAux(this.owner.address)).to.equal('0'); 204 | await this.erc721a.setAux(this.owner.address, uint64Max); 205 | expect(await this.erc721a.getAux(this.owner.address)).to.equal(uint64Max); 206 | 207 | expect(await this.erc721a.getAux(this.addr1.address)).to.equal('0'); 208 | await this.erc721a.setAux(this.addr1.address, '1'); 209 | expect(await this.erc721a.getAux(this.addr1.address)).to.equal('1'); 210 | 211 | await this.erc721a.setAux(this.addr3.address, '5'); 212 | expect(await this.erc721a.getAux(this.addr3.address)).to.equal('5'); 213 | 214 | expect(await this.erc721a.getAux(this.addr1.address)).to.equal('1'); 215 | }); 216 | }); 217 | 218 | describe('ownerOf', async function () { 219 | it('returns the right owner', async function () { 220 | for (const minter of [this.addr1, this.addr2, this.addr3]) { 221 | for (const tokenId of minter.expected.tokens) { 222 | expect(await this.erc721a.ownerOf(tokenId)).to.equal(minter.address); 223 | } 224 | } 225 | }); 226 | 227 | it('reverts for an invalid token', async function () { 228 | await expect(this.erc721a.ownerOf(10)).to.be.revertedWith('OwnerQueryForNonexistentToken'); 229 | 230 | if (this.startTokenId > 0) { 231 | await expect(this.erc721a.ownerOf(0)).to.be.revertedWith('OwnerQueryForNonexistentToken'); 232 | } 233 | }); 234 | }); 235 | 236 | describe('approve', async function () { 237 | beforeEach(function () { 238 | this.tokenId = this.addr1.expected.tokens[0]; 239 | this.tokenId2 = this.addr2.expected.tokens[0]; 240 | }); 241 | 242 | it('sets approval for the target address', async function () { 243 | await this.erc721a.connect(this.addr1).approve(this.addr2.address, this.tokenId); 244 | const approval = await this.erc721a.getApproved(this.tokenId); 245 | expect(approval).to.equal(this.addr2.address); 246 | }); 247 | 248 | it('set approval for the target address on behalf of the owner', async function () { 249 | await this.erc721a.connect(this.addr1).setApprovalForAll(this.addr2.address, true); 250 | await this.erc721a.connect(this.addr2).approve(this.addr3.address, this.tokenId); 251 | const approval = await this.erc721a.getApproved(this.tokenId); 252 | expect(approval).to.equal(this.addr3.address); 253 | }); 254 | 255 | it('rejects an unapproved caller', async function () { 256 | await expect(this.erc721a.approve(this.addr2.address, this.tokenId)).to.be.revertedWith( 257 | 'ApprovalCallerNotOwnerNorApproved' 258 | ); 259 | }); 260 | 261 | it('does not get approved for invalid tokens', async function () { 262 | await expect(this.erc721a.getApproved(10)).to.be.revertedWith('ApprovalQueryForNonexistentToken'); 263 | }); 264 | 265 | it('approval allows token transfer', async function () { 266 | await expect( 267 | this.erc721a.connect(this.addr3).transferFrom(this.addr1.address, this.addr3.address, this.tokenId) 268 | ).to.be.revertedWith('TransferCallerNotOwnerNorApproved'); 269 | await this.erc721a.connect(this.addr1).approve(this.addr3.address, this.tokenId); 270 | await this.erc721a.connect(this.addr3).transferFrom(this.addr1.address, this.addr3.address, this.tokenId); 271 | await expect( 272 | this.erc721a.connect(this.addr1).transferFrom(this.addr3.address, this.addr1.address, this.tokenId) 273 | ).to.be.revertedWith('TransferCallerNotOwnerNorApproved'); 274 | }); 275 | 276 | it('token owner can approve self as operator', async function () { 277 | expect(await this.erc721a.getApproved(this.tokenId)).to.not.equal(this.addr1.address); 278 | await expect(this.erc721a.connect(this.addr1).approve(this.addr1.address, this.tokenId) 279 | ).to.not.be.reverted; 280 | expect(await this.erc721a.getApproved(this.tokenId)).to.equal(this.addr1.address); 281 | }); 282 | 283 | it('self-approval is cleared on token transfer', async function () { 284 | await this.erc721a.connect(this.addr1).approve(this.addr1.address, this.tokenId); 285 | expect(await this.erc721a.getApproved(this.tokenId)).to.equal(this.addr1.address); 286 | 287 | await this.erc721a.connect(this.addr1).transferFrom(this.addr1.address, this.addr2.address, this.tokenId); 288 | expect(await this.erc721a.getApproved(this.tokenId)).to.not.equal(this.addr1.address); 289 | }); 290 | 291 | it('direct approve works', async function () { 292 | expect(await this.erc721a.getApproved(this.tokenId)).to.not.equal(this.addr1.address); 293 | await this.erc721a.connect(this.addr2).directApprove(this.addr1.address, this.tokenId); 294 | expect(await this.erc721a.getApproved(this.tokenId)).to.equal(this.addr1.address); 295 | }); 296 | }); 297 | 298 | describe('setApprovalForAll', async function () { 299 | it('sets approval for all properly', async function () { 300 | const approvalTx = await this.erc721a.setApprovalForAll(this.addr1.address, true); 301 | await expect(approvalTx) 302 | .to.emit(this.erc721a, 'ApprovalForAll') 303 | .withArgs(this.owner.address, this.addr1.address, true); 304 | expect(await this.erc721a.isApprovedForAll(this.owner.address, this.addr1.address)).to.be.true; 305 | }); 306 | 307 | it('caller can approve all with self as operator', async function () { 308 | expect( 309 | await this.erc721a.connect(this.addr1).isApprovedForAll(this.addr1.address, this.addr1.address) 310 | ).to.be.false; 311 | await expect( 312 | this.erc721a.connect(this.addr1).setApprovalForAll(this.addr1.address, true) 313 | ).to.not.be.reverted; 314 | expect( 315 | await this.erc721a.connect(this.addr1).isApprovedForAll(this.addr1.address, this.addr1.address) 316 | ).to.be.true; 317 | }); 318 | }); 319 | 320 | context('test transfer functionality', function () { 321 | const testSuccessfulTransfer = function (transferFn, transferToContract = true) { 322 | beforeEach(async function () { 323 | const sender = this.addr2; 324 | this.tokenId = this.addr2.expected.tokens[0]; 325 | this.from = sender.address; 326 | this.to = transferToContract ? this.receiver : this.addr4; 327 | await this.erc721a.connect(sender).approve(this.to.address, this.tokenId); 328 | 329 | const ownershipBefore = await this.erc721a.getOwnershipAt(this.tokenId); 330 | this.timestampBefore = parseInt(ownershipBefore.startTimestamp); 331 | this.timestampToMine = (await getBlockTimestamp()) + 12345; 332 | await mineBlockTimestamp(this.timestampToMine); 333 | this.timestampMined = await getBlockTimestamp(); 334 | 335 | // prettier-ignore 336 | this.transferTx = await this.erc721a 337 | .connect(sender)[transferFn](this.from, this.to.address, this.tokenId); 338 | 339 | const ownershipAfter = await this.erc721a.getOwnershipAt(this.tokenId); 340 | this.timestampAfter = parseInt(ownershipAfter.startTimestamp); 341 | }); 342 | 343 | it('transfers the ownership of the given token ID to the given address', async function () { 344 | expect(await this.erc721a.ownerOf(this.tokenId)).to.be.equal(this.to.address); 345 | }); 346 | 347 | it('emits a Transfer event', async function () { 348 | await expect(this.transferTx) 349 | .to.emit(this.erc721a, 'Transfer') 350 | .withArgs(this.from, this.to.address, this.tokenId); 351 | }); 352 | 353 | it('clears the approval for the token ID', async function () { 354 | expect(await this.erc721a.getApproved(this.tokenId)).to.be.equal(ZERO_ADDRESS); 355 | }); 356 | 357 | it('adjusts owners balances', async function () { 358 | expect(await this.erc721a.balanceOf(this.from)).to.be.equal(1); 359 | }); 360 | 361 | it('startTimestamp updated correctly', async function () { 362 | expect(this.timestampBefore).to.be.lt(this.timestampToMine); 363 | expect(this.timestampAfter).to.be.gte(this.timestampToMine); 364 | expect(this.timestampAfter).to.be.lt(this.timestampToMine + 10); 365 | expect(this.timestampToMine).to.be.eq(this.timestampMined); 366 | }); 367 | }; 368 | 369 | const testUnsuccessfulTransfer = function (transferFn) { 370 | beforeEach(function () { 371 | this.tokenId = this.addr2.expected.tokens[0]; 372 | this.sender = this.addr1; 373 | }); 374 | 375 | it('rejects unapproved transfer', async function () { 376 | await expect( 377 | this.erc721a.connect(this.sender)[transferFn](this.addr2.address, this.sender.address, this.tokenId) 378 | ).to.be.revertedWith('TransferCallerNotOwnerNorApproved'); 379 | }); 380 | 381 | it('rejects transfer from incorrect owner', async function () { 382 | await this.erc721a.connect(this.addr2).setApprovalForAll(this.sender.address, true); 383 | await expect( 384 | this.erc721a.connect(this.sender)[transferFn](this.addr3.address, this.sender.address, this.tokenId) 385 | ).to.be.revertedWith('TransferFromIncorrectOwner'); 386 | }); 387 | 388 | it('rejects transfer to zero address', async function () { 389 | await this.erc721a.connect(this.addr2).setApprovalForAll(this.sender.address, true); 390 | await expect( 391 | this.erc721a.connect(this.sender)[transferFn](this.addr2.address, ZERO_ADDRESS, this.tokenId) 392 | ).to.be.revertedWith('TransferToZeroAddress'); 393 | }); 394 | }; 395 | 396 | context('successful transfers', function () { 397 | context('transferFrom', function () { 398 | describe('to contract', function () { 399 | testSuccessfulTransfer('transferFrom'); 400 | }); 401 | 402 | describe('to EOA', function () { 403 | testSuccessfulTransfer('transferFrom', false); 404 | }); 405 | }); 406 | 407 | context('safeTransferFrom', function () { 408 | describe('to contract', function () { 409 | testSuccessfulTransfer('safeTransferFrom(address,address,uint256)'); 410 | 411 | it('validates ERC721Received', async function () { 412 | await expect(this.transferTx) 413 | .to.emit(this.receiver, 'Received') 414 | .withArgs(this.addr2.address, this.addr2.address, this.tokenId, '0x', GAS_MAGIC_VALUE); 415 | }); 416 | }); 417 | 418 | describe('to EOA', function () { 419 | testSuccessfulTransfer('safeTransferFrom(address,address,uint256)', false); 420 | }); 421 | }); 422 | }); 423 | 424 | context('unsuccessful transfers', function () { 425 | describe('transferFrom', function () { 426 | testUnsuccessfulTransfer('transferFrom'); 427 | }); 428 | 429 | describe('safeTransferFrom', function () { 430 | testUnsuccessfulTransfer('safeTransferFrom(address,address,uint256)'); 431 | 432 | it('reverts for non-receivers', async function () { 433 | const nonReceiver = this.erc721a; 434 | // prettier-ignore 435 | await expect( 436 | this.erc721a.connect(this.addr1)['safeTransferFrom(address,address,uint256)']( 437 | this.addr1.address, 438 | nonReceiver.address, 439 | offsetted(0) 440 | ) 441 | ).to.be.revertedWith('TransferToNonERC721ReceiverImplementer'); 442 | }); 443 | 444 | it('reverts when the receiver reverted', async function () { 445 | // prettier-ignore 446 | await expect( 447 | this.erc721a.connect(this.addr1)['safeTransferFrom(address,address,uint256,bytes)']( 448 | this.addr1.address, 449 | this.receiver.address, 450 | offsetted(0), 451 | '0x01' 452 | ) 453 | ).to.be.revertedWith('reverted in the receiver contract!'); 454 | }); 455 | 456 | it('reverts if the receiver returns the wrong value', async function () { 457 | // prettier-ignore 458 | await expect( 459 | this.erc721a.connect(this.addr1)['safeTransferFrom(address,address,uint256,bytes)']( 460 | this.addr1.address, 461 | this.receiver.address, 462 | offsetted(0), 463 | '0x02' 464 | ) 465 | ).to.be.revertedWith('TransferToNonERC721ReceiverImplementer'); 466 | }); 467 | }); 468 | }); 469 | }); 470 | 471 | describe('_burn', async function () { 472 | beforeEach(function () { 473 | this.tokenIdToBurn = offsetted(0); 474 | }); 475 | 476 | it('can burn if approvalCheck is false', async function () { 477 | expect(await this.erc721a.exists(this.tokenIdToBurn)).to.be.true; 478 | await this.erc721a.connect(this.addr2)['burn(uint256,bool)'](this.tokenIdToBurn, false); 479 | expect(await this.erc721a.exists(this.tokenIdToBurn)).to.be.false; 480 | }); 481 | 482 | it('revert if approvalCheck is true', async function () { 483 | await expect( 484 | this.erc721a.connect(this.addr2)['burn(uint256,bool)'](this.tokenIdToBurn, true) 485 | ).to.be.revertedWith('TransferCallerNotOwnerNorApproved'); 486 | }); 487 | 488 | it('can burn without approvalCheck parameter', async function () { 489 | expect(await this.erc721a.exists(this.tokenIdToBurn)).to.be.true; 490 | await this.erc721a.connect(this.addr2)['burn(uint256)'](this.tokenIdToBurn); 491 | expect(await this.erc721a.exists(this.tokenIdToBurn)).to.be.false; 492 | }); 493 | 494 | it('cannot burn a token owned by another if not approved', async function () { 495 | expect(await this.erc721a.exists(this.tokenIdToBurn)).to.be.true; 496 | await this.erc721a.connect(this.addr2)['burn(uint256)'](this.tokenIdToBurn); 497 | expect(await this.erc721a.exists(this.tokenIdToBurn)).to.be.false; 498 | }); 499 | }); 500 | 501 | describe('_initializeOwnershipAt', async function () { 502 | it('successfuly sets ownership of empty slot', async function () { 503 | const lastTokenId = this.addr3.expected.tokens[2]; 504 | const ownership1 = await this.erc721a.getOwnershipAt(lastTokenId); 505 | expect(ownership1[0]).to.equal(ZERO_ADDRESS); 506 | await this.erc721a.initializeOwnershipAt(lastTokenId); 507 | const ownership2 = await this.erc721a.getOwnershipAt(lastTokenId); 508 | expect(ownership2[0]).to.equal(this.addr3.address); 509 | }); 510 | 511 | it("doesn't set ownership if it's already setted", async function () { 512 | const lastTokenId = this.addr3.expected.tokens[2]; 513 | expect(await this.erc721a.ownerOf(lastTokenId)).to.be.equal(this.addr3.address); 514 | const tx1 = await this.erc721a.initializeOwnershipAt(lastTokenId); 515 | expect(await this.erc721a.ownerOf(lastTokenId)).to.be.equal(this.addr3.address); 516 | const tx2 = await this.erc721a.initializeOwnershipAt(lastTokenId); 517 | 518 | // We assume the 2nd initialization doesn't set again due to less gas used. 519 | const receipt1 = await tx1.wait(); 520 | const receipt2 = await tx2.wait(); 521 | expect(receipt2.gasUsed.toNumber()).to.be.lessThan(receipt1.gasUsed.toNumber()); 522 | }); 523 | }); 524 | }); 525 | 526 | context('test mint functionality', function () { 527 | beforeEach(async function () { 528 | const [owner, addr1] = await ethers.getSigners(); 529 | this.owner = owner; 530 | this.addr1 = addr1; 531 | }); 532 | 533 | const testSuccessfulMint = function (safe, quantity, mintForContract = true) { 534 | beforeEach(async function () { 535 | this.minter = mintForContract ? this.receiver : this.addr1; 536 | 537 | const mintFn = safe ? 'safeMint(address,uint256)' : 'mint(address,uint256)'; 538 | 539 | this.balanceBefore = (await this.erc721a.balanceOf(this.minter.address)).toNumber(); 540 | 541 | this.timestampToMine = (await getBlockTimestamp()) + 12345; 542 | await mineBlockTimestamp(this.timestampToMine); 543 | this.timestampMined = await getBlockTimestamp(); 544 | 545 | this.mintTx = await this.erc721a[mintFn](this.minter.address, quantity); 546 | }); 547 | 548 | it('changes ownership', async function () { 549 | for (let tokenId = offsetted(0); tokenId < offsetted(quantity); tokenId++) { 550 | expect(await this.erc721a.ownerOf(tokenId)).to.equal(this.minter.address); 551 | } 552 | }); 553 | 554 | it('emits a Transfer event', async function () { 555 | for (let tokenId = offsetted(0); tokenId < offsetted(quantity); tokenId++) { 556 | await expect(this.mintTx) 557 | .to.emit(this.erc721a, 'Transfer') 558 | .withArgs(ZERO_ADDRESS, this.minter.address, tokenId); 559 | } 560 | }); 561 | 562 | it('adjusts owners balances', async function () { 563 | expect(await this.erc721a.balanceOf(this.minter.address)).to.be.equal(this.balanceBefore + quantity); 564 | }); 565 | 566 | it('adjusts OwnershipAt and OwnershipOf', async function () { 567 | const ownership = await this.erc721a.getOwnershipAt(offsetted(0)); 568 | expect(ownership.startTimestamp).to.be.gte(this.timestampToMine); 569 | expect(ownership.startTimestamp).to.be.lt(this.timestampToMine + 10); 570 | expect(ownership.burned).to.be.false; 571 | 572 | for (let tokenId = offsetted(0); tokenId < offsetted(quantity); tokenId++) { 573 | const ownership = await this.erc721a.getOwnershipOf(tokenId); 574 | expect(ownership.addr).to.equal(this.minter.address); 575 | expect(ownership.startTimestamp).to.be.gte(this.timestampToMine); 576 | expect(ownership.startTimestamp).to.be.lt(this.timestampToMine + 10); 577 | expect(ownership.burned).to.be.false; 578 | } 579 | 580 | expect(this.timestampToMine).to.be.eq(this.timestampMined); 581 | }); 582 | 583 | if (safe && mintForContract) { 584 | it('validates ERC721Received', async function () { 585 | for (let tokenId = offsetted(0); tokenId < offsetted(quantity); tokenId++) { 586 | await expect(this.mintTx) 587 | .to.emit(this.minter, 'Received') 588 | .withArgs(this.owner.address, ZERO_ADDRESS, tokenId, '0x', GAS_MAGIC_VALUE); 589 | } 590 | }); 591 | } 592 | }; 593 | 594 | const testUnsuccessfulMint = function (safe) { 595 | beforeEach(async function () { 596 | this.mintFn = safe ? 'safeMint(address,uint256)' : 'mint(address,uint256)'; 597 | }); 598 | 599 | it('rejects mints to the zero address', async function () { 600 | await expect(this.erc721a[this.mintFn](ZERO_ADDRESS, 1)).to.be.revertedWith('MintToZeroAddress'); 601 | }); 602 | 603 | it('requires quantity to be greater than 0', async function () { 604 | await expect(this.erc721a[this.mintFn](this.owner.address, 0)).to.be.revertedWith('MintZeroQuantity'); 605 | }); 606 | }; 607 | 608 | context('successful mints', function () { 609 | context('mint', function () { 610 | context('for contract', function () { 611 | describe('single token', function () { 612 | testSuccessfulMint(false, 1); 613 | }); 614 | 615 | describe('multiple tokens', function () { 616 | testSuccessfulMint(false, 5); 617 | }); 618 | 619 | it('does not revert for non-receivers', async function () { 620 | const nonReceiver = this.erc721a; 621 | await this.erc721a.mint(nonReceiver.address, 1); 622 | expect(await this.erc721a.ownerOf(offsetted(0))).to.equal(nonReceiver.address); 623 | }); 624 | }); 625 | 626 | context('for EOA', function () { 627 | describe('single token', function () { 628 | testSuccessfulMint(false, 1, false); 629 | }); 630 | 631 | describe('multiple tokens', function () { 632 | testSuccessfulMint(false, 5, false); 633 | }); 634 | }); 635 | }); 636 | 637 | context('safeMint', function () { 638 | context('for contract', function () { 639 | describe('single token', function () { 640 | testSuccessfulMint(true, 1); 641 | }); 642 | 643 | describe('multiple tokens', function () { 644 | testSuccessfulMint(true, 5); 645 | }); 646 | 647 | it('validates ERC721Received with custom _data', async function () { 648 | const customData = ethers.utils.formatBytes32String('custom data'); 649 | const tx = await this.erc721a['safeMint(address,uint256,bytes)'](this.receiver.address, 1, customData); 650 | await expect(tx) 651 | .to.emit(this.receiver, 'Received') 652 | .withArgs(this.owner.address, ZERO_ADDRESS, offsetted(0), customData, GAS_MAGIC_VALUE); 653 | }); 654 | }); 655 | 656 | context('for EOA', function () { 657 | describe('single token', function () { 658 | testSuccessfulMint(true, 1, false); 659 | }); 660 | 661 | describe('multiple tokens', function () { 662 | testSuccessfulMint(true, 5, false); 663 | }); 664 | }); 665 | }); 666 | }); 667 | 668 | context('unsuccessful mints', function () { 669 | context('mint', function () { 670 | testUnsuccessfulMint(false); 671 | }); 672 | 673 | context('safeMint', function () { 674 | testUnsuccessfulMint(true); 675 | 676 | it('reverts for non-receivers', async function () { 677 | const nonReceiver = this.erc721a; 678 | await expect(this.erc721a['safeMint(address,uint256)'](nonReceiver.address, 1)).to.be.revertedWith( 679 | 'TransferToNonERC721ReceiverImplementer' 680 | ); 681 | }); 682 | 683 | it('reverts when the receiver reverted', async function () { 684 | await expect( 685 | this.erc721a['safeMint(address,uint256,bytes)'](this.receiver.address, 1, '0x01') 686 | ).to.be.revertedWith('reverted in the receiver contract!'); 687 | }); 688 | 689 | it('reverts if the receiver returns the wrong value', async function () { 690 | await expect( 691 | this.erc721a['safeMint(address,uint256,bytes)'](this.receiver.address, 1, '0x02') 692 | ).to.be.revertedWith('TransferToNonERC721ReceiverImplementer'); 693 | }); 694 | 695 | it('reverts with reentrant call', async function () { 696 | await expect( 697 | this.erc721a['safeMint(address,uint256,bytes)'](this.receiver.address, 1, '0x03') 698 | ).to.be.reverted; 699 | }); 700 | }); 701 | }); 702 | }); 703 | 704 | context('with direct set burn bit', async function () { 705 | it('ownerOf reverts for an uninitialized burnt token', async function () { 706 | const [owner] = await ethers.getSigners(); 707 | await this.erc721a['safeMint(address,uint256)'](owner.address, 3); 708 | await this.erc721a['safeMint(address,uint256)'](owner.address, 2); 709 | await this.erc721a['safeMint(address,uint256)'](owner.address, 1); 710 | for (let i = 0; i < 3 + 2 + 1; ++i) { 711 | expect(await this.erc721a.ownerOf(this.startTokenId + i)).to.eq(owner.address); 712 | } 713 | await this.erc721a.directSetBurnBit(this.startTokenId + 3); 714 | for (let i = 0; i < 3 + 2 + 1; ++i) { 715 | if (3 <= i && i < 3 + 2) { 716 | await expect(this.erc721a.ownerOf(this.startTokenId + i)) 717 | .to.be.revertedWith('OwnerQueryForNonexistentToken'); 718 | await expect(this.erc721a.getApproved(this.startTokenId + i)) 719 | .to.be.revertedWith('ApprovalQueryForNonexistentToken'); 720 | await expect(this.erc721a.tokenURI(this.startTokenId + i)) 721 | .to.be.revertedWith('URIQueryForNonexistentToken'); 722 | } else { 723 | expect(await this.erc721a.ownerOf(this.startTokenId + i)).to.eq(owner.address); 724 | } 725 | } 726 | }); 727 | }); 728 | 729 | context('_toString', async function () { 730 | it('returns correct value', async function () { 731 | expect(await this.erc721a['toString(uint256)']('0')).to.eq('0'); 732 | expect(await this.erc721a['toString(uint256)']('1')).to.eq('1'); 733 | expect(await this.erc721a['toString(uint256)']('2')).to.eq('2'); 734 | const uint256Max = BigNumber.from(2).pow(256).sub(1).toString(); 735 | expect(await this.erc721a['toString(uint256)'](uint256Max)).to.eq(uint256Max); 736 | }); 737 | }); 738 | }); 739 | }; 740 | 741 | describe('ERC721A', createTestSuite({ contract: 'ERC721AMock', constructorArgs: ['Azuki', 'AZUKI'] })); 742 | 743 | describe( 744 | 'ERC721A override _startTokenId()', 745 | createTestSuite({ contract: 'ERC721AStartTokenIdMock', constructorArgs: ['Azuki', 'AZUKI', 1] }) 746 | ); 747 | 748 | describe('ERC721A with ERC2309', async function () { 749 | beforeEach(async function () { 750 | const [owner, addr1] = await ethers.getSigners(); 751 | this.owner = owner; 752 | this.addr1 = addr1; 753 | 754 | let args; 755 | args = ['Azuki', 'AZUKI', this.owner.address, 1, true]; 756 | this.erc721aMint1 = await deployContract('ERC721AWithERC2309Mock', args); 757 | args = ['Azuki', 'AZUKI', this.owner.address, 10, true]; 758 | this.erc721aMint10 = await deployContract('ERC721AWithERC2309Mock', args); 759 | }); 760 | 761 | it('emits a ConsecutiveTransfer event for single mint', async function () { 762 | expect(this.erc721aMint1.deployTransaction) 763 | .to.emit(this.erc721aMint1, 'ConsecutiveTransfer') 764 | .withArgs(0, 0, ZERO_ADDRESS, this.owner.address); 765 | }); 766 | 767 | it('emits a ConsecutiveTransfer event for a batch mint', async function () { 768 | expect(this.erc721aMint10.deployTransaction) 769 | .to.emit(this.erc721aMint10, 'ConsecutiveTransfer') 770 | .withArgs(0, 9, ZERO_ADDRESS, this.owner.address); 771 | }); 772 | 773 | it('requires quantity to be below mint limit', async function () { 774 | let args; 775 | const mintLimit = 5000; 776 | args = ['Azuki', 'AZUKI', this.owner.address, mintLimit, true]; 777 | await deployContract('ERC721AWithERC2309Mock', args); 778 | args = ['Azuki', 'AZUKI', this.owner.address, mintLimit + 1, true]; 779 | await expect(deployContract('ERC721AWithERC2309Mock', args)).to.be.revertedWith('MintERC2309QuantityExceedsLimit'); 780 | }) 781 | 782 | it('rejects mints to the zero address', async function () { 783 | let args = ['Azuki', 'AZUKI', ZERO_ADDRESS, 1, true]; 784 | await expect(deployContract('ERC721AWithERC2309Mock', args)).to.be.revertedWith('MintToZeroAddress'); 785 | }); 786 | 787 | it('requires quantity to be greater than 0', async function () { 788 | let args = ['Azuki', 'AZUKI', this.owner.address, 0, true]; 789 | await expect(deployContract('ERC721AWithERC2309Mock', args)).to.be.revertedWith('MintZeroQuantity'); 790 | }); 791 | }); 792 | -------------------------------------------------------------------------------- /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('transferFrom', function () { 44 | beforeEach(async function () { 45 | await this.erc721a.mintTen(this.owner.address); 46 | await this.erc721a.mintOne(this.owner.address); 47 | 48 | await this.erc721a.mintTen(this.addr1.address); 49 | await this.erc721a.mintOne(this.addr1.address); 50 | }); 51 | 52 | it('transfer to and from two addresses', async function () { 53 | for (let i = 0; i < 2; ++i) { 54 | await this.erc721a.connect(this.owner).transferFrom(this.owner.address, this.addr1.address, 1); 55 | await this.erc721a.connect(this.addr1).transferFrom(this.addr1.address, this.owner.address, 1); 56 | } 57 | }); 58 | 59 | it('transferTen ascending order', async function () { 60 | await this.erc721a.connect(this.owner).transferTenAsc(this.addr1.address); 61 | }); 62 | 63 | it('transferTen descending order', async function () { 64 | await this.erc721a.connect(this.owner).transferTenDesc(this.addr1.address); 65 | }); 66 | 67 | it('transferTen average order', async function () { 68 | await this.erc721a.connect(this.owner).transferTenAvg(this.addr1.address); 69 | }); 70 | }); 71 | 72 | it('mintOneERC2309', async function () { 73 | // The following call `_mintERC3201` outside of contract creation. 74 | // This is non-compliant with the ERC721 standard, 75 | // and is only meant for gas comparisons. 76 | let args = ['Azuki', 'AZUKI', this.owner.address, 0, false]; 77 | let contract = await deployContract('ERC721AWithERC2309Mock', args); 78 | await contract.mintOneERC2309(this.owner.address); 79 | await contract.mintOneERC2309(this.owner.address); 80 | await contract.mintOneERC2309(this.addr1.address); 81 | }); 82 | 83 | it('mintTenERC2309', async function () { 84 | // The following call `_mintERC3201` outside of contract creation. 85 | // This is non-compliant with the ERC721 standard, 86 | // and is only meant for gas comparisons. 87 | let args = ['Azuki', 'AZUKI', this.owner.address, 0, false]; 88 | let contract = await deployContract('ERC721AWithERC2309Mock', args); 89 | await contract.mintTenERC2309(this.owner.address); 90 | await contract.mintTenERC2309(this.owner.address); 91 | await contract.mintTenERC2309(this.addr1.address); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------