-
Notifications
You must be signed in to change notification settings - Fork 11.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Ability to set starting token id for ERC721Consecutive #4097
Conversation
🦋 Changeset detectedLatest commit: 7959df3 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Hello @kfishnchips There is something troublesome with you implementation: Additionally, reading IMO, if this is a feature we want, we should implement it by allowing
That way you could create gaps in your allocation, not only before the first |
Was it naive from me to think that minted tokens would only have token ids larger than the batch? Now that I think of it, you could mint batches with gaps and then have |
|
@Amxx is this last commit somewhere along the lines of what you were thinking? |
Thanks for changing your PR so fast. Usually we discuss the feature before going into an actual implementation. @frangio how do you feel about it? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tests needs more checks, but please wait until we discuss that with @frangio and @ernestognw before putting work into it.
Allowing batches to start with a larger token id makes sense to me. However, I would not reuse What are the performance implications of this implementation approach? |
The performance implication is that the the trace array will be longer, making |
That seems like a big price to pay for a feature that appears to be implementable essentially for free with a single immutable variable (or pure virtual function) to mark the start of the consecutive mints. Is there a reason I'm not seeing why that wouldn't work? It's not exactly the same as |
See the first part of this message. TLDR:
While we are at it, don't we want to support gaps ? That sounds like a reasonable feature. |
From a user standpoint, I think most comfortable solution would be to have a virtual function that I can override to specify a starting token id. I'm not sure that this contract is scoped only for such as use case so I understand the discussion around creating gaps. |
Gaps sound "reasonable" but no one has requested it and I feel that we should favor the more efficient option here. |
To me, it felt efficient in the sens that it reuses existing logic, and doesn't add more logic to this contract. An immutable initial offset might be more gas effective, but it also adds code complexity. |
Generally I agree but when we're talking about removing SSTORE/SLOAD, I think additional code complexity is justifiable. In particular this contract has the specific goal of being efficient. |
Ok @kfishnchips lets go back to implementing this with a function _firstConsecutiveId() internal view virtual returns (uint96) { return 0; } We need to make sure that |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just leaving some small suggestions, but I agree with the approach of an overrideable function so far.
Wouldn't we need to revert for |
@ernestognw @Amxx I believe that what was requested is ready, additional tests using bound and the internal overridable function. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We still need to address the consistency of the range checks when there's a _firstConsecutiveId()
and add tests for that. Currently, although the _maxBatchSize()
is 5000, there are missing edge cases to consider:
- What happens if a regular
_mint(...)
is before the_firstConsecutiveId()
? - Are we testing correctly the effects on
_afterTokenTransfer
andownerOf(...)
?
I added detailed comments with my thoughts, but still, thank you @kfishnchips for the fast response.
function _totalConsecutiveSupply() private view returns (uint96) { | ||
(bool exists, uint96 latestId, ) = _sequentialOwnership.latestCheckpoint(); | ||
return exists ? latestId + 1 : 0; | ||
return exists ? latestId + 1 : _firstConsecutiveId(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think 0 to _firstConsecutiveId()
should be accounted for the _totalConsecutiveSupply
, it affects line 135, which will be excluding the after hook for real burned tokenId
s
For example:
- Someone replaces
_firstConsecutiveId()
with a5
- They consecutively mint
tokenId
s from 5 to 9 (5 mints) - After that, someone else mints the token
2
via normal minting - Then, they burn it, and the token will make it to the
_sequentialBurn
on theownerOf(...)
method, which is not what we want
I think this is what we should aim for:
return exists ? latestId + 1 : _firstConsecutiveId(); | |
return exists ? latestId + 1 - _firstConsecutiveId() : 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it make more sense for someone to consecutively mint token 10
instead of 2
? I was thinking that there could be no minting below the first consecutive id.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would requiring that the tokenId be greater than the last batch minted one in the _mint
function be a viable option? It might be too inefficient...
function _mint(address to, uint256 tokenId) internal virtual override {
require(Address.isContract(address(this)), "ERC721Consecutive: can't mint during construction");
(, uint96 latestId, ) = _sequentialOwnership.latestCheckpoint();
require(tokenId > latestId, "ERC721Consecutive: token id must be greater than latest id");
super._mint(to, tokenId);
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That is something we don't want, because it adds 2 (cold) sload to the mint operation, which we want to keep as cheap as possible.
Also, if we do that, it would be impossible to _mint
a token that was first minted as part of a batch then burnt.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
note that mint already check if the tokenId has an owner, and revert if that is the case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, I'm still wrapping my head around the internals of this but I think I got it.
|
||
uint256 startingTokenId = bound(startingId, 1, 5000); | ||
|
||
// We assume _maxBatchSize is 5000 (the default). This test will break otherwise. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this breaking? If it's breaking because of the ownerOf
range check, we definitely need to address that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment about the breaking test is from a previous commit which I still can't decipher
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test tries to mint a batch that is too large. too large means bigger than _maxBatchSize
. Since the contract is not yet constructed, we assume the value is 5000. If you ever override _maxBatchSize
in ERC721ConsecutiveTarget
, you should make sure it is reflected here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, understood. Thanks for clarifying!
function _ownerOf(uint256 tokenId) internal view virtual override returns (address) {
address owner = super._ownerOf(tokenId);
// If token is owned by the core, or beyond consecutive range, return base value
if (owner != address(0) || tokenId > (uint256(type(uint96).max) + _firstConsecutiveId()) || tokenId < _firstConsecutiveId()) {
return owner;
}
// Otherwise, check the token was not burned, and fetch ownership from the anchors
// Note: no need for safe cast, we know that tokenId <= type(uint96).max
return _sequentialBurn.get(tokenId) ? address(0) : address(_sequentialOwnership.lowerLookup(uint96(tokenId)));
}
function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
_requireMinted(tokenId);
string memory baseURI = _baseURI();
return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, tokenId.toString())) : "";
} for some reason calling function _exists(uint256 tokenId) internal view virtual returns (bool) {
return _ownerOf(tokenId) != address(0);
} ------------ edit This is how we solved it /**
* @dev Reverts if the tokenId has not been minted yet.
*/
function _requireMinted(uint256 tokenId) internal view override {
require(tokenId >= _firstConsecutiveId() && tokenId < _nextTokenId, "ERC721: invalid token ID");
} |
Many of our contracts start with token id 1, ERC721Consecutive currently assumes the starting token id to be zero.
Add a constructor which allows setting the starting token id.
PR Checklist
npx changeset add
)