├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── benchmarks.md ├── contracts ├── ERC721AMock.sol ├── Optimint.sol ├── OptimintEnumerable.sol ├── OptimintSupply.sol ├── token │ └── ERC721 │ │ ├── ERC721.sol │ │ ├── extensions │ │ ├── ERC721Enumerable.sol │ │ ├── ERC721Metadata.sol │ │ └── ERC721Supply.sol │ │ └── utils │ │ ├── ERC721Inventory.sol │ │ ├── ERC721Receiver.sol │ │ └── ERC721TokenId.sol └── utils │ └── Bitmap.sol ├── hardhat.config.js ├── package-lock.json ├── package.json └── scripts ├── benchmarks.js └── libraries └── deployable.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | cache 3 | artifacts -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 90, 3 | "tabWidth": 4, 4 | "bracketSpacing": true 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 nftyte 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Optimint Hardhat Implementation 2 | 3 | Optimint is a gas-optimized ERC721 reference implementation that's based on OpenZeppelin Contracts, and requires only 1 SSTORE operation per batch-mint. Please see the [benchmarks](./benchmarks.md) file to compare gas-usage. 4 | 5 | **Note:** We use OpenZeppelin Contracts as a base, but these optimizations can be utilized in other implemenations. 6 | 7 | ## Optimized Gas 8 | 9 | `Optimint` gas optimization is achieved by storing owner addresses inside token identifiers, instead of writing them to storage: 10 | 11 | ```Solidity 12 | uint tokenId = (uint(owner) << x) | N; 13 | ownerOf(tokenId) = address(tokenId >> x); 14 | ``` 15 | 16 | We commit a token owner's address to storage only after the token's been transferred for the first time: 17 | 18 | ```Solidity 19 | ownerOf(tokenId) = _owners[tokenId]; 20 | ``` 21 | 22 | ### Limitations 23 | 24 | - Total supply should be limited based on the `ERC721Inventory.BALANCE_BITSIZE` setting. For example, if it's set to `16`, then supply should be limited to `type(uint16).max`. 25 | The `ERC721Supply` extension is provided to enforce this limitation, and its `_maxSupply` method can be overridden to enforce a lower one. 26 | - The maximum number of tokens a single address can mint is limited based on `ERC721Inventory.SLOTS_PER_INVENTORY`. It's set to `232` by default, but may change when updating other settings. You can enforce a lower limit by overriding `ERC721._maxMintBalance`. 27 | - Enumerability isn't trivial. 28 | 29 | ### Enumerability 30 | 31 | This repo includes an `ERC721Enumerable` implementation that can be used to provide on-chain enumerability. While most interactions remain unaffected, gas usage on mint is a little higher than ERC721A's. See [benchmarks](./benchmarks.md) for more details. 32 | 33 | ## Benchmarks 34 | 35 | You can run the benchmarks locally: 36 | 37 | ```console 38 | npx hardhat run scripts/benchmarks.js 39 | ``` 40 | 41 | **Note:** Benchmarks may take a minute to complete. 42 | 43 | ## Contributations 44 | 45 | All contribuations are welcome! Feel free to open a PR or an issue. 46 | 47 | ## License 48 | 49 | MIT license. Anyone can use or modify this software for their purposes. 50 | -------------------------------------------------------------------------------- /benchmarks.md: -------------------------------------------------------------------------------- 1 | ## Deployment 2 | 3 | | Contract | Deploy | 4 | | --- | --- | 5 | | Optimint | 1,111,595 | 6 | | OptimintSupply | 1,165,511 | 7 | | OptimintEnumerable | 1,378,764 | 8 | | ERC721A | 885,778 | 9 | 10 | ## Mint 11 | 12 | | Contract | Mint 1 | Mint 2 | Mint 3 | Mint 4 | Mint 5 | Mint 10 | Mint 50 | Mint 100 | 13 | | --- | --- | --- | --- | --- | --- | --- | --- | --- | 14 | | Optimint | 46,168 | 48,104 | 50,040 | 51,976 | 53,912 | 63,592 | 141,032 | 237,832 | 15 | | OptimintSupply | 51,411 | 53,347 | 55,283 | 57,219 | 59,155 | 68,835 | 146,275 | 243,075 | 16 | | OptimintEnumerable | 73,794 | 75,730 | 77,666 | 79,602 | 81,538 | 91,218 | 168,658 | 265,458 | 17 | | ERC721A | 72,960 | 74,896 | 76,832 | 78,768 | 80,704 | 90,384 | 167,824 | 264,624 | 18 | 19 | ## Transfer 20 | 21 | ### Owner to non-owner 22 | 23 | | Contract | Token #1 | 24 | | --- | --- | 25 | | Optimint | 60,362 | 26 | | OptimintSupply | 60,362 | 27 | | OptimintEnumerable | 60,384 | 28 | | ERC721A | 61,361 | 29 | 30 | ### Owner to owner 31 | 32 | | Contract | Token #1 | 33 | | --- | --- | 34 | | Optimint | 43,262 | 35 | | OptimintSupply | 43,262 | 36 | | OptimintEnumerable | 43,284 | 37 | | ERC721A | 44,261 | 38 | 39 | ### Minter to non-owner 40 | 41 | | Contract | Token #1 | Token #2 | Token #3 | Token #4 | Token #5 | Token #10 | Token #50 | Token #100 | 42 | | --- | --- | --- | --- | --- | --- | --- | --- | --- | 43 | | Optimint | 78,031 | 78,043 | 78,043 | 78,043 | 78,043 | 78,043 | 78,043 | 78,043 | 44 | | OptimintSupply | 78,031 | 78,043 | 78,043 | 78,043 | 78,043 | 78,043 | 78,043 | 78,043 | 45 | | OptimintEnumerable | 78,053 | 78,065 | 78,065 | 78,065 | 78,065 | 78,065 | 78,065 | 78,065 | 46 | | ERC721A | 83,814 | 103,138 | 105,350 | 107,562 | 109,774 | 120,834 | 209,314 | 319,914 | 47 | 48 | ### Minter to owner 49 | 50 | | Contract | Token #1 | Token #2 | Token #3 | Token #4 | Token #5 | Token #10 | Token #50 | Token #100 | 51 | | --- | --- | --- | --- | --- | --- | --- | --- | --- | 52 | | Optimint | 60,931 | 60,943 | 60,943 | 60,943 | 60,943 | 60,943 | 60,943 | 60,943 | 53 | | OptimintSupply | 60,931 | 60,943 | 60,943 | 60,943 | 60,943 | 60,943 | 60,943 | 60,943 | 54 | | OptimintEnumerable | 60,953 | 60,965 | 60,965 | 60,965 | 60,965 | 60,965 | 60,965 | 60,965 | 55 | | ERC721A | 66,714 | 86,038 | 88,250 | 90,462 | 92,674 | 103,734 | 192,214 | 302,814 | 56 | 57 | -------------------------------------------------------------------------------- /contracts/ERC721AMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import { ERC721A } from "erc721a/contracts/ERC721A.sol"; 6 | 7 | /** 8 | * @dev Mock ERC721A implementation used in benchmarks. 9 | * @author https://github.com/nftyte 10 | */ 11 | contract ERC721AMock is ERC721A("ERC721A", "ERC721A") { 12 | function mint(uint256 amount) public payable virtual { 13 | _mint(msg.sender, amount); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /contracts/Optimint.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import { ERC721Metadata } from "./token/ERC721/extensions/ERC721Metadata.sol"; 6 | 7 | /** 8 | * @dev Gas optimized ERC721 implementation, including the Metadata extension. 9 | * @author https://github.com/nftyte 10 | */ 11 | contract Optimint is ERC721Metadata("Optimint", "OPTI") { 12 | function mint(uint256 amount) public payable virtual { 13 | _mint(msg.sender, amount); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /contracts/OptimintEnumerable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import { Optimint, ERC721Metadata } from "./Optimint.sol"; 6 | import { ERC721Enumerable, ERC721 } from "./token/ERC721/extensions/ERC721Enumerable.sol"; 7 | 8 | /** 9 | * @dev Gas optimized ERC721 implementation, including optional extensions. 10 | * Note: Tracks and limits mint supply, see {ERC721Supply-_maxSupply}. 11 | * @author https://github.com/nftyte 12 | */ 13 | contract OptimintEnumerable is Optimint, ERC721Enumerable { 14 | /** 15 | * @dev See {ERC721-_mintTokens}. 16 | */ 17 | function _mintTokens(address to, uint256 amount) 18 | internal 19 | virtual 20 | override(ERC721, ERC721Enumerable) 21 | returns (uint256, uint256) 22 | { 23 | return super._mintTokens(to, amount); 24 | } 25 | 26 | /** 27 | * @dev See {ERC721-_burn}. 28 | */ 29 | function _burn(uint256 tokenId) internal virtual override(ERC721, ERC721Enumerable) { 30 | return super._burn(tokenId); 31 | } 32 | 33 | /** 34 | * @dev See {IERC165-supportsInterface}. 35 | */ 36 | function supportsInterface(bytes4 interfaceId) 37 | public 38 | view 39 | virtual 40 | override(ERC721Metadata, ERC721Enumerable) 41 | returns (bool) 42 | { 43 | return super.supportsInterface(interfaceId); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /contracts/OptimintSupply.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import { Optimint, ERC721Metadata } from "./Optimint.sol"; 6 | import { ERC721Supply, ERC721 } from "./token/ERC721/extensions/ERC721Supply.sol"; 7 | 8 | /** 9 | * @dev Gas optimized ERC721 implementation, including the Metadata extension. 10 | * Note: Tracks and limits mint supply, see {ERC721Supply-_maxSupply}. 11 | * @author https://github.com/nftyte 12 | */ 13 | contract OptimintSupply is Optimint, ERC721Supply { 14 | /** 15 | * @dev See {ERC721-_mintTokens}. 16 | */ 17 | function _mintTokens(address to, uint256 amount) 18 | internal 19 | virtual 20 | override(ERC721, ERC721Supply) 21 | returns (uint256, uint256) 22 | { 23 | return super._mintTokens(to, amount); 24 | } 25 | 26 | /** 27 | * @dev See {IERC165-supportsInterface}. 28 | */ 29 | function supportsInterface(bytes4 interfaceId) 30 | public 31 | view 32 | virtual 33 | override(ERC721, ERC721Metadata) 34 | returns (bool) 35 | { 36 | return super.supportsInterface(interfaceId); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /contracts/token/ERC721/ERC721.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; 6 | import { IERC721Receiver } from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; 7 | import { Address } from "@openzeppelin/contracts/utils/Address.sol"; 8 | import { Context } from "@openzeppelin/contracts/utils/Context.sol"; 9 | import { ERC165, IERC165 } from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; 10 | import { ERC721Inventory } from "./utils/ERC721Inventory.sol"; 11 | import { ERC721TokenId } from "./utils/ERC721TokenId.sol"; 12 | import { ERC721Receiver } from "./utils/ERC721Receiver.sol"; 13 | 14 | /** 15 | * @dev Implementation of https://eips.ethereum.org/EIPS/eip-721[ERC721] Non-Fungible Token Standard, 16 | * excluding optional extensions. 17 | * Note: Modified from OpenZeppelin Contracts, see: 18 | * https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/ERC721.sol 19 | * @author https://github.com/nftyte 20 | */ 21 | contract ERC721 is Context, ERC165, IERC721 { 22 | using ERC721Inventory for uint256; 23 | using ERC721TokenId for uint256; 24 | using ERC721TokenId for address; 25 | using ERC721Receiver for address; 26 | using Address for address; 27 | 28 | // Mapping from token ID to owner address 29 | mapping(uint256 => address) private _owners; 30 | 31 | // Mapping owner address to token count 32 | mapping(address => uint256) private _inventories; 33 | 34 | // Mapping from token ID to approved address 35 | mapping(uint256 => address) private _tokenApprovals; 36 | 37 | // Mapping from owner to operator approvals 38 | mapping(address => mapping(address => bool)) private _operatorApprovals; 39 | 40 | /** 41 | * @dev See {IERC165-supportsInterface}. 42 | */ 43 | function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) { 44 | return 45 | interfaceId == type(IERC721).interfaceId || 46 | super.supportsInterface(interfaceId); 47 | } 48 | 49 | /** 50 | * @dev See {IERC721-balanceOf}. 51 | */ 52 | function balanceOf(address owner) public view virtual returns (uint256) { 53 | require(owner != address(0), "ERC721: address zero is not a valid owner"); 54 | return _inventories[owner].balance(); 55 | } 56 | 57 | /** 58 | * @dev See {IERC721-ownerOf}. 59 | */ 60 | function ownerOf(uint256 tokenId) public view virtual returns (address owner) { 61 | if ((owner = _owners[tokenId]) == address(0)) { 62 | address minter = tokenId.minter(); 63 | if (_inventories[minter].has(tokenId.index())) { 64 | owner = minter; 65 | } 66 | } 67 | 68 | require(owner != address(0), "ERC721: invalid token ID"); 69 | } 70 | 71 | /** 72 | * @dev See {IERC721-approve}. 73 | */ 74 | function approve(address to, uint256 tokenId) public virtual { 75 | address owner = ownerOf(tokenId); 76 | require(to != owner, "ERC721: approval to current owner"); 77 | 78 | require( 79 | _msgSender() == owner || isApprovedForAll(owner, _msgSender()), 80 | "ERC721: approve caller is not token owner nor approved for all" 81 | ); 82 | 83 | _approve(to, tokenId); 84 | } 85 | 86 | /** 87 | * @dev See {IERC721-getApproved}. 88 | */ 89 | function getApproved(uint256 tokenId) public view virtual returns (address) { 90 | _requireMinted(tokenId); 91 | 92 | return _tokenApprovals[tokenId]; 93 | } 94 | 95 | /** 96 | * @dev See {IERC721-setApprovalForAll}. 97 | */ 98 | function setApprovalForAll(address operator, bool approved) public virtual { 99 | _setApprovalForAll(_msgSender(), operator, approved); 100 | } 101 | 102 | /** 103 | * @dev See {IERC721-isApprovedForAll}. 104 | */ 105 | function isApprovedForAll(address owner, address operator) public view virtual returns (bool) { 106 | return _operatorApprovals[owner][operator]; 107 | } 108 | 109 | /** 110 | * @dev See {IERC721-transferFrom}. 111 | */ 112 | function transferFrom( 113 | address from, 114 | address to, 115 | uint256 tokenId 116 | ) public virtual override { 117 | //solhint-disable-next-line max-line-length 118 | require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: caller is not token owner nor approved"); 119 | 120 | _transfer(from, to, tokenId); 121 | } 122 | 123 | /** 124 | * @dev See {IERC721-safeTransferFrom}. 125 | */ 126 | function safeTransferFrom( 127 | address from, 128 | address to, 129 | uint256 tokenId 130 | ) public virtual override { 131 | safeTransferFrom(from, to, tokenId, ""); 132 | } 133 | 134 | /** 135 | * @dev See {IERC721-safeTransferFrom}. 136 | */ 137 | function safeTransferFrom( 138 | address from, 139 | address to, 140 | uint256 tokenId, 141 | bytes memory data 142 | ) public virtual override { 143 | require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: caller is not token owner nor approved"); 144 | _safeTransfer(from, to, tokenId, data); 145 | } 146 | 147 | /** 148 | * @dev Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients 149 | * are aware of the ERC721 protocol to prevent tokens from being forever locked. 150 | * 151 | * `data` is additional data, it has no specified format and it is sent in call to `to`. 152 | * 153 | * This internal function is equivalent to {safeTransferFrom}, and can be used to e.g. 154 | * implement alternative mechanisms to perform token transfer, such as signature-based. 155 | * 156 | * Requirements: 157 | * 158 | * - `from` cannot be the zero address. 159 | * - `to` cannot be the zero address. 160 | * - `tokenId` token must exist and be owned by `from`. 161 | * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. 162 | * 163 | * Emits a {Transfer} event. 164 | */ 165 | function _safeTransfer( 166 | address from, 167 | address to, 168 | uint256 tokenId, 169 | bytes memory data 170 | ) internal virtual { 171 | _transfer(from, to, tokenId); 172 | 173 | if (to.isContract()) { 174 | require( 175 | to.checkOnERC721Received(msg.sender, from, tokenId, data), 176 | "ERC721: transfer to non ERC721Receiver implementer" 177 | ); 178 | } 179 | } 180 | 181 | /** 182 | * @dev Returns whether `tokenId` exists. 183 | * 184 | * Tokens can be managed by their owner or approved accounts via {approve} or {setApprovalForAll}. 185 | * 186 | * Tokens start existing when they are minted (`_mint`), 187 | * and stop existing when they are burned (`_burn`). 188 | */ 189 | function _exists(uint256 tokenId) internal view virtual returns (bool) { 190 | if (_owners[tokenId] == address(0)) { 191 | return _inventories[tokenId.minter()].has(tokenId.index()); 192 | } 193 | 194 | return true; 195 | } 196 | 197 | /** 198 | * @dev Returns whether `spender` is allowed to manage `tokenId`. 199 | * 200 | * Requirements: 201 | * 202 | * - `tokenId` must exist. 203 | */ 204 | function _isApprovedOrOwner(address spender, uint256 tokenId) internal view virtual returns (bool) { 205 | address owner = ownerOf(tokenId); 206 | return (spender == owner || isApprovedForAll(owner, spender) || getApproved(tokenId) == spender); 207 | } 208 | 209 | /** 210 | * @dev Safely mints `amount` tokens and transfers them to `to`. 211 | * 212 | * Requirements: 213 | * 214 | * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. 215 | * - `to` cannot mint more than {_maxMintBalance} tokens. 216 | * 217 | * Emits a {Transfer} event. 218 | */ 219 | function _safeMint(address to, uint256 amount) internal virtual { 220 | _safeMint(to, amount, ""); 221 | } 222 | 223 | /** 224 | * @dev Same as {xref-ERC721-_safeMint-address-uint256-}[`_safeMint`], with an additional `data` parameter which is 225 | * forwarded in {IERC721Receiver-onERC721Received} to contract recipients. 226 | */ 227 | function _safeMint(address to, uint256 amount, bytes memory data) internal virtual { 228 | if (to.isContract()) { 229 | (uint256 tokenId, uint256 maxTokenId) = _mintTokens(to, amount); 230 | 231 | unchecked { 232 | while (tokenId < maxTokenId) { 233 | emit Transfer(address(0), to, tokenId); 234 | require( 235 | to.checkOnERC721Received(msg.sender, address(0), tokenId++, data), 236 | "ERC721: transfer to non ERC721Receiver implementer" 237 | ); 238 | } 239 | } 240 | } else { 241 | _mint(to, amount); 242 | } 243 | } 244 | 245 | /** 246 | * @dev Mints `amount` tokens and transfers them to `to`. 247 | * 248 | * WARNING: Usage of this method is discouraged, use {_safeMint} whenever possible 249 | * 250 | * Requirements: 251 | * 252 | * - `to` cannot be the zero address. 253 | * - `to` cannot mint more than {_maxMintBalance} tokens. 254 | * 255 | * Emits a {Transfer} event. 256 | */ 257 | function _mint(address to, uint256 amount) internal virtual { 258 | (uint256 tokenId, uint256 maxTokenId) = _mintTokens(to, amount); 259 | 260 | unchecked { 261 | while (tokenId < maxTokenId) { 262 | emit Transfer(address(0), to, tokenId++); 263 | } 264 | } 265 | } 266 | 267 | /** 268 | * @dev Mints `amount` tokens into `to`'s inventory. 269 | * 270 | * Requirements: 271 | * 272 | * - `to` cannot be the zero address. 273 | * - `to` cannot mint more than {_maxMintBalance} tokens. 274 | */ 275 | function _mintTokens(address to, uint256 amount) internal virtual returns (uint256 tokenId, uint256 maxTokenId) { 276 | require(to != address(0), "ERC721: mint to the zero address"); 277 | uint256 minted = _mintBalanceOf(to); 278 | 279 | unchecked { 280 | require(minted + amount < _maxMintBalance() + 1, "ERC721: mint balance exceeded"); 281 | tokenId = to.toTokenId() | minted; 282 | maxTokenId = tokenId + amount; 283 | } 284 | 285 | _inventories[to] = _inventories[to].add(amount); 286 | } 287 | 288 | /** 289 | * @dev Returns the number of tokens minted by `owner`'s account. 290 | */ 291 | function _mintBalanceOf(address minter) internal view virtual returns (uint256) { 292 | return _inventories[minter].current(); 293 | } 294 | 295 | /** 296 | * @dev Returns the maximum number of tokens that can be minted by an account. 297 | */ 298 | function _maxMintBalance() internal view virtual returns (uint256) { 299 | return ERC721Inventory.SLOTS_PER_INVENTORY; 300 | } 301 | 302 | /** 303 | * @dev Destroys `tokenId`. 304 | * The approval is cleared when the token is burned. 305 | * 306 | * Requirements: 307 | * 308 | * - `tokenId` must exist. 309 | * 310 | * Emits a {Transfer} event. 311 | */ 312 | function _burn(uint256 tokenId) internal virtual { 313 | address owner = ownerOf(tokenId); 314 | 315 | // Clear approvals 316 | if (_tokenApprovals[tokenId] != address(0)) { 317 | _approve(address(0), tokenId); 318 | } 319 | 320 | if (_owners[tokenId] == owner) { 321 | delete _owners[tokenId]; 322 | unchecked { 323 | _inventories[owner]--; 324 | } 325 | } else { 326 | _inventories[owner] = _inventories[owner].remove(tokenId.index()); 327 | } 328 | 329 | emit Transfer(owner, address(0), tokenId); 330 | } 331 | 332 | /** 333 | * @dev Transfers `tokenId` from `from` to `to`. 334 | * As opposed to {transferFrom}, this imposes no restrictions on msg.sender. 335 | * 336 | * Requirements: 337 | * 338 | * - `to` cannot be the zero address. 339 | * - `tokenId` token must be owned by `from`. 340 | * 341 | * Emits a {Transfer} event. 342 | */ 343 | function _transfer( 344 | address from, 345 | address to, 346 | uint256 tokenId 347 | ) internal virtual { 348 | require(ownerOf(tokenId) == from, "ERC721: transfer from incorrect owner"); 349 | require(to != address(0), "ERC721: transfer to the zero address"); 350 | 351 | // Clear approvals from the previous owner 352 | if (_tokenApprovals[tokenId] != address(0)) { 353 | _approve(address(0), tokenId); 354 | } 355 | 356 | unchecked { 357 | if (_owners[tokenId] == from) { 358 | _inventories[from]--; 359 | } else { 360 | _inventories[from] = _inventories[from].remove(tokenId.index()); 361 | } 362 | 363 | _owners[tokenId] = to; 364 | _inventories[to]++; 365 | } 366 | 367 | emit Transfer(from, to, tokenId); 368 | } 369 | 370 | /** 371 | * @dev Approve `to` to operate on `tokenId` 372 | * 373 | * Emits an {Approval} event. 374 | */ 375 | function _approve(address to, uint256 tokenId) internal virtual { 376 | _tokenApprovals[tokenId] = to; 377 | emit Approval(ownerOf(tokenId), to, tokenId); 378 | } 379 | 380 | /** 381 | * @dev Approve `operator` to operate on all of `owner` tokens 382 | * 383 | * Emits an {ApprovalForAll} event. 384 | */ 385 | function _setApprovalForAll( 386 | address owner, 387 | address operator, 388 | bool approved 389 | ) internal virtual { 390 | require(owner != operator, "ERC721: approve to caller"); 391 | _operatorApprovals[owner][operator] = approved; 392 | emit ApprovalForAll(owner, operator, approved); 393 | } 394 | 395 | /** 396 | * @dev Reverts if the `tokenId` has not been minted yet. 397 | */ 398 | function _requireMinted(uint256 tokenId) internal view virtual { 399 | require(_exists(tokenId), "ERC721: invalid token ID"); 400 | } 401 | } 402 | -------------------------------------------------------------------------------- /contracts/token/ERC721/extensions/ERC721Enumerable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import { ERC721Supply, ERC721 } from "./ERC721Supply.sol"; 6 | import { IERC721Enumerable } from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Enumerable.sol"; 7 | import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; 8 | import { ERC721Inventory } from "../utils/ERC721Inventory.sol"; 9 | import { ERC721TokenId } from "../utils/ERC721TokenId.sol"; 10 | 11 | /** 12 | * @dev Implementation of https://eips.ethereum.org/EIPS/eip-721[ERC721] Non-Fungible Token Standard, 13 | * including the Enumerable extension. 14 | * @author https://github.com/nftyte 15 | */ 16 | contract ERC721Enumerable is ERC721Supply, IERC721Enumerable { 17 | using ERC721Inventory for uint256; 18 | using ERC721TokenId for uint256; 19 | using ERC721TokenId for address; 20 | 21 | // Burned supply counter, set to 1 to save gas during burn 22 | uint256 private _burned = 1; 23 | 24 | // Array with all minter addresses, used for enumeration 25 | mapping(uint256 => address) private _minters; 26 | 27 | /** 28 | * @dev See {IERC165-supportsInterface}. 29 | */ 30 | function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC721) returns (bool) { 31 | return interfaceId == type(IERC721Enumerable).interfaceId || super.supportsInterface(interfaceId); 32 | } 33 | 34 | /** 35 | * @dev See {IERC721Enumerable-totalSupply}. 36 | */ 37 | function totalSupply() public view virtual returns (uint256) { 38 | unchecked { 39 | return _mintedSupply() - _burnedSupply(); 40 | } 41 | } 42 | 43 | /** 44 | * @dev See {IERC721Enumerable-tokenOfOwnerByIndex}. 45 | */ 46 | function tokenOfOwnerByIndex(address owner, uint256 index) public view virtual returns (uint256 tokenId) { 47 | unchecked { 48 | require(index++ < balanceOf(owner), "ERC721Enumerable: owner index out of bounds"); 49 | uint256 supply = _mintedSupply(); 50 | 51 | for (uint i; i < supply; i++) { 52 | address minter = _minters[i]; 53 | 54 | if (minter != address(0)) { 55 | tokenId = minter.toTokenId(); 56 | uint256 maxTokenId = tokenId + _mintBalanceOf(minter); 57 | 58 | while (tokenId < maxTokenId) { 59 | if (ownerOf(tokenId) == owner && --index == 0) { 60 | return tokenId; 61 | } 62 | 63 | tokenId++; 64 | } 65 | } 66 | } 67 | } 68 | } 69 | 70 | /** 71 | * @dev See {IERC721Enumerable-tokenByIndex}. 72 | */ 73 | function tokenByIndex(uint256 index) public view virtual returns (uint256 tokenId) { 74 | unchecked { 75 | require(index++ < totalSupply(), "ERC721Enumerable: global index out of bounds"); 76 | uint256 supply = _mintedSupply(); 77 | 78 | for (uint i; i < supply; i++) { 79 | address minter = _minters[i]; 80 | 81 | if (minter != address(0)) { 82 | tokenId = minter.toTokenId(); 83 | uint256 maxTokenId = tokenId + _mintBalanceOf(minter); 84 | 85 | while (tokenId < maxTokenId) { 86 | if (_exists(tokenId) && --index == 0) { 87 | return tokenId; 88 | } 89 | 90 | tokenId++; 91 | } 92 | } 93 | } 94 | } 95 | } 96 | 97 | /** 98 | * @dev See {ERC721-_mintTokens}. 99 | */ 100 | function _mintTokens(address to, uint256 amount) 101 | internal 102 | virtual 103 | override 104 | returns (uint256 tokenId, uint256 maxTokenId) 105 | { 106 | uint256 mintedSupply = _mintedSupply(); 107 | (tokenId, maxTokenId) = super._mintTokens(to, amount); 108 | 109 | unchecked { 110 | if (tokenId.index() == 0) { 111 | _minters[mintedSupply] = to; 112 | } 113 | } 114 | } 115 | 116 | /** 117 | * @dev Returns the number of burned tokens. 118 | */ 119 | function _burnedSupply() internal view virtual returns (uint256) { 120 | unchecked { 121 | return _burned - 1; 122 | } 123 | } 124 | 125 | /** 126 | * @dev See {ERC721-_burn}. 127 | */ 128 | function _burn(uint256 tokenId) internal virtual override { 129 | super._burn(tokenId); 130 | 131 | unchecked { 132 | _burned++; 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /contracts/token/ERC721/extensions/ERC721Metadata.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import { ERC721, IERC165 } from "../ERC721.sol"; 6 | import { IERC721Metadata } from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; 7 | import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; 8 | 9 | /** 10 | * @dev Implementation of https://eips.ethereum.org/EIPS/eip-721[ERC721] Non-Fungible Token Standard, 11 | * including the Metadata extension. 12 | * Note: Based on OpenZeppelin Contracts, see: 13 | * https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/ERC721.sol 14 | * @author https://github.com/nftyte 15 | */ 16 | contract ERC721Metadata is ERC721, IERC721Metadata { 17 | using Strings for uint256; 18 | 19 | // Token name 20 | string private _name; 21 | 22 | // Token symbol 23 | string private _symbol; 24 | 25 | /** 26 | * @dev Initializes the contract by setting a `name` and a `symbol` to the token collection. 27 | */ 28 | constructor(string memory name_, string memory symbol_) { 29 | _name = name_; 30 | _symbol = symbol_; 31 | } 32 | 33 | /** 34 | * @dev See {IERC165-supportsInterface}. 35 | */ 36 | function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC721) returns (bool) { 37 | return interfaceId == type(IERC721Metadata).interfaceId || super.supportsInterface(interfaceId); 38 | } 39 | 40 | /** 41 | * @dev See {IERC721Metadata-name}. 42 | */ 43 | function name() public view virtual returns (string memory) { 44 | return _name; 45 | } 46 | 47 | /** 48 | * @dev See {IERC721Metadata-symbol}. 49 | */ 50 | function symbol() public view virtual returns (string memory) { 51 | return _symbol; 52 | } 53 | 54 | /** 55 | * @dev See {IERC721Metadata-tokenURI}. 56 | */ 57 | function tokenURI(uint256 tokenId) public view virtual returns (string memory) { 58 | _requireMinted(tokenId); 59 | 60 | string memory baseURI = _baseURI(); 61 | return 62 | bytes(baseURI).length > 0 63 | ? string(abi.encodePacked(baseURI, tokenId.toString())) 64 | : ""; 65 | } 66 | 67 | /** 68 | * @dev Base URI for computing {tokenURI}. If set, the resulting URI for each 69 | * token will be the concatenation of the `baseURI` and the `tokenId`. Empty 70 | * by default, can be overridden in child contracts. 71 | */ 72 | function _baseURI() internal view virtual returns (string memory) { 73 | return ""; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /contracts/token/ERC721/extensions/ERC721Supply.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import { ERC721 } from "../ERC721.sol"; 6 | import { ERC721Inventory } from "../utils/ERC721Inventory.sol"; 7 | 8 | /** 9 | * @dev Implementation of https://eips.ethereum.org/EIPS/eip-721[ERC721] Non-Fungible Token Standard. 10 | * Note: Tracks and limits mint supply. 11 | * @author https://github.com/nftyte 12 | */ 13 | contract ERC721Supply is ERC721 { 14 | using ERC721Inventory for uint256; 15 | 16 | // Supply counter, set to 1 to save gas during mint 17 | uint256 private _supply = 1; 18 | 19 | /** 20 | * @dev Returns the maximum number of tokens that can be minted. 21 | * Note: Based on {ERC721Inventory-BALANCE_BITSIZE}. 22 | */ 23 | function _maxSupply() internal view virtual returns (uint256) { 24 | unchecked { 25 | return (1 << ERC721Inventory.BALANCE_BITSIZE) - 1; 26 | } 27 | } 28 | 29 | /** 30 | * @dev Returns the number of minted tokens. 31 | */ 32 | function _mintedSupply() internal view virtual returns (uint256) { 33 | unchecked { 34 | return _supply - 1; 35 | } 36 | } 37 | 38 | /** 39 | * @dev See {ERC721-_mintTokens}. 40 | */ 41 | function _mintTokens(address to, uint256 amount) 42 | internal 43 | virtual 44 | override 45 | returns (uint256 tokenId, uint256 maxTokenId) 46 | { 47 | unchecked { 48 | require(_mintedSupply() + amount < _maxSupply() + 1, "ERC721Supply: exceeds maximum supply"); 49 | (tokenId, maxTokenId) = super._mintTokens(to, amount); 50 | 51 | _supply += amount; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /contracts/token/ERC721/utils/ERC721Inventory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import { Bitmap } from "../../../utils/Bitmap.sol"; 6 | 7 | /** 8 | * @title ERC721 token inventory utilities 9 | * @author https://github.com/nftyte 10 | */ 11 | library ERC721Inventory { 12 | using Bitmap for uint256; 13 | 14 | uint256 constant BALANCE_BITSIZE = 16; 15 | uint256 constant BALANCE_BITMASK = (1 << BALANCE_BITSIZE) - 1; // 0xFFFF; 16 | uint256 constant CURRENT_SLOT_BITSIZE = 8; 17 | uint256 constant CURRENT_SLOT_BITMASK = (1 << CURRENT_SLOT_BITSIZE) - 1; // 0xFF; 18 | uint256 constant BITMAP_OFFSET = BALANCE_BITSIZE + CURRENT_SLOT_BITSIZE; // 24 19 | uint256 constant SLOTS_PER_INVENTORY = 256 - BITMAP_OFFSET; // 232 20 | 21 | function balance(uint256 inventory) internal pure returns (uint256) { 22 | return inventory & BALANCE_BITMASK; 23 | } 24 | 25 | function current(uint256 inventory) internal pure returns (uint256) { 26 | return (inventory >> BALANCE_BITSIZE) & CURRENT_SLOT_BITMASK; 27 | } 28 | 29 | function has(uint256 inventory, uint256 index) internal pure returns (bool) { 30 | unchecked { 31 | return inventory.isSet(BITMAP_OFFSET + index); 32 | } 33 | } 34 | 35 | function add(uint256 inventory, uint256 amount) internal pure returns (uint256) { 36 | unchecked { 37 | return 38 | inventory.setRange(BITMAP_OFFSET + current(inventory), amount) + 39 | (amount << BALANCE_BITSIZE) + 40 | amount; 41 | } 42 | } 43 | 44 | function remove(uint256 inventory, uint256 index) internal pure returns (uint256) { 45 | unchecked { 46 | return inventory.unset(BITMAP_OFFSET + index) - 1; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /contracts/token/ERC721/utils/ERC721Receiver.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import { IERC721Receiver } from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; 6 | 7 | /** 8 | * @title ERC721 token receiver utilities 9 | * @author https://github.com/nftyte 10 | */ 11 | library ERC721Receiver { 12 | function checkOnERC721Received( 13 | address to, 14 | address operator, 15 | address from, 16 | uint256 tokenId, 17 | bytes memory data 18 | ) internal returns (bool) { 19 | try IERC721Receiver(to).onERC721Received(operator, from, tokenId, data) returns ( 20 | bytes4 retval 21 | ) { 22 | return retval == IERC721Receiver.onERC721Received.selector; 23 | } catch (bytes memory reason) { 24 | if (reason.length == 0) { 25 | revert("ERC721: transfer to non ERC721Receiver implementer"); 26 | } else { 27 | assembly { 28 | revert(add(reason, 0x20), mload(reason)) 29 | } 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /contracts/token/ERC721/utils/ERC721TokenId.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | /** 6 | * @title ERC721 token id utilities 7 | * @author https://github.com/nftyte 8 | */ 9 | library ERC721TokenId { 10 | uint256 constant MINTER_OFFSET = 8; 11 | uint256 constant INDEX_BITMASK = (1 << MINTER_OFFSET) - 1; // 0xFF 12 | 13 | function toTokenId(address owner) internal pure returns (uint256) { 14 | return uint256(uint160(owner)) << MINTER_OFFSET; 15 | } 16 | 17 | function index(uint256 tokenId) internal pure returns (uint256) { 18 | return tokenId & INDEX_BITMASK; 19 | } 20 | 21 | function minter(uint256 tokenId) internal pure returns (address) { 22 | return address(uint160(tokenId >> MINTER_OFFSET)); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /contracts/utils/Bitmap.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | /** 6 | * @title Bitmap utilities 7 | * @author https://github.com/nftyte 8 | */ 9 | library Bitmap { 10 | function get(uint256 bitmap, uint256 index) internal pure returns (uint256) { 11 | return (bitmap >> index) & 1; 12 | } 13 | 14 | function isSet(uint256 bitmap, uint256 index) internal pure returns (bool) { 15 | return get(bitmap, index) == 1; 16 | } 17 | 18 | function set(uint256 bitmap, uint256 index) internal pure returns (uint256) { 19 | return bitmap | (1 << index); 20 | } 21 | 22 | function setRange(uint256 bitmap, uint256 offset, uint256 amount) internal pure returns (uint256) { 23 | unchecked { 24 | return bitmap | (((1 << amount) - 1) << offset); 25 | } 26 | } 27 | 28 | function unset(uint256 bitmap, uint256 index) internal pure returns (uint256) { 29 | return bitmap & toggle(type(uint256).max, index); 30 | } 31 | 32 | function unsetRange(uint256 bitmap, uint256 offset, uint256 amount) internal pure returns (uint256) { 33 | return bitmap & toggleRange(type(uint256).max, offset, amount); 34 | } 35 | 36 | function toggle(uint256 bitmap, uint256 index) internal pure returns (uint256) { 37 | return bitmap ^ (1 << index); 38 | } 39 | 40 | function toggleRange(uint256 bitmap, uint256 offset, uint256 amount) internal pure returns (uint256) { 41 | unchecked { 42 | return bitmap ^ (((1 << amount) - 1) << offset); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /hardhat.config.js: -------------------------------------------------------------------------------- 1 | // hardhat.config.js 2 | require("@nomiclabs/hardhat-ethers"); 3 | 4 | /** @type import('hardhat/config').HardhatUserConfig */ 5 | module.exports = { 6 | // networks: { 7 | // hardhat: { 8 | // loggingEnabled: true, 9 | // }, 10 | // }, 11 | solidity: { 12 | version: "0.8.15", 13 | settings: { 14 | viaIR: true, 15 | optimizer: { 16 | enabled: true, 17 | runs: 200, 18 | }, 19 | }, 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "optimint", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/nftyte/optimint.git" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "ISC", 16 | "bugs": { 17 | "url": "https://github.com/nftyte/optimint/issues" 18 | }, 19 | "homepage": "https://github.com/nftyte/optimint#readme", 20 | "devDependencies": { 21 | "@nomiclabs/hardhat-ethers": "^2.1.0", 22 | "@openzeppelin/contracts": "^4.7.1", 23 | "erc721a": "^4.2.0", 24 | "ethers": "^5.6.9", 25 | "hardhat": "^2.10.1" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /scripts/benchmarks.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const { join } = require("path"); 3 | const { deployable } = require("./libraries/deployable"); 4 | 5 | const TransferType = { 6 | OwnerToNonOwner: { 7 | label: "Owner to non-owner", 8 | transferTokens: [1], 9 | }, 10 | OwnerToOwner: { 11 | label: "Owner to owner", 12 | transferTokens: [1], 13 | }, 14 | MinterToNonOwner: { 15 | label: "Minter to non-owner", 16 | transferTokens: [1, 2, 3, 4, 5, 10, 50, 100], 17 | }, 18 | MinterToOwner: { 19 | label: "Minter to owner", 20 | transferTokens: [1, 2, 3, 4, 5, 10, 50, 100], 21 | }, 22 | }, 23 | mintAmounts = [1, 2, 3, 4, 5, 10, 50, 100], 24 | contracts = ["Optimint", "OptimintSupply", "OptimintEnumerable", "ERC721AMock"], 25 | results = Object.fromEntries( 26 | contracts.map((c) => [ 27 | c, 28 | { 29 | mint: {}, 30 | transfer: Object.fromEntries( 31 | Object.keys(TransferType).map((k) => [k, {}]) 32 | ), 33 | }, 34 | ]) 35 | ); 36 | 37 | async function benchmarks() { 38 | const [owner, ...accs] = await ethers.getSigners(); 39 | 40 | let tx, receipt; 41 | 42 | for (let contractName of contracts) { 43 | const Contract = await deployable(contractName).contract(); 44 | let contract = await Contract.deploy(); 45 | tx = await contract.deployed(); 46 | results[contractName].deploy = tx.deployTransaction.gasLimit; 47 | 48 | for (let amount of mintAmounts) { 49 | contract = await deployable(contractName).deploy(); 50 | if (contractName == "ERC721AMock") { 51 | // Preliminary mint to reduce gas during benchmark 52 | await contract.connect(accs[accs.length - 1]).mint(1); 53 | } 54 | tx = await contract.mint(amount); 55 | receipt = await tx.wait(); 56 | results[contractName].mint[amount] = receipt.gasUsed; 57 | } 58 | 59 | for (let k of Object.keys(TransferType)) { 60 | await Promise.all( 61 | TransferType[k].transferTokens.map((i) => 62 | transferBenchmark(contractName, owner, accs, parseInt(i), k) 63 | ) 64 | ); 65 | } 66 | 67 | console.log(`Completed benchmarks for ${contractName.replace("Mock", "")}`); 68 | } 69 | 70 | let res = `## Deployment\n\n${deployResults(results)}\n\n`; 71 | res += `## Mint\n\n${mintResults(results)}\n\n`; 72 | res += "## Transfer\n\n"; 73 | for (let [k, { label }] of Object.entries(TransferType)) { 74 | res += `### ${label}\n\n${transferResults(results, k)}\n\n`; 75 | } 76 | 77 | console.log("Completed benchmarks, writing to file...\n"); 78 | fs.writeFileSync(join(process.cwd(), "benchmarks.md"), res); 79 | console.log(res); 80 | } 81 | 82 | function deployResults(results) { 83 | let table = tableRow("Contract", "Deploy"); 84 | table += "\n" + tableRow("---", "---"); 85 | 86 | for (let [c, r] of Object.entries(results)) { 87 | table += 88 | "\n" + tableRow(c.replace("Mock", ""), parseInt(r.deploy).toLocaleString()); 89 | } 90 | 91 | return table; 92 | } 93 | 94 | function mintResults(results) { 95 | let table = tableRow("Contract", ...mintAmounts.map((m) => `Mint ${m}`)); 96 | table += "\n" + tableRow("---", ...mintAmounts.map(() => "---")); 97 | 98 | for (let [c, r] of Object.entries(results)) { 99 | table += 100 | "\n" + 101 | tableRow( 102 | c.replace("Mock", ""), 103 | ...mintAmounts.map((m) => parseInt(r.mint[m]).toLocaleString()) 104 | ); 105 | } 106 | 107 | return table; 108 | } 109 | 110 | function transferResults(results, k) { 111 | const transferTokens = TransferType[k].transferTokens; 112 | let table = tableRow("Contract", ...transferTokens.map((t) => `Token #${t}`)); 113 | table += "\n" + tableRow("---", ...transferTokens.map(() => "---")); 114 | 115 | for (let [c, r] of Object.entries(results)) { 116 | table += 117 | "\n" + 118 | tableRow( 119 | c.replace("Mock", ""), 120 | ...transferTokens.map((t) => parseInt(r.transfer[k][t]).toLocaleString()) 121 | ); 122 | } 123 | 124 | return table; 125 | } 126 | 127 | function tableRow(...values) { 128 | return `| ${values.join(" | ")} |`; 129 | } 130 | 131 | async function transferBenchmark(contractName, owner, accs, nthToken, transferType) { 132 | const tokens = {}, 133 | contract = await deployable(contractName).deploy(); 134 | await contract.mint(nthToken + 1); 135 | await new Promise((resolve) => { 136 | let count = 0; 137 | contract.on("Transfer", (from, to, tokenId) => { 138 | if (from == ethers.constants.AddressZero && to == owner.address) { 139 | tokens[tokenId.toHexString()] = tokenId; 140 | if (++count == nthToken + 1) { 141 | resolve(); 142 | } 143 | } 144 | }); 145 | }); 146 | 147 | let from, to; 148 | const tokenIds = Object.values(tokens); 149 | 150 | switch (TransferType[transferType]) { 151 | case TransferType.MinterToOwner: 152 | await contract.connect(accs[0]).mint(1); 153 | case TransferType.MinterToNonOwner: 154 | from = owner; 155 | to = accs[0]; 156 | break; 157 | case TransferType.OwnerToOwner: 158 | await contract.connect(accs[1]).mint(1); 159 | case TransferType.OwnerToNonOwner: 160 | const transfers = []; 161 | for (let j = 0; j < nthToken + 1; j++) { 162 | transfers.push( 163 | contract.transferFrom(owner.address, accs[0].address, tokenIds[j]) 164 | ); 165 | } 166 | await Promise.all(transfers); 167 | from = accs[0]; 168 | to = accs[1]; 169 | break; 170 | } 171 | 172 | const tx = await contract 173 | .connect(from) 174 | .transferFrom(from.address, to.address, tokenIds[nthToken - 1]), 175 | receipt = await tx.wait(); 176 | results[contractName].transfer[transferType][nthToken] = receipt.gasUsed; 177 | } 178 | 179 | if (require.main === module) { 180 | benchmarks() 181 | .then(() => process.exit(0)) 182 | .catch((error) => { 183 | console.error(error); 184 | process.exit(1); 185 | }); 186 | } 187 | -------------------------------------------------------------------------------- /scripts/libraries/deployable.js: -------------------------------------------------------------------------------- 1 | function Deployable(contractName) { 2 | this.contractName = contractName; 3 | } 4 | 5 | Object.assign(Deployable.prototype, { 6 | async contract() { 7 | return await ethers.getContractFactory(this.contractName); 8 | }, 9 | async at(address) { 10 | return await ethers.getContractAt(this.contractName, address); 11 | }, 12 | async deploy(...args) { 13 | const contract = await this.contract(); 14 | const deployed = await contract.deploy(...args); 15 | await deployed.deployed(); 16 | return deployed; 17 | }, 18 | }); 19 | 20 | exports.deployer = (contractName) => ({ 21 | async deploy(...args) { 22 | const deployable = new Deployable(await ethers.getContractFactory(contractName)); 23 | return await deployable.deploy(...args); 24 | }, 25 | }); 26 | 27 | exports.deployable = (contractName) => new Deployable(contractName); 28 | --------------------------------------------------------------------------------