Skip to content

Commit 6064c6a

Browse files
PczekVectorizedahbanavi
authored
Start Sequentially Minting at _startTokenId() instead of hardcoded 0 (chiru-labs#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 chiru-labs#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>
1 parent 810629d commit 6064c6a

13 files changed

+655
-455
lines changed

README.md

-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ contract Azuki is ERC721A {
5454

5555
## Roadmap
5656

57-
- [] Add flexibility for the first token id to not start at 0
5857
- [] Support ERC721 Upgradeable
5958
- [] Add more documentation on benefits of using ERC721A
6059
- [] Increase test coverage

contracts/ERC721A.sol

+30-9
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ error URIQueryForNonexistentToken();
3535
* @dev Implementation of https://eips.ethereum.org/EIPS/eip-721[ERC721] Non-Fungible Token Standard, including
3636
* the Metadata extension. Built to optimize for lower gas during batch mints.
3737
*
38-
* Assumes serials are sequentially minted starting at 0 (e.g. 0, 1, 2, 3..).
38+
* Assumes serials are sequentially minted starting at _startTokenId() (defaults to 0, e.g. 0, 1, 2, 3..).
3939
*
4040
* Assumes that an owner cannot have more than 2**64 - 1 (max value of uint64) of supply.
4141
*
@@ -64,7 +64,7 @@ contract ERC721A is Context, ERC165, IERC721, IERC721Metadata {
6464
// Keeps track of burn count with minimal overhead for tokenomics.
6565
uint64 numberBurned;
6666
// For miscellaneous variable(s) pertaining to the address
67-
// (e.g. number of whitelist mint slots used).
67+
// (e.g. number of whitelist mint slots used).
6868
// If there are multiple variables, please pack them into a uint64.
6969
uint64 aux;
7070
}
@@ -97,16 +97,36 @@ contract ERC721A is Context, ERC165, IERC721, IERC721Metadata {
9797
constructor(string memory name_, string memory symbol_) {
9898
_name = name_;
9999
_symbol = symbol_;
100+
_currentIndex = _startTokenId();
101+
}
102+
103+
/**
104+
* To change the starting tokenId, please override this function.
105+
*/
106+
function _startTokenId() internal view virtual returns (uint256) {
107+
return 0;
100108
}
101109

102110
/**
103111
* @dev See {IERC721Enumerable-totalSupply}.
112+
* @dev Burned tokens are calculated here, use _totalMinted() if you want to count just minted tokens.
104113
*/
105114
function totalSupply() public view returns (uint256) {
106115
// Counter underflow is impossible as _burnCounter cannot be incremented
107-
// more than _currentIndex times
116+
// more than _currentIndex - _startTokenId() times
108117
unchecked {
109-
return _currentIndex - _burnCounter;
118+
return _currentIndex - _burnCounter - _startTokenId();
119+
}
120+
}
121+
122+
/**
123+
* Returns the total amount of tokens minted in the contract.
124+
*/
125+
function _totalMinted() internal view returns (uint256) {
126+
// Counter underflow is impossible as _currentIndex does not decrement,
127+
// and it is initialized to _startTokenId()
128+
unchecked {
129+
return _currentIndex - _startTokenId();
110130
}
111131
}
112132

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

171191
unchecked {
172-
if (curr < _currentIndex) {
192+
if (_startTokenId() <= curr && curr < _currentIndex) {
173193
TokenOwnership memory ownership = _ownerships[curr];
174194
if (!ownership.burned) {
175195
if (ownership.addr != address(0)) {
176196
return ownership;
177197
}
178-
// Invariant:
179-
// There will always be an ownership that has an address and is not burned
198+
// Invariant:
199+
// There will always be an ownership that has an address and is not burned
180200
// before an ownership that does not have an address and is not burned.
181201
// Hence, curr will not underflow.
182202
while (true) {
@@ -317,7 +337,8 @@ contract ERC721A is Context, ERC165, IERC721, IERC721Metadata {
317337
* Tokens start existing when they are minted (`_mint`),
318338
*/
319339
function _exists(uint256 tokenId) internal view returns (bool) {
320-
return tokenId < _currentIndex && !_ownerships[tokenId].burned;
340+
return _startTokenId() <= tokenId && tokenId < _currentIndex &&
341+
!_ownerships[tokenId].burned;
321342
}
322343

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

495516
// Overflow not possible, as _burnCounter cannot be exceed _currentIndex times.
496-
unchecked {
517+
unchecked {
497518
_burnCounter++;
498519
}
499520
}

contracts/extensions/ERC721AOwnersExplicit.sol

+4-1
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,11 @@ abstract contract ERC721AOwnersExplicit is ERC721A {
1717
*/
1818
function _setOwnersExplicit(uint256 quantity) internal {
1919
if (quantity == 0) revert QuantityMustBeNonZero();
20-
if (_currentIndex == 0) revert NoTokensMintedYet();
20+
if (_currentIndex == _startTokenId()) revert NoTokensMintedYet();
2121
uint256 _nextOwnerToExplicitlySet = nextOwnerToExplicitlySet;
22+
if (_nextOwnerToExplicitlySet == 0) {
23+
_nextOwnerToExplicitlySet = _startTokenId();
24+
}
2225
if (_nextOwnerToExplicitlySet >= _currentIndex) revert AllOwnershipsHaveBeenSet();
2326

2427
// Index underflow is impossible.

contracts/mocks/ERC721ABurnableMock.sol

+4
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,8 @@ contract ERC721ABurnableMock is ERC721A, ERC721ABurnable {
1919
function getOwnershipAt(uint256 index) public view returns (TokenOwnership memory) {
2020
return _ownerships[index];
2121
}
22+
23+
function totalMinted() public view returns (uint256) {
24+
return _totalMinted();
25+
}
2226
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// SPDX-License-Identifier: MIT
2+
// Creators: Chiru Labs
3+
4+
pragma solidity ^0.8.4;
5+
6+
import './ERC721ABurnableMock.sol';
7+
import './StartTokenIdHelper.sol';
8+
9+
contract ERC721ABurnableStartTokenIdMock is StartTokenIdHelper, ERC721ABurnableMock {
10+
constructor(
11+
string memory name_,
12+
string memory symbol_,
13+
uint256 startTokenId_
14+
) StartTokenIdHelper(startTokenId_) ERC721ABurnableMock(name_, symbol_) {}
15+
16+
function _startTokenId() internal view override returns (uint256) {
17+
return startTokenId;
18+
}
19+
}

contracts/mocks/ERC721AMock.sol

+4
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ contract ERC721AMock is ERC721A {
1212
return _numberMinted(owner);
1313
}
1414

15+
function totalMinted() public view returns (uint256) {
16+
return _totalMinted();
17+
}
18+
1519
function getAux(address owner) public view returns (uint64) {
1620
return _getAux(owner);
1721
}

contracts/mocks/ERC721AExplicitOwnershipMock.sol contracts/mocks/ERC721AOwnersExplicitMock.sol

+2-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ contract ERC721AOwnersExplicitMock is ERC721AOwnersExplicit {
1616
_setOwnersExplicit(quantity);
1717
}
1818

19-
function getOwnershipAt(uint256 index) public view returns (TokenOwnership memory) {
20-
return _ownerships[index];
19+
function getOwnershipAt(uint256 tokenId) public view returns (TokenOwnership memory) {
20+
return _ownerships[tokenId];
2121
}
2222
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// SPDX-License-Identifier: MIT
2+
// Creators: Chiru Labs
3+
4+
pragma solidity ^0.8.4;
5+
6+
import './ERC721AOwnersExplicitMock.sol';
7+
import './StartTokenIdHelper.sol';
8+
9+
contract ERC721AOwnersExplicitStartTokenIdMock is StartTokenIdHelper, ERC721AOwnersExplicitMock {
10+
constructor(
11+
string memory name_,
12+
string memory symbol_,
13+
uint256 startTokenId_
14+
) StartTokenIdHelper(startTokenId_) ERC721AOwnersExplicitMock(name_, symbol_) {}
15+
16+
function _startTokenId() internal view override returns (uint256) {
17+
return startTokenId;
18+
}
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// SPDX-License-Identifier: MIT
2+
// Creators: Chiru Labs
3+
4+
pragma solidity ^0.8.4;
5+
6+
import './ERC721AMock.sol';
7+
import './StartTokenIdHelper.sol';
8+
9+
contract ERC721AStartTokenIdMock is StartTokenIdHelper, ERC721AMock {
10+
constructor(
11+
string memory name_,
12+
string memory symbol_,
13+
uint256 startTokenId_
14+
) StartTokenIdHelper(startTokenId_) ERC721AMock(name_, symbol_) {}
15+
16+
function _startTokenId() internal view override returns (uint256) {
17+
return startTokenId;
18+
}
19+
}
+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// SPDX-License-Identifier: MIT
2+
// Creators: Chiru Labs
3+
4+
pragma solidity ^0.8.4;
5+
6+
/**
7+
* This Helper is used to return a dynmamic value in the overriden _startTokenId() function.
8+
* Extending this Helper before the ERC721A contract give us access to the herein set `startTokenId`
9+
* to be returned by the overriden `_startTokenId()` function of ERC721A in the ERC721AStartTokenId mocks.
10+
*/
11+
contract StartTokenIdHelper {
12+
uint256 public immutable startTokenId;
13+
14+
constructor(uint256 startTokenId_) {
15+
startTokenId = startTokenId_;
16+
}
17+
}

0 commit comments

Comments
 (0)