Skip to content

Commit

Permalink
Start Sequentially Minting at _startTokenId() instead of hardcoded 0 (
Browse files Browse the repository at this point in the history
#66)

* replaces currentIndex with _nextTokenId which starts at 1 instead of 0

* adapts test to new starting index of 1

* makes `ERC721AOwnersExplicit` test work with new starting index

* explicitly sets `nextOwnerToExplicitlySet` to 1 because it's initialised with zero

* remove vscode files

* implements @jpegditials suggestions

* adds tests for `tokenByIndex` and `tokenOfOwnerByIndex`

* prevents tokenId 0 to be an existing token

* adapts `tokenOfOwnerByIndex` for starting token index 1 and making tests for it more elaborate.

* renames variable to make it more clear, it's index by the tokenId

* adds unchecked scopes to save on gas as suggested by @Vectorized

* more gas optimizations by @Vectorized

* implements suggested `_startTokenId` function to override to set starting index

* minor adaptions to work with _startTokenId() properly

* brings back original tests

* creates copy of original test with startTokenId set to 1

* adapts comment to reflect flexibility of the starting tokenId

* adapts solidity version of tests

* Replaces revert messages with new contract errors

* fix merge hiccup

* optimisations to save on gas used during deployment

* Added #61

* Edited comments

* Edited README roadmap

* Removed burn test with one-index mock

* Changed _startTokenId to _currentIndex

* adapting test and mock related to index starting at 1 to make them work with recent changes on `main`

* add _totalMinted function

* add tests for _totalMinted

* removes uint128 cast, simplifies comment and lints file

* rename `ERC721AExplicitOwnershipMock` to `ERC721AOwnersExplicitMock` to be aligned with the naming of the contract

* extend from already mocked ERC721A versions to dry code.

* aligns file naming and contract naming

* creates new ERC721A mock which enables the user to specify the startTokenId dynamically

* converts ERC721A test to a parameterized test to work with other mocks as well.

* extracts Helper contract to it's own file

* parameterises ERC721A Explicit Owner Tests to also test a Version of the Contract with a different start token id.

* makes it more transparent where the startTokenId value is coming from

* creates a ERC721ABurnable Mock with custom startTokenId and uses it in a parameterised test suite testing the burnable extension.

* removes code used during development

* unifies code style across all tests

Co-authored-by: webby1111 <webby1111@hotmail.com>
Co-authored-by: Amirhossein Banavi <ahbanavi@gmail.com>
  • Loading branch information
3 people authored Feb 26, 2022
1 parent 810629d commit 6064c6a
Show file tree
Hide file tree
Showing 13 changed files with 655 additions and 455 deletions.
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ contract Azuki is ERC721A {

## Roadmap

- [] Add flexibility for the first token id to not start at 0
- [] Support ERC721 Upgradeable
- [] Add more documentation on benefits of using ERC721A
- [] Increase test coverage
Expand Down
39 changes: 30 additions & 9 deletions contracts/ERC721A.sol
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ error URIQueryForNonexistentToken();
* @dev Implementation of https://eips.ethereum.org/EIPS/eip-721[ERC721] Non-Fungible Token Standard, including
* the Metadata extension. Built to optimize for lower gas during batch mints.
*
* Assumes serials are sequentially minted starting at 0 (e.g. 0, 1, 2, 3..).
* Assumes serials are sequentially minted starting at _startTokenId() (defaults to 0, e.g. 0, 1, 2, 3..).
*
* Assumes that an owner cannot have more than 2**64 - 1 (max value of uint64) of supply.
*
Expand Down Expand Up @@ -64,7 +64,7 @@ contract ERC721A is Context, ERC165, IERC721, IERC721Metadata {
// Keeps track of burn count with minimal overhead for tokenomics.
uint64 numberBurned;
// For miscellaneous variable(s) pertaining to the address
// (e.g. number of whitelist mint slots used).
// (e.g. number of whitelist mint slots used).
// If there are multiple variables, please pack them into a uint64.
uint64 aux;
}
Expand Down Expand Up @@ -97,16 +97,36 @@ contract ERC721A is Context, ERC165, IERC721, IERC721Metadata {
constructor(string memory name_, string memory symbol_) {
_name = name_;
_symbol = symbol_;
_currentIndex = _startTokenId();
}

/**
* To change the starting tokenId, please override this function.
*/
function _startTokenId() internal view virtual returns (uint256) {
return 0;
}

/**
* @dev See {IERC721Enumerable-totalSupply}.
* @dev Burned tokens are calculated here, use _totalMinted() if you want to count just minted tokens.
*/
function totalSupply() public view returns (uint256) {
// Counter underflow is impossible as _burnCounter cannot be incremented
// more than _currentIndex times
// more than _currentIndex - _startTokenId() times
unchecked {
return _currentIndex - _burnCounter;
return _currentIndex - _burnCounter - _startTokenId();
}
}

/**
* Returns the total amount of tokens minted in the contract.
*/
function _totalMinted() internal view returns (uint256) {
// Counter underflow is impossible as _currentIndex does not decrement,
// and it is initialized to _startTokenId()
unchecked {
return _currentIndex - _startTokenId();
}
}

Expand Down Expand Up @@ -169,14 +189,14 @@ contract ERC721A is Context, ERC165, IERC721, IERC721Metadata {
uint256 curr = tokenId;

unchecked {
if (curr < _currentIndex) {
if (_startTokenId() <= curr && curr < _currentIndex) {
TokenOwnership memory ownership = _ownerships[curr];
if (!ownership.burned) {
if (ownership.addr != address(0)) {
return ownership;
}
// Invariant:
// There will always be an ownership that has an address and is not burned
// Invariant:
// There will always be an ownership that has an address and is not burned
// before an ownership that does not have an address and is not burned.
// Hence, curr will not underflow.
while (true) {
Expand Down Expand Up @@ -317,7 +337,8 @@ contract ERC721A is Context, ERC165, IERC721, IERC721Metadata {
* Tokens start existing when they are minted (`_mint`),
*/
function _exists(uint256 tokenId) internal view returns (bool) {
return tokenId < _currentIndex && !_ownerships[tokenId].burned;
return _startTokenId() <= tokenId && tokenId < _currentIndex &&
!_ownerships[tokenId].burned;
}

function _safeMint(address to, uint256 quantity) internal {
Expand Down Expand Up @@ -493,7 +514,7 @@ contract ERC721A is Context, ERC165, IERC721, IERC721Metadata {
_afterTokenTransfers(prevOwnership.addr, address(0), tokenId, 1);

// Overflow not possible, as _burnCounter cannot be exceed _currentIndex times.
unchecked {
unchecked {
_burnCounter++;
}
}
Expand Down
5 changes: 4 additions & 1 deletion contracts/extensions/ERC721AOwnersExplicit.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@ abstract contract ERC721AOwnersExplicit is ERC721A {
*/
function _setOwnersExplicit(uint256 quantity) internal {
if (quantity == 0) revert QuantityMustBeNonZero();
if (_currentIndex == 0) revert NoTokensMintedYet();
if (_currentIndex == _startTokenId()) revert NoTokensMintedYet();
uint256 _nextOwnerToExplicitlySet = nextOwnerToExplicitlySet;
if (_nextOwnerToExplicitlySet == 0) {
_nextOwnerToExplicitlySet = _startTokenId();
}
if (_nextOwnerToExplicitlySet >= _currentIndex) revert AllOwnershipsHaveBeenSet();

// Index underflow is impossible.
Expand Down
4 changes: 4 additions & 0 deletions contracts/mocks/ERC721ABurnableMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,8 @@ contract ERC721ABurnableMock is ERC721A, ERC721ABurnable {
function getOwnershipAt(uint256 index) public view returns (TokenOwnership memory) {
return _ownerships[index];
}

function totalMinted() public view returns (uint256) {
return _totalMinted();
}
}
19 changes: 19 additions & 0 deletions contracts/mocks/ERC721ABurnableStartTokenIdMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// SPDX-License-Identifier: MIT
// Creators: Chiru Labs

pragma solidity ^0.8.4;

import './ERC721ABurnableMock.sol';
import './StartTokenIdHelper.sol';

contract ERC721ABurnableStartTokenIdMock is StartTokenIdHelper, ERC721ABurnableMock {
constructor(
string memory name_,
string memory symbol_,
uint256 startTokenId_
) StartTokenIdHelper(startTokenId_) ERC721ABurnableMock(name_, symbol_) {}

function _startTokenId() internal view override returns (uint256) {
return startTokenId;
}
}
4 changes: 4 additions & 0 deletions contracts/mocks/ERC721AMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ contract ERC721AMock is ERC721A {
return _numberMinted(owner);
}

function totalMinted() public view returns (uint256) {
return _totalMinted();
}

function getAux(address owner) public view returns (uint64) {
return _getAux(owner);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ contract ERC721AOwnersExplicitMock is ERC721AOwnersExplicit {
_setOwnersExplicit(quantity);
}

function getOwnershipAt(uint256 index) public view returns (TokenOwnership memory) {
return _ownerships[index];
function getOwnershipAt(uint256 tokenId) public view returns (TokenOwnership memory) {
return _ownerships[tokenId];
}
}
19 changes: 19 additions & 0 deletions contracts/mocks/ERC721AOwnersExplicitStartTokenIdMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// SPDX-License-Identifier: MIT
// Creators: Chiru Labs

pragma solidity ^0.8.4;

import './ERC721AOwnersExplicitMock.sol';
import './StartTokenIdHelper.sol';

contract ERC721AOwnersExplicitStartTokenIdMock is StartTokenIdHelper, ERC721AOwnersExplicitMock {
constructor(
string memory name_,
string memory symbol_,
uint256 startTokenId_
) StartTokenIdHelper(startTokenId_) ERC721AOwnersExplicitMock(name_, symbol_) {}

function _startTokenId() internal view override returns (uint256) {
return startTokenId;
}
}
19 changes: 19 additions & 0 deletions contracts/mocks/ERC721AStartTokenIdMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// SPDX-License-Identifier: MIT
// Creators: Chiru Labs

pragma solidity ^0.8.4;

import './ERC721AMock.sol';
import './StartTokenIdHelper.sol';

contract ERC721AStartTokenIdMock is StartTokenIdHelper, ERC721AMock {
constructor(
string memory name_,
string memory symbol_,
uint256 startTokenId_
) StartTokenIdHelper(startTokenId_) ERC721AMock(name_, symbol_) {}

function _startTokenId() internal view override returns (uint256) {
return startTokenId;
}
}
17 changes: 17 additions & 0 deletions contracts/mocks/StartTokenIdHelper.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// SPDX-License-Identifier: MIT
// Creators: Chiru Labs

pragma solidity ^0.8.4;

/**
* This Helper is used to return a dynmamic value in the overriden _startTokenId() function.
* Extending this Helper before the ERC721A contract give us access to the herein set `startTokenId`
* to be returned by the overriden `_startTokenId()` function of ERC721A in the ERC721AStartTokenId mocks.
*/
contract StartTokenIdHelper {
uint256 public immutable startTokenId;

constructor(uint256 startTokenId_) {
startTokenId = startTokenId_;
}
}
Loading

0 comments on commit 6064c6a

Please sign in to comment.