diff --git a/audits/ConsenSysDiligence/2022-01-graph-pr527-audit.pdf b/audits/ConsenSysDiligence/2022-01-graph-pr527-audit.pdf new file mode 100644 index 000000000..781a35017 Binary files /dev/null and b/audits/ConsenSysDiligence/2022-01-graph-pr527-audit.pdf differ diff --git a/contracts/base/ISubgraphNFTDescriptor.sol b/contracts/base/ISubgraphNFTDescriptor.sol deleted file mode 100644 index fc7e2fecc..000000000 --- a/contracts/base/ISubgraphNFTDescriptor.sol +++ /dev/null @@ -1,15 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -pragma solidity ^0.7.6; - -import "../discovery/IGNS.sol"; - -/// @title Describes subgraph NFT tokens via URI -interface ISubgraphNFTDescriptor { - /// @notice Produces the URI describing a particular token ID for a Subgraph - /// @dev Note this URI may be a data: URI with the JSON contents directly inlined - /// @param _gns GNS contract that holds the Subgraph data - /// @param _subgraphID The ID of the subgraph NFT for which to produce a description, which may not be valid - /// @return The URI of the ERC721-compliant metadata - function tokenURI(IGNS _gns, uint256 _subgraphID) external view returns (string memory); -} diff --git a/contracts/base/SubgraphNFT.sol b/contracts/base/SubgraphNFT.sol deleted file mode 100644 index b1e2a94f9..000000000 --- a/contracts/base/SubgraphNFT.sol +++ /dev/null @@ -1,38 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -pragma solidity ^0.7.6; - -import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol"; - -import "./ISubgraphNFTDescriptor.sol"; - -abstract contract SubgraphNFT is ERC721Upgradeable { - ISubgraphNFTDescriptor public tokenDescriptor; - - // -- Events -- - - event TokenDescriptorUpdated(address tokenDescriptor); - - // -- Functions -- - - /** - * @dev Initializes the contract by setting a `name`, `symbol` and `descriptor` to the token collection. - */ - function __SubgraphNFT_init(address _tokenDescriptor) internal initializer { - __ERC721_init("Subgraph", "SG"); - _setTokenDescriptor(address(_tokenDescriptor)); - } - - /** - * @dev Set the token descriptor contract used to create the ERC-721 metadata URI - * @param _tokenDescriptor Address of the contract that creates the NFT token URI - */ - function _setTokenDescriptor(address _tokenDescriptor) internal { - require( - _tokenDescriptor != address(0) && AddressUpgradeable.isContract(_tokenDescriptor), - "NFT: Invalid token descriptor" - ); - tokenDescriptor = ISubgraphNFTDescriptor(_tokenDescriptor); - emit TokenDescriptorUpdated(_tokenDescriptor); - } -} diff --git a/contracts/base/SubgraphNFTDescriptor.sol b/contracts/base/SubgraphNFTDescriptor.sol deleted file mode 100644 index 6809ccc03..000000000 --- a/contracts/base/SubgraphNFTDescriptor.sol +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -pragma solidity ^0.7.6; - -import "./ISubgraphNFTDescriptor.sol"; - -/// @title Describes subgraph NFT tokens via URI -contract SubgraphNFTDescriptor is ISubgraphNFTDescriptor { - /// @inheritdoc ISubgraphNFTDescriptor - function tokenURI(IGNS _gns, uint256 _subgraphID) - external - view - override - returns (string memory) - { - // TODO: fancy implementation - // uint256 signal = _gns.subgraphSignal(_subgraphID); - // uint256 tokens = _gns.subgraphTokens(_subgraphID); - // id - // owner - return ""; - } -} diff --git a/contracts/discovery/GNS.sol b/contracts/discovery/GNS.sol index 12a047210..3f0e16f6d 100644 --- a/contracts/discovery/GNS.sol +++ b/contracts/discovery/GNS.sol @@ -4,9 +4,9 @@ pragma solidity ^0.7.6; pragma abicoder v2; import "@openzeppelin/contracts/math/SafeMath.sol"; +import "@openzeppelin/contracts/utils/Address.sol"; import "../base/Multicall.sol"; -import "../base/SubgraphNFT.sol"; import "../bancor/BancorFormula.sol"; import "../upgrades/GraphUpgradeable.sol"; import "../utils/TokenUtils.sol"; @@ -38,6 +38,8 @@ contract GNS is GNSV2Storage, GraphUpgradeable, IGNS, Multicall { // -- Events -- + event SubgraphNFTUpdated(address subgraphNFT); + /** * @dev Emitted when graph account sets its default name */ @@ -143,16 +145,16 @@ contract GNS is GNSV2Storage, GraphUpgradeable, IGNS, Multicall { function initialize( address _controller, address _bondingCurve, - address _tokenDescriptor + address _subgraphNFT ) external onlyImpl { Managed._initialize(_controller); // Dependencies bondingCurve = _bondingCurve; - __SubgraphNFT_init(_tokenDescriptor); // Settings _setOwnerTaxPercentage(500000); + _setSubgraphNFT(_subgraphNFT); } /** @@ -162,6 +164,8 @@ contract GNS is GNSV2Storage, GraphUpgradeable, IGNS, Multicall { graphToken().approve(address(curation()), MAX_UINT256); } + // -- Config -- + /** * @dev Set the owner fee percentage. This is used to prevent a subgraph owner to drain all * the name curators tokens while upgrading or deprecating and is configurable in parts per million. @@ -171,14 +175,6 @@ contract GNS is GNSV2Storage, GraphUpgradeable, IGNS, Multicall { _setOwnerTaxPercentage(_ownerTaxPercentage); } - /** - * @dev Set the token descriptor contract. - * @param _tokenDescriptor Address of the contract that creates the NFT token URI - */ - function setTokenDescriptor(address _tokenDescriptor) external override onlyGovernor { - _setTokenDescriptor(_tokenDescriptor); - } - /** * @dev Internal: Set the owner tax percentage. This is used to prevent a subgraph owner to drain all * the name curators tokens while upgrading or deprecating and is configurable in parts per million. @@ -190,6 +186,32 @@ contract GNS is GNSV2Storage, GraphUpgradeable, IGNS, Multicall { emit ParameterUpdated("ownerTaxPercentage"); } + /** + * @dev Set the NFT registry contract + * NOTE: Calling this function will break the ownership model unless + * it is replaced with a fully migrated version of the NFT contract state + * Use with care. + * @param _subgraphNFT Address of the ERC721 contract + */ + function setSubgraphNFT(address _subgraphNFT) public onlyGovernor { + _setSubgraphNFT(_subgraphNFT); + } + + /** + * @dev Internal: Set the NFT registry contract + * @param _subgraphNFT Address of the ERC721 contract + */ + function _setSubgraphNFT(address _subgraphNFT) private { + require( + _subgraphNFT != address(0) && Address.isContract(_subgraphNFT), + "NFT must be valid" + ); + subgraphNFT = ISubgraphNFT(_subgraphNFT); + emit SubgraphNFTUpdated(_subgraphNFT); + } + + // -- Actions -- + /** * @dev Allows a graph account to set a default name * @param _graphAccount Account that is setting its name @@ -217,7 +239,7 @@ contract GNS is GNSV2Storage, GraphUpgradeable, IGNS, Multicall { override onlySubgraphAuth(_subgraphID) { - emit SubgraphMetadataUpdated(_subgraphID, _subgraphMetadata); + _setSubgraphMetadata(_subgraphID, _subgraphMetadata); } /** @@ -241,12 +263,14 @@ contract GNS is GNSV2Storage, GraphUpgradeable, IGNS, Multicall { subgraphData.subgraphDeploymentID = _subgraphDeploymentID; subgraphData.reserveRatio = defaultReserveRatio; - // Mint the NFT. Use the subgraphID as tokenId. - // This function will check the if tokenId already exists. - _mint(subgraphOwner, subgraphID); - + // Mint the NFT. Use the subgraphID as tokenID. + // This function will check the if tokenID already exists. + _mintNFT(subgraphOwner, subgraphID); emit SubgraphPublished(subgraphID, _subgraphDeploymentID, defaultReserveRatio); - emit SubgraphMetadataUpdated(subgraphID, _subgraphMetadata); + + // Set the token metadata + _setSubgraphMetadata(subgraphID, _subgraphMetadata); + emit SubgraphVersionUpdated(subgraphID, _subgraphDeploymentID, _versionMetadata); } @@ -356,7 +380,7 @@ contract GNS is GNSV2Storage, GraphUpgradeable, IGNS, Multicall { // subgraphData.subgraphDeploymentID = 0; // Burn the NFT - _burn(_subgraphID); + _burnNFT(_subgraphID); emit SubgraphDeprecated(_subgraphID, subgraphData.withdrawableGRT); } @@ -637,21 +661,17 @@ contract GNS is GNSV2Storage, GraphUpgradeable, IGNS, Multicall { return 0; } - /** - * @dev Return the URI describing a particular token ID for a Subgraph. - * @param _subgraphID Subgraph ID - * @return The URI of the ERC721-compliant metadata - */ - function tokenURI(uint256 _subgraphID) public view override returns (string memory) { - return tokenDescriptor.tokenURI(this, _subgraphID); - } - /** * @dev Create subgraphID for legacy subgraph and mint ownership NFT. * @param _graphAccount Account that created the subgraph * @param _subgraphNumber The sequence number of the created subgraph + * @param _subgraphMetadata IPFS hash for the subgraph metadata */ - function migrateLegacySubgraph(address _graphAccount, uint256 _subgraphNumber) external { + function migrateLegacySubgraph( + address _graphAccount, + uint256 _subgraphNumber, + bytes32 _subgraphMetadata + ) external { // Must be an existing legacy subgraph bool legacySubgraphExists = legacySubgraphData[_graphAccount][_subgraphNumber] .subgraphDeploymentID != 0; @@ -675,9 +695,11 @@ contract GNS is GNSV2Storage, GraphUpgradeable, IGNS, Multicall { // Mint the NFT and send to owner // The subgraph owner is the graph account that created it - _mint(_graphAccount, subgraphID); - + _mintNFT(_graphAccount, subgraphID); emit LegacySubgraphClaimed(_graphAccount, _subgraphNumber); + + // Set the token metadata + _setSubgraphMetadata(subgraphID, _subgraphMetadata); } /** @@ -757,4 +779,45 @@ contract GNS is GNSV2Storage, GraphUpgradeable, IGNS, Multicall { require(_isPublished(subgraphData) == true, "GNS: Must be active"); return subgraphData; } + + // -- NFT -- + + /** + * @dev Return the owner of a subgraph. + * @param _tokenID Subgraph ID + * @return Owner address + */ + function ownerOf(uint256 _tokenID) public view override returns (address) { + return subgraphNFT.ownerOf(_tokenID); + } + + /** + * @dev Mint the NFT for the subgraph. + * @param _owner Owner address + * @param _tokenID Subgraph ID + */ + function _mintNFT(address _owner, uint256 _tokenID) internal { + subgraphNFT.mint(_owner, _tokenID); + } + + /** + * @dev Burn the NFT for the subgraph. + * @param _tokenID Subgraph ID + */ + function _burnNFT(uint256 _tokenID) internal { + subgraphNFT.burn(_tokenID); + } + + /** + * @dev Set the subgraph metadata. + * @param _tokenID Subgraph ID + * @param _subgraphMetadata IPFS hash of the subgraph metadata + */ + function _setSubgraphMetadata(uint256 _tokenID, bytes32 _subgraphMetadata) internal { + subgraphNFT.setSubgraphMetadata(_tokenID, _subgraphMetadata); + + // Even if the following event is emitted in the NFT we emit it here to facilitate + // subgraph indexing + emit SubgraphMetadataUpdated(_tokenID, _subgraphMetadata); + } } diff --git a/contracts/discovery/GNSStorage.sol b/contracts/discovery/GNSStorage.sol index ee5973719..50a480777 100644 --- a/contracts/discovery/GNSStorage.sol +++ b/contracts/discovery/GNSStorage.sol @@ -3,11 +3,11 @@ pragma solidity ^0.7.6; pragma abicoder v2; -import "../base/SubgraphNFT.sol"; import "../governance/Managed.sol"; import "./erc1056/IEthereumDIDRegistry.sol"; import "./IGNS.sol"; +import "./ISubgraphNFT.sol"; abstract contract GNSV1Storage is Managed { // -- State -- @@ -37,7 +37,7 @@ abstract contract GNSV1Storage is Managed { IEthereumDIDRegistry private __DEPRECATED_erc1056Registry; } -abstract contract GNSV2Storage is GNSV1Storage, SubgraphNFT { +abstract contract GNSV2Storage is GNSV1Storage { // Use it whenever a legacy (v1) subgraph NFT was claimed to maintain compatibility // Keep a reference from subgraphID => (graphAccount, subgraphNumber) mapping(uint256 => IGNS.LegacySubgraphKey) public legacySubgraphKeys; @@ -45,4 +45,7 @@ abstract contract GNSV2Storage is GNSV1Storage, SubgraphNFT { // Store data for all NFT-based (v2) subgraphs // subgraphID => SubgraphData mapping(uint256 => IGNS.SubgraphData) public subgraphs; + + // Contract that represents subgraph ownership through an NFT + ISubgraphNFT public subgraphNFT; } diff --git a/contracts/discovery/IGNS.sol b/contracts/discovery/IGNS.sol index 02926bb11..92300627e 100644 --- a/contracts/discovery/IGNS.sol +++ b/contracts/discovery/IGNS.sol @@ -26,8 +26,6 @@ interface IGNS { function setOwnerTaxPercentage(uint32 _ownerTaxPercentage) external; - function setTokenDescriptor(address _tokenDescriptor) external; - // -- Publishing -- function setDefaultName( @@ -71,6 +69,8 @@ interface IGNS { // -- Getters -- + function ownerOf(uint256 _tokenID) external view returns (address); + function subgraphSignal(uint256 _subgraphID) external view returns (uint256); function subgraphTokens(uint256 _subgraphID) external view returns (uint256); diff --git a/contracts/discovery/ISubgraphNFT.sol b/contracts/discovery/ISubgraphNFT.sol new file mode 100644 index 000000000..4b0495a28 --- /dev/null +++ b/contracts/discovery/ISubgraphNFT.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; + +import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; + +interface ISubgraphNFT is IERC721 { + // -- Config -- + + function setMinter(address _minter) external; + + function setTokenDescriptor(address _tokenDescriptor) external; + + function setBaseURI(string memory _baseURI) external; + + // -- Actions -- + + function mint(address _to, uint256 _tokenId) external; + + function burn(uint256 _tokenId) external; + + function setSubgraphMetadata(uint256 _tokenId, bytes32 _subgraphMetadata) external; + + function tokenURI(uint256 _tokenId) external view returns (string memory); +} diff --git a/contracts/discovery/ISubgraphNFTDescriptor.sol b/contracts/discovery/ISubgraphNFTDescriptor.sol new file mode 100644 index 000000000..cd0785dcb --- /dev/null +++ b/contracts/discovery/ISubgraphNFTDescriptor.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; + +/// @title Describes subgraph NFT tokens via URI +interface ISubgraphNFTDescriptor { + /// @notice Produces the URI describing a particular token ID for a Subgraph + /// @dev Note this URI may be data: URI with the JSON contents directly inlined + /// @param _minter Address of the allowed minter + /// @param _tokenId The ID of the subgraph NFT for which to produce a description, which may not be valid + /// @param _baseURI The base URI that could be prefixed to the final URI + /// @param _subgraphMetadata Subgraph metadata set for the subgraph + /// @return The URI of the ERC721-compliant metadata + function tokenURI( + address _minter, + uint256 _tokenId, + string calldata _baseURI, + bytes32 _subgraphMetadata + ) external view returns (string memory); +} diff --git a/contracts/discovery/SubgraphNFT.sol b/contracts/discovery/SubgraphNFT.sol new file mode 100644 index 000000000..c6dadaa81 --- /dev/null +++ b/contracts/discovery/SubgraphNFT.sol @@ -0,0 +1,167 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; + +import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import "@openzeppelin/contracts/utils/Address.sol"; + +import "../governance/Governed.sol"; +import "../libraries/HexStrings.sol"; +import "./ISubgraphNFT.sol"; +import "./ISubgraphNFTDescriptor.sol"; + +/// @title NFT that represents ownership of a Subgraph +contract SubgraphNFT is Governed, ERC721, ISubgraphNFT { + // -- State -- + + address public minter; + ISubgraphNFTDescriptor public tokenDescriptor; + mapping(uint256 => bytes32) private _subgraphMetadataHashes; + + // -- Events -- + + event MinterUpdated(address minter); + event TokenDescriptorUpdated(address tokenDescriptor); + event SubgraphMetadataUpdated(uint256 indexed tokenID, bytes32 subgraphURI); + + // -- Modifiers -- + + modifier onlyMinter() { + require(msg.sender == minter, "Must be a minter"); + _; + } + + constructor(address _governor) ERC721("Subgraph", "SG") { + _initialize(_governor); + } + + // -- Config -- + + /** + * @notice Set the minter allowed to perform actions on the NFT. + * @dev Minter can mint, burn and update the metadata + * @param _minter Address of the allowed minter + */ + function setMinter(address _minter) external override onlyGovernor { + _setMinter(_minter); + } + + /** + * @notice Internal: Set the minter allowed to perform actions on the NFT. + * @dev Minter can mint, burn and update the metadata. Can be set to zero. + * @param _minter Address of the allowed minter + */ + function _setMinter(address _minter) internal { + minter = _minter; + emit MinterUpdated(_minter); + } + + /** + * @notice Set the token descriptor contract. + * @dev Token descriptor can be zero. If set, it must be a contract. + * @param _tokenDescriptor Address of the contract that creates the NFT token URI + */ + function setTokenDescriptor(address _tokenDescriptor) external override onlyGovernor { + _setTokenDescriptor(_tokenDescriptor); + } + + /** + * @dev Internal: Set the token descriptor contract used to create the ERC-721 metadata URI. + * @param _tokenDescriptor Address of the contract that creates the NFT token URI + */ + function _setTokenDescriptor(address _tokenDescriptor) internal { + require( + _tokenDescriptor == address(0) || Address.isContract(_tokenDescriptor), + "NFT: Invalid token descriptor" + ); + tokenDescriptor = ISubgraphNFTDescriptor(_tokenDescriptor); + emit TokenDescriptorUpdated(_tokenDescriptor); + } + + /** + * @notice Set the base URI. + * @dev Can be set to empty. + * @param _baseURI Base URI to use to build the token URI + */ + function setBaseURI(string memory _baseURI) external override onlyGovernor { + _setBaseURI(_baseURI); + } + + // -- Minter actions -- + + /** + * @notice Mint `_tokenId` and transfers it to `_to`. + * @dev `tokenId` must not exist and `to` cannot be the zero address. + * @param _to Address receiving the minted NFT + * @param _tokenId ID of the NFT + */ + function mint(address _to, uint256 _tokenId) external override onlyMinter { + _mint(_to, _tokenId); + } + + /** + * @notice Burn `_tokenId`. + * @dev The approval is cleared when the token is burned. + * @param _tokenId ID of the NFT + */ + function burn(uint256 _tokenId) external override onlyMinter { + _burn(_tokenId); + } + + /** + * @notice Set the metadata for a subgraph represented by `_tokenId`. + * @dev `_tokenId` must exist. + * @param _tokenId ID of the NFT + * @param _subgraphMetadata IPFS hash for the metadata + */ + function setSubgraphMetadata(uint256 _tokenId, bytes32 _subgraphMetadata) + external + override + onlyMinter + { + require(_exists(_tokenId), "ERC721Metadata: URI set of nonexistent token"); + _subgraphMetadataHashes[_tokenId] = _subgraphMetadata; + emit SubgraphMetadataUpdated(_tokenId, _subgraphMetadata); + } + + // -- NFT display -- + + /// @inheritdoc ERC721 + function tokenURI(uint256 _tokenId) + public + view + override(ERC721, ISubgraphNFT) + returns (string memory) + { + require(_exists(_tokenId), "ERC721Metadata: URI query for nonexistent token"); + + // Delegates rendering of the metadata to the token descriptor if existing + // This allows for some flexibility in adapting the token URI + if (address(tokenDescriptor) != address(0)) { + return + tokenDescriptor.tokenURI( + minter, + _tokenId, + baseURI(), + _subgraphMetadataHashes[_tokenId] + ); + } + + // Default token URI + uint256 metadata = uint256(_subgraphMetadataHashes[_tokenId]); + + string memory _subgraphURI = metadata > 0 ? HexStrings.toString(metadata) : ""; + string memory base = baseURI(); + + // If there is no base URI, return the token URI. + if (bytes(base).length == 0) { + return _subgraphURI; + } + // If both are set, concatenate the baseURI and tokenURI (via abi.encodePacked). + if (bytes(_subgraphURI).length > 0) { + return string(abi.encodePacked(base, _subgraphURI)); + } + // If there is a baseURI but no tokenURI, concatenate the tokenID to the baseURI. + return string(abi.encodePacked(base, HexStrings.toString(_tokenId))); + } +} diff --git a/contracts/discovery/SubgraphNFTDescriptor.sol b/contracts/discovery/SubgraphNFTDescriptor.sol new file mode 100644 index 000000000..751db2353 --- /dev/null +++ b/contracts/discovery/SubgraphNFTDescriptor.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; + +import "../libraries/Base58Encoder.sol"; +import "./ISubgraphNFTDescriptor.sol"; + +/// @title Describes subgraph NFT tokens via URI +contract SubgraphNFTDescriptor is ISubgraphNFTDescriptor { + /// @inheritdoc ISubgraphNFTDescriptor + function tokenURI( + address, /* _minter */ + uint256, /* _tokenId */ + string calldata _baseURI, + bytes32 _subgraphMetadata + ) external pure override returns (string memory) { + bytes memory b58 = Base58Encoder.encode( + abi.encodePacked(Base58Encoder.sha256MultiHash, _subgraphMetadata) + ); + if (bytes(_baseURI).length == 0) { + return string(b58); + } + return string(abi.encodePacked(_baseURI, b58)); + } +} diff --git a/contracts/libraries/Base58Encoder.sol b/contracts/libraries/Base58Encoder.sol new file mode 100644 index 000000000..9af197855 --- /dev/null +++ b/contracts/libraries/Base58Encoder.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; + +/// @title Base58Encoder +/// @author Original author - Martin Lundfall (martin.lundfall@gmail.com) +/// Based on https://github.com/MrChico/verifyIPFS +library Base58Encoder { + bytes constant sha256MultiHash = hex"1220"; + bytes constant ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; + + /// @dev Converts hex string to base 58 + function encode(bytes memory source) internal pure returns (bytes memory) { + if (source.length == 0) return new bytes(0); + uint8[] memory digits = new uint8[](64); + digits[0] = 0; + uint8 digitlength = 1; + for (uint256 i = 0; i < source.length; ++i) { + uint256 carry = uint8(source[i]); + for (uint256 j = 0; j < digitlength; ++j) { + carry += uint256(digits[j]) * 256; + digits[j] = uint8(carry % 58); + carry = carry / 58; + } + + while (carry > 0) { + digits[digitlength] = uint8(carry % 58); + digitlength++; + carry = carry / 58; + } + } + return toAlphabet(reverse(truncate(digits, digitlength))); + } + + function truncate(uint8[] memory array, uint8 length) internal pure returns (uint8[] memory) { + uint8[] memory output = new uint8[](length); + for (uint256 i = 0; i < length; i++) { + output[i] = array[i]; + } + return output; + } + + function reverse(uint8[] memory input) internal pure returns (uint8[] memory) { + uint8[] memory output = new uint8[](input.length); + for (uint256 i = 0; i < input.length; i++) { + output[i] = input[input.length - 1 - i]; + } + return output; + } + + function toAlphabet(uint8[] memory indices) internal pure returns (bytes memory) { + bytes memory output = new bytes(indices.length); + for (uint256 i = 0; i < indices.length; i++) { + output[i] = ALPHABET[indices[i]]; + } + return output; + } +} diff --git a/contracts/libraries/HexStrings.sol b/contracts/libraries/HexStrings.sol new file mode 100644 index 000000000..4842883a9 --- /dev/null +++ b/contracts/libraries/HexStrings.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; + +/// @title HexStrings +/// Based on https://github.com/OpenZeppelin/openzeppelin-contracts/blob/8dd744fc1843d285c38e54e9d439dea7f6b93495/contracts/utils/Strings.sol +library HexStrings { + bytes16 private constant _HEX_SYMBOLS = "0123456789abcdef"; + + /// @dev Converts a `uint256` to its ASCII `string` hexadecimal representation. + function toString(uint256 value) internal pure returns (string memory) { + if (value == 0) { + return "0x00"; + } + uint256 temp = value; + uint256 length = 0; + while (temp != 0) { + length++; + temp >>= 8; + } + return toHexString(value, length); + } + + /// @dev Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed length. + function toHexString(uint256 value, uint256 length) internal pure returns (string memory) { + bytes memory buffer = new bytes(2 * length + 2); + buffer[0] = "0"; + buffer[1] = "x"; + for (uint256 i = 2 * length + 1; i > 1; --i) { + buffer[i] = _HEX_SYMBOLS[value & 0xf]; + value >>= 4; + } + require(value == 0, "Strings: hex length insufficient"); + return string(buffer); + } +} diff --git a/test/gns.test.ts b/test/gns.test.ts index ad31afc4d..6494e665b 100644 --- a/test/gns.test.ts +++ b/test/gns.test.ts @@ -1,16 +1,19 @@ import { expect } from 'chai' import { ethers, ContractTransaction, BigNumber, Event } from 'ethers' import { solidityKeccak256 } from 'ethers/lib/utils' +import { SubgraphDeploymentID } from '@graphprotocol/common-ts' import { GNS } from '../build/types/GNS' import { GraphToken } from '../build/types/GraphToken' import { Curation } from '../build/types/Curation' +import { SubgraphNFT } from '../build/types/SubgraphNFT' import { getAccounts, randomHexBytes, Account, toGRT } from './lib/testHelpers' import { NetworkFixture } from './lib/fixtures' import { toBN, formatGRT } from './lib/testHelpers' +import { getContractAt } from '../cli/network' -const { AddressZero } = ethers.constants +const { AddressZero, HashZero } = ethers.constants // Entities interface PublishSubgraph { @@ -518,22 +521,22 @@ describe('GNS', () => { }) }) - describe('setTokenDescriptor', function () { - it('should set `tokenDescriptor`', async function () { - const newTokenDescriptor = gns.address // I just use any contract address - const tx = gns.connect(governor.signer).setTokenDescriptor(newTokenDescriptor) - await expect(tx).emit(gns, 'TokenDescriptorUpdated').withArgs(newTokenDescriptor) - expect(await gns.tokenDescriptor()).eq(newTokenDescriptor) + describe('setSubgraphNFT', function () { + it('should set `setSubgraphNFT`', async function () { + const newValue = gns.address // I just use any contract address + const tx = gns.connect(governor.signer).setSubgraphNFT(newValue) + await expect(tx).emit(gns, 'SubgraphNFTUpdated').withArgs(newValue) + expect(await gns.subgraphNFT()).eq(newValue) }) it('revert set to empty address', async function () { - const tx = gns.connect(governor.signer).setTokenDescriptor(AddressZero) - await expect(tx).revertedWith('NFT: Invalid token descriptor') + const tx = gns.connect(governor.signer).setSubgraphNFT(AddressZero) + await expect(tx).revertedWith('NFT must be valid') }) it('revert set to non-contract', async function () { - const tx = gns.connect(governor.signer).setTokenDescriptor(randomHexBytes(20)) - await expect(tx).revertedWith('NFT: Invalid token descriptor') + const tx = gns.connect(governor.signer).setSubgraphNFT(randomHexBytes(20)) + await expect(tx).revertedWith('NFT must be valid') }) }) }) @@ -604,11 +607,7 @@ describe('GNS', () => { it('should prevent subgraphDeploymentID of 0 to be used', async function () { const tx = gns .connect(me.signer) - .publishNewSubgraph( - ethers.constants.HashZero, - newSubgraph0.versionMetadata, - newSubgraph0.subgraphMetadata, - ) + .publishNewSubgraph(HashZero, newSubgraph0.versionMetadata, newSubgraph0.subgraphMetadata) await expect(tx).revertedWith('GNS: Cannot set deploymentID to 0 in publish') }) }) @@ -1006,4 +1005,67 @@ describe('GNS', () => { await expect(tx).revertedWith('') }) }) + + describe('NFT descriptor', function () { + it('with token descriptor', async function () { + const subgraph0 = await publishNewSubgraph(me, newSubgraph0) + + const subgraphNFTAddress = await gns.subgraphNFT() + const subgraphNFT = getContractAt('SubgraphNFT', subgraphNFTAddress) as SubgraphNFT + const tokenURI = await subgraphNFT.connect(me.signer).tokenURI(subgraph0.id) + + const sub = new SubgraphDeploymentID(newSubgraph0.subgraphMetadata) + expect(sub.ipfsHash).eq(tokenURI) + }) + + it('with token descriptor and baseURI', async function () { + const subgraph0 = await publishNewSubgraph(me, newSubgraph0) + + const subgraphNFTAddress = await gns.subgraphNFT() + const subgraphNFT = getContractAt('SubgraphNFT', subgraphNFTAddress) as SubgraphNFT + await subgraphNFT.connect(governor.signer).setBaseURI('ipfs://') + const tokenURI = await subgraphNFT.connect(me.signer).tokenURI(subgraph0.id) + + const sub = new SubgraphDeploymentID(newSubgraph0.subgraphMetadata) + expect('ipfs://' + sub.ipfsHash).eq(tokenURI) + }) + + it('without token descriptor', async function () { + const subgraph0 = await publishNewSubgraph(me, newSubgraph0) + + const subgraphNFTAddress = await gns.subgraphNFT() + const subgraphNFT = getContractAt('SubgraphNFT', subgraphNFTAddress) as SubgraphNFT + await subgraphNFT.connect(governor.signer).setTokenDescriptor(AddressZero) + const tokenURI = await subgraphNFT.connect(me.signer).tokenURI(subgraph0.id) + + const sub = new SubgraphDeploymentID(newSubgraph0.subgraphMetadata) + expect(sub.bytes32).eq(tokenURI) + }) + + it('without token descriptor and baseURI', async function () { + const subgraph0 = await publishNewSubgraph(me, newSubgraph0) + + const subgraphNFTAddress = await gns.subgraphNFT() + const subgraphNFT = getContractAt('SubgraphNFT', subgraphNFTAddress) as SubgraphNFT + await subgraphNFT.connect(governor.signer).setTokenDescriptor(AddressZero) + await subgraphNFT.connect(governor.signer).setBaseURI('ipfs://') + const tokenURI = await subgraphNFT.connect(me.signer).tokenURI(subgraph0.id) + + const sub = new SubgraphDeploymentID(newSubgraph0.subgraphMetadata) + expect('ipfs://' + sub.bytes32).eq(tokenURI) + }) + + it('without token descriptor and 0x0 metadata', async function () { + const newSubgraphNoMetadata = buildSubgraph() + newSubgraphNoMetadata.subgraphMetadata = HashZero + const subgraph0 = await publishNewSubgraph(me, newSubgraphNoMetadata) + + const subgraphNFTAddress = await gns.subgraphNFT() + const subgraphNFT = getContractAt('SubgraphNFT', subgraphNFTAddress) as SubgraphNFT + await subgraphNFT.connect(governor.signer).setTokenDescriptor(AddressZero) + await subgraphNFT.connect(governor.signer).setBaseURI('ipfs://') + const tokenURI = await subgraphNFT.connect(me.signer).tokenURI(subgraph0.id) + expect('ipfs://' + subgraph0.id).eq(tokenURI) + }) + }) }) diff --git a/test/lib/deployment.ts b/test/lib/deployment.ts index 75b1b80df..9dac30b7d 100644 --- a/test/lib/deployment.ts +++ b/test/lib/deployment.ts @@ -20,6 +20,7 @@ import { EthereumDIDRegistry } from '../../build/types/EthereumDIDRegistry' import { GDAI } from '../../build/types/GDAI' import { GSRManager } from '../../build/types/GSRManager' import { GraphGovernance } from '../../build/types/GraphGovernance' +import { SubgraphNFT } from '../../build/types/SubgraphNFT' // Disable logging for tests logger.pause() @@ -178,14 +179,25 @@ export async function deployGNS( // Dependency const bondingCurve = (await deployContract('BancorFormula', deployer)) as unknown as BancorFormula const subgraphDescriptor = await deployContract('SubgraphNFTDescriptor', deployer) + const subgraphNFT = (await deployContract( + 'SubgraphNFT', + deployer, + await deployer.getAddress(), + )) as SubgraphNFT // Deploy - return network.deployContractWithProxy( + const proxy = (await network.deployContractWithProxy( proxyAdmin, 'GNS', - [controller, bondingCurve.address, subgraphDescriptor.address], + [controller, bondingCurve.address, subgraphNFT.address], deployer, - ) as unknown as GNS + )) as unknown as GNS + + // Post-config + await subgraphNFT.connect(deployer).setMinter(proxy.address) + await subgraphNFT.connect(deployer).setTokenDescriptor(subgraphDescriptor.address) + + return proxy } export async function deployEthereumDIDRegistry(deployer: Signer): Promise { diff --git a/tsconfig.json b/tsconfig.json index 1857a87d3..519ded40c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,9 +1,9 @@ { "compilerOptions": { - "lib": ["ES2018", "dom"], + "lib": ["ES2020", "dom"], "module": "commonjs", "moduleResolution": "node", - "target": "ES2018", + "target": "ES2020", "outDir": "dist", "resolveJsonModule": true, "esModuleInterop": true