Skip to content
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

Add ERC721AQueryable #224

Merged
merged 8 commits into from
Apr 10, 2022
Merged

Conversation

Vectorized
Copy link
Collaborator

@Vectorized Vectorized commented Apr 2, 2022

This is made to continue from #114.

The main concern with adding a tokensOfOwner(owner) function is that if the collection grows too big, the method can fail for off-chain calls (at the moment of writing, Infura has a limit of 30 times of block gas limit).

This PR adds a tokensOfOwnerIn(owner, start, stop) function to allow querying token IDs in a certain range. This effectively allows all token IDs to be queried, regardless of how big the collection gets.

You can make a loop to call the function on your frontend / backend, as long as it stays below Infura's rate limits.

As such, the ERC721ALowCap extension will be generalizable to collections of all sizes, and should be renamed to ERC721AQueries.

Additional functions include rawOwnershipOf and rawOwnershipsOf, which are useful for various purposes
(imo, if the queries extension can only query tokensOfOwner, it feels kind of empty).

I've used different function names for these public/external functions instead of function overloading.
This makes it easier to use in web3 calls.

I highly recommend this, so that people won't need to copy and paste code (which can introduce errors).

@ahbanavi
Copy link
Contributor

ahbanavi commented Apr 3, 2022

WDYT about adding a with no minted tokens context in tests?
To be sure that the for loops don't introduce bugs if so.

@Austinhs
Copy link
Contributor

Austinhs commented Apr 5, 2022

I think it's important to have

                // All tokens have been found, we don't need to keep searching
                if (tokenIdsIdx == holdingAmount) {
                    break;
                }

For whatever odd reason if someone we're to call this from another contract it could potentially save some gas for them.

@Vectorized
Copy link
Collaborator Author

@Austinhs It is in the boolean condition of the for loops.

@Austinhs
Copy link
Contributor

Austinhs commented Apr 7, 2022

@Vectorized 👍

Excuse me 🙇

contracts/extensions/ERC721AQueries.sol Outdated Show resolved Hide resolved
contracts/extensions/ERC721AQueries.sol Outdated Show resolved Hide resolved
contracts/extensions/ERC721AQueries.sol Outdated Show resolved Hide resolved
contracts/extensions/ERC721AQueries.sol Outdated Show resolved Hide resolved
contracts/extensions/ERC721AQueries.sol Outdated Show resolved Hide resolved
contracts/extensions/ERC721AQueries.sol Outdated Show resolved Hide resolved
contracts/extensions/ERC721AQueries.sol Outdated Show resolved Hide resolved
contracts/extensions/ERC721AQueries.sol Outdated Show resolved Hide resolved
@Vectorized Vectorized requested a review from cygaar April 7, 2022 04:31
contracts/extensions/ERC721AQueries.sol Outdated Show resolved Hide resolved
contracts/extensions/ERC721AQueries.sol Outdated Show resolved Hide resolved
@Vectorized Vectorized requested a review from cygaar April 8, 2022 05:10
tokenIdsMaxLength = windowLength;
}
} else {
tokenIdsMaxLength = 0;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it'd be better to check if start >= stop at the top of the function and throw an error if it's true. These are invalid parameters so it's better to error out than return 0 semantically

Copy link
Collaborator Author

@Vectorized Vectorized Apr 8, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am leaning on the side of not reverting and just returning an empty array,
to make it easier for querying and avoid having an additional custom error.

Even if we early revert at the top, the rest of the function still needs to stay the same to pass the tests.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with @Vectorized, Most of these features are for front-end queries anyways, it would be nice to be returned an empty array to handle instead of an error IMO

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be easier to handle on the frontend if you gracefully handle the error, but it doesn't mean it's the right way to do this. If you look at most of the functions in OZ's contracts, they all error out whenever you pass in invalid input parameters.

@Vectorized why do you need to keep the rest of the function the same? You will definitely have to change some tests if you make this change, I wouldn't expect the existing tests to pass.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I remove the checks and set to zero after the clamping, the function can go into a near infinite loop.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the exact scenario you're thinking of? I don't see why we still need the if-statement on line 88. Line 93 can never evaluate to true

Copy link
Collaborator Author

@Vectorized Vectorized Apr 9, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suppose a collection with _currentIndex = 8, and we pass in start = 10 and stop = 11.

After setting start to max(_startTokenId(), start), and
stop to min(_currentIndex, stop),
start will remain 10, and stop will be 8.

This will cause the search loop to be stuck, because we didn't early return.

Alternatives:

  • Use (start < stop) instead of (start != stop) in the for loop. Each iteration costs more. Even though this is an external view function, a few gas multiplied by several thousand is quite a lot.
  • Set start to clamp(start, _startTokenId(), _currentIndex) and stop to clamp(stop, _startTokenId(), _currentIndex). More opcode needed.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about doing if (start >= stop || start >= stopLimit || stop <= _startTokenId) revert InvalidQueryRange(); after line 80? Basically, do the input validation after you know the start index and stop limit.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That will cause it to revert for empty collections, which is not desired.

* - `startTimestamp` = `<Timestamp of start of ownership>`
* - `burned = `false`
*/
function rawOwnershipOf(uint256 tokenId) public view returns (TokenOwnership memory) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be better named as "explicitOwnershipOf"

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

@Vectorized Vectorized requested a review from cygaar April 8, 2022 07:44
@Vectorized Vectorized mentioned this pull request Apr 8, 2022
* @title ERC721A Queries
* @dev ERC721A subclass with convenience query functions.
*/
abstract contract ERC721AQueries is ERC721A {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's name this ERC721AQueryable

tokenIdsMaxLength = windowLength;
}
} else {
tokenIdsMaxLength = 0;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be easier to handle on the frontend if you gracefully handle the error, but it doesn't mean it's the right way to do this. If you look at most of the functions in OZ's contracts, they all error out whenever you pass in invalid input parameters.

@Vectorized why do you need to keep the rest of the function the same? You will definitely have to change some tests if you make this change, I wouldn't expect the existing tests to pass.

@cygaar cygaar changed the title Feature/queries Add ERC721AQueryable Apr 8, 2022
@Vectorized Vectorized requested a review from cygaar April 8, 2022 21:26
@Vectorized Vectorized merged commit ec73ed8 into chiru-labs:main Apr 10, 2022
@Vectorized Vectorized deleted the feature/queries branch April 12, 2022 13:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants