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