From 8da40903c737ccee0caf65aa78beacf352a2a079 Mon Sep 17 00:00:00 2001
From: WhiteOakKong <92236155+WhiteOakKong@users.noreply.github.com>
Date: Mon, 20 Nov 2023 12:54:30 -0500
Subject: [PATCH] add queryable extension to ERC721Base (#560)
* add queryable to erc721base
* clean/lint
* uncomment natspec
* use erc721Avirtualapprove
---------
Signed-off-by: WhiteOakKong <92236155+WhiteOakKong@users.noreply.github.com>
Co-authored-by: nkrishang <62195808+nkrishang@users.noreply.github.com>
---
contracts/base/ERC721Base.sol | 6 +-
contracts/eip/queryable/ERC721AQueryable.sol | 168 ++++++++++++++++++
contracts/eip/queryable/IERC721AQueryable.sol | 73 ++++++++
3 files changed, 244 insertions(+), 3 deletions(-)
create mode 100644 contracts/eip/queryable/ERC721AQueryable.sol
create mode 100644 contracts/eip/queryable/IERC721AQueryable.sol
diff --git a/contracts/base/ERC721Base.sol b/contracts/base/ERC721Base.sol
index bd6123979..5bde74392 100644
--- a/contracts/base/ERC721Base.sol
+++ b/contracts/base/ERC721Base.sol
@@ -3,7 +3,7 @@ pragma solidity ^0.8.0;
/// @author thirdweb
-import { ERC721A } from "../eip/ERC721AVirtualApprove.sol";
+import "../eip/queryable/ERC721AQueryable.sol";
import "../extension/ContractMetadata.sol";
import "../extension/Multicall.sol";
@@ -30,7 +30,7 @@ import "../lib/TWStrings.sol";
* - EIP 2981 compliance for royalty support on NFT marketplaces.
*/
-contract ERC721Base is ERC721A, ContractMetadata, Multicall, Ownable, Royalty, BatchMintMetadata {
+contract ERC721Base is ERC721AQueryable, ContractMetadata, Multicall, Ownable, Royalty, BatchMintMetadata {
using TWStrings for uint256;
/*//////////////////////////////////////////////////////////////
@@ -89,7 +89,7 @@ contract ERC721Base is ERC721A, ContractMetadata, Multicall, Ownable, Royalty, B
*
* @param _tokenId The tokenId of an NFT.
*/
- function tokenURI(uint256 _tokenId) public view virtual override returns (string memory) {
+ function tokenURI(uint256 _tokenId) public view virtual override(ERC721A, IERC721Metadata) returns (string memory) {
string memory fullUriForToken = fullURI[_tokenId];
if (bytes(fullUriForToken).length > 0) {
return fullUriForToken;
diff --git a/contracts/eip/queryable/ERC721AQueryable.sol b/contracts/eip/queryable/ERC721AQueryable.sol
new file mode 100644
index 000000000..a5026b487
--- /dev/null
+++ b/contracts/eip/queryable/ERC721AQueryable.sol
@@ -0,0 +1,168 @@
+// SPDX-License-Identifier: MIT
+// ERC721A Contracts v3.3.0
+// Creator: Chiru Labs
+
+pragma solidity ^0.8.4;
+
+import "./IERC721AQueryable.sol";
+import "../ERC721AVirtualApprove.sol";
+
+/**
+ * @title ERC721A Queryable
+ * @dev ERC721A subclass with convenience query functions.
+ */
+abstract contract ERC721AQueryable is ERC721A, IERC721AQueryable {
+ /**
+ * @dev Returns the `TokenOwnership` struct at `tokenId` without reverting.
+ *
+ * If the `tokenId` is out of bounds:
+ * - `addr` = `address(0)`
+ * - `startTimestamp` = `0`
+ * - `burned` = `false`
+ *
+ * If the `tokenId` is burned:
+ * - `addr` = `
`
+ * - `startTimestamp` = ``
+ * - `burned = `true`
+ *
+ * Otherwise:
+ * - `addr` = ``
+ * - `startTimestamp` = ``
+ * - `burned = `false`
+ */
+ function explicitOwnershipOf(uint256 tokenId) public view override returns (TokenOwnership memory) {
+ TokenOwnership memory ownership;
+ if (tokenId < _startTokenId() || tokenId >= _currentIndex) {
+ return ownership;
+ }
+ ownership = _ownerships[tokenId];
+ if (ownership.burned) {
+ return ownership;
+ }
+ return _ownershipOf(tokenId);
+ }
+
+ /**
+ * @dev Returns an array of `TokenOwnership` structs at `tokenIds` in order.
+ * See {ERC721AQueryable-explicitOwnershipOf}
+ */
+ function explicitOwnershipsOf(uint256[] memory tokenIds) external view override returns (TokenOwnership[] memory) {
+ unchecked {
+ uint256 tokenIdsLength = tokenIds.length;
+ TokenOwnership[] memory ownerships = new TokenOwnership[](tokenIdsLength);
+ for (uint256 i; i != tokenIdsLength; ++i) {
+ ownerships[i] = explicitOwnershipOf(tokenIds[i]);
+ }
+ return ownerships;
+ }
+ }
+
+ /**
+ * @dev Returns an array of token IDs owned by `owner`,
+ * in the range [`start`, `stop`)
+ * (i.e. `start <= tokenId < stop`).
+ *
+ * This function allows for tokens to be queried if the collection
+ * grows too big for a single call of {ERC721AQueryable-tokensOfOwner}.
+ *
+ * Requirements:
+ *
+ * - `start` < `stop`
+ */
+ /* solhint-disable*/
+ function tokensOfOwnerIn(
+ address owner,
+ uint256 start,
+ uint256 stop
+ ) external view override returns (uint256[] memory) {
+ unchecked {
+ if (start >= stop) revert InvalidQueryRange();
+ uint256 tokenIdsIdx;
+ uint256 stopLimit = _currentIndex;
+ // Set `start = max(start, _startTokenId())`.
+ if (start < _startTokenId()) {
+ start = _startTokenId();
+ }
+ // Set `stop = min(stop, _currentIndex)`.
+ if (stop > stopLimit) {
+ stop = stopLimit;
+ }
+ uint256 tokenIdsMaxLength = balanceOf(owner);
+ // Set `tokenIdsMaxLength = min(balanceOf(owner), stop - start)`,
+ // to cater for cases where `balanceOf(owner)` is too big.
+ if (start < stop) {
+ uint256 rangeLength = stop - start;
+ if (rangeLength < tokenIdsMaxLength) {
+ tokenIdsMaxLength = rangeLength;
+ }
+ } else {
+ tokenIdsMaxLength = 0;
+ }
+ uint256[] memory tokenIds = new uint256[](tokenIdsMaxLength);
+ if (tokenIdsMaxLength == 0) {
+ return tokenIds;
+ }
+ // We need to call `explicitOwnershipOf(start)`,
+ // because the slot at `start` may not be initialized.
+ TokenOwnership memory ownership = explicitOwnershipOf(start);
+ address currOwnershipAddr;
+ // If the starting slot exists (i.e. not burned), initialize `currOwnershipAddr`.
+ // `ownership.address` will not be zero, as `start` is clamped to the valid token ID range.
+ if (!ownership.burned) {
+ currOwnershipAddr = ownership.addr;
+ }
+ for (uint256 i = start; i != stop && tokenIdsIdx != tokenIdsMaxLength; ++i) {
+ ownership = _ownerships[i];
+ if (ownership.burned) {
+ continue;
+ }
+ if (ownership.addr != address(0)) {
+ currOwnershipAddr = ownership.addr;
+ }
+ if (currOwnershipAddr == owner) {
+ tokenIds[tokenIdsIdx++] = i;
+ }
+ }
+ // Downsize the array to fit.
+ assembly {
+ mstore(tokenIds, tokenIdsIdx)
+ }
+ return tokenIds;
+ }
+ }
+
+ /* solhint-enable */
+
+ /**
+ * @dev Returns an array of token IDs owned by `owner`.
+ *
+ * This function scans the ownership mapping and is O(totalSupply) in complexity.
+ * It is meant to be called off-chain.
+ *
+ * See {ERC721AQueryable-tokensOfOwnerIn} for splitting the scan into
+ * multiple smaller scans if the collection is large enough to cause
+ * an out-of-gas error (10K pfp collections should be fine).
+ */
+ function tokensOfOwner(address owner) external view override returns (uint256[] memory) {
+ unchecked {
+ uint256 tokenIdsIdx;
+ address currOwnershipAddr;
+ uint256 tokenIdsLength = balanceOf(owner);
+ uint256[] memory tokenIds = new uint256[](tokenIdsLength);
+ TokenOwnership memory ownership;
+ for (uint256 i = _startTokenId(); tokenIdsIdx != tokenIdsLength; ++i) {
+ ownership = _ownerships[i];
+ if (ownership.burned) {
+ continue;
+ }
+ if (ownership.addr != address(0)) {
+ currOwnershipAddr = ownership.addr;
+ }
+ if (currOwnershipAddr == owner) {
+ tokenIds[tokenIdsIdx++] = i;
+ }
+ }
+ return tokenIds;
+ }
+ }
+}
diff --git a/contracts/eip/queryable/IERC721AQueryable.sol b/contracts/eip/queryable/IERC721AQueryable.sol
new file mode 100644
index 000000000..f8a22f715
--- /dev/null
+++ b/contracts/eip/queryable/IERC721AQueryable.sol
@@ -0,0 +1,73 @@
+// SPDX-License-Identifier: MIT
+// ERC721A Contracts v3.3.0
+// Creator: Chiru Labs
+
+pragma solidity ^0.8.4;
+
+import "../interface/IERC721A.sol";
+
+/**
+ * @dev Interface of an ERC721AQueryable compliant contract.
+ */
+interface IERC721AQueryable is IERC721A {
+ /**
+ * Invalid query range (`start` >= `stop`).
+ */
+ error InvalidQueryRange();
+
+ /**
+ * @dev Returns the `TokenOwnership` struct at `tokenId` without reverting.
+ *
+ * If the `tokenId` is out of bounds:
+ * - `addr` = `address(0)`
+ * - `startTimestamp` = `0`
+ * - `burned` = `false`
+ *
+ * If the `tokenId` is burned:
+ * - `addr` = ``
+ * - `startTimestamp` = ``
+ * - `burned = `true`
+ *
+ * Otherwise:
+ * - `addr` = ``
+ * - `startTimestamp` = ``
+ * - `burned = `false`
+ */
+ function explicitOwnershipOf(uint256 tokenId) external view returns (TokenOwnership memory);
+
+ /**
+ * @dev Returns an array of `TokenOwnership` structs at `tokenIds` in order.
+ * See {ERC721AQueryable-explicitOwnershipOf}
+ */
+ function explicitOwnershipsOf(uint256[] memory tokenIds) external view returns (TokenOwnership[] memory);
+
+ /**
+ * @dev Returns an array of token IDs owned by `owner`,
+ * in the range [`start`, `stop`)
+ * (i.e. `start <= tokenId < stop`).
+ *
+ * This function allows for tokens to be queried if the collection
+ * grows too big for a single call of {ERC721AQueryable-tokensOfOwner}.
+ *
+ * Requirements:
+ *
+ * - `start` < `stop`
+ */
+ function tokensOfOwnerIn(
+ address owner,
+ uint256 start,
+ uint256 stop
+ ) external view returns (uint256[] memory);
+
+ /**
+ * @dev Returns an array of token IDs owned by `owner`.
+ *
+ * This function scans the ownership mapping and is O(totalSupply) in complexity.
+ * It is meant to be called off-chain.
+ *
+ * See {ERC721AQueryable-tokensOfOwnerIn} for splitting the scan into
+ * multiple smaller scans if the collection is large enough to cause
+ * an out-of-gas error (10K pfp collections should be fine).
+ */
+ function tokensOfOwner(address owner) external view returns (uint256[] memory);
+}