From 04714b14f6b502e5021a8e5a893bb7504edd947c Mon Sep 17 00:00:00 2001 From: Matej Poklemba Date: Sat, 30 Mar 2024 19:59:28 +0100 Subject: [PATCH] PRC3 changes (#335) * Add virtual to functions * Append `eip155:chainId` in tokenURI * Add IERC721Metadata req, add tokenURI function with contract call, polish stuff --- .vscode/settings.json | 3 ++ .../contracts/token/IInverseProjectedNft.sol | 14 ++++-- .../contracts/token/ITokenUri.sol | 10 ++++ .../token/InverseAppProjectedNft.sol | 33 +++++++++---- .../token/InverseBaseProjectedNft.sol | 38 ++++++++++----- .../test/InverseAppProjectedNft.t.sol | 48 +++++++++++-------- .../test/InverseBaseProjectedNft.t.sol | 48 +++++++++++-------- 7 files changed, 130 insertions(+), 64 deletions(-) create mode 100644 packages/contracts/evm-contracts/contracts/token/ITokenUri.sol diff --git a/.vscode/settings.json b/.vscode/settings.json index 3104de24b..a1e5773eb 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,6 +7,9 @@ "[typescriptreact]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, + "[solidity]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, "files.associations": { ".env.*": "properties" }, diff --git a/packages/contracts/evm-contracts/contracts/token/IInverseProjectedNft.sol b/packages/contracts/evm-contracts/contracts/token/IInverseProjectedNft.sol index 7e913d4e9..d024d97cd 100644 --- a/packages/contracts/evm-contracts/contracts/token/IInverseProjectedNft.sol +++ b/packages/contracts/evm-contracts/contracts/token/IInverseProjectedNft.sol @@ -1,14 +1,12 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.13; -import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; -import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; +import {IERC721Metadata} from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; import {IERC4906} from "@openzeppelin/contracts/interfaces/IERC4906.sol"; +import {ITokenUri} from "./ITokenUri.sol"; /// @dev A standard ERC721 that can be burned and has a special tokenURI function accepting a custom base URI. -interface IInverseProjectedNft is IERC4906 { +interface IInverseProjectedNft is IERC4906, IERC721Metadata { /// @dev Emitted when `baseExtension` is updated from `oldBaseExtension` to `newBaseExtension`. event SetBaseExtension(string oldBaseExtension, string newBaseExtension); @@ -33,4 +31,10 @@ interface IInverseProjectedNft is IERC4906 { uint256 tokenId, string memory customBaseUri ) external view returns (string memory); + + /// @dev Returns the token URI of specified `tokenId` using a call to contract implementing `ITokenUri`. + function tokenURI( + uint256 tokenId, + ITokenUri customUriInterface + ) external view returns (string memory); } diff --git a/packages/contracts/evm-contracts/contracts/token/ITokenUri.sol b/packages/contracts/evm-contracts/contracts/token/ITokenUri.sol new file mode 100644 index 000000000..2046ffefd --- /dev/null +++ b/packages/contracts/evm-contracts/contracts/token/ITokenUri.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +/// @dev An interface exposing the `tokenURI` function from IERC721Metadata. +interface ITokenUri { + /** + * @dev Returns the Uniform Resource Identifier (URI) for `tokenId` token. + */ + function tokenURI(uint256 tokenId) external view returns (string memory); +} diff --git a/packages/contracts/evm-contracts/contracts/token/InverseAppProjectedNft.sol b/packages/contracts/evm-contracts/contracts/token/InverseAppProjectedNft.sol index 61d65cdcc..cb7f9085f 100644 --- a/packages/contracts/evm-contracts/contracts/token/InverseAppProjectedNft.sol +++ b/packages/contracts/evm-contracts/contracts/token/InverseAppProjectedNft.sol @@ -3,12 +3,12 @@ pragma solidity ^0.8.13; import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import {IERC721Metadata} from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; -import {IERC4906} from "@openzeppelin/contracts/interfaces/IERC4906.sol"; import {IInverseProjectedNft} from "./IInverseProjectedNft.sol"; import {IInverseAppProjectedNft} from "./IInverseAppProjectedNft.sol"; +import {ITokenUri} from "./ITokenUri.sol"; struct MintEntry { address minter; @@ -52,7 +52,7 @@ contract InverseAppProjectedNft is IInverseAppProjectedNft, ERC721, Ownable { /// @dev Returns true if this contract implements the interface defined by `interfaceId`. See EIP165. function supportsInterface( bytes4 interfaceId - ) public view override(IERC165, ERC721) returns (bool) { + ) public view virtual override(IERC165, ERC721) returns (bool) { return interfaceId == type(IInverseProjectedNft).interfaceId || interfaceId == type(IInverseAppProjectedNft).interfaceId || @@ -64,7 +64,7 @@ contract InverseAppProjectedNft is IInverseAppProjectedNft, ERC721, Ownable { /// Increases the `totalSupply` and `currentTokenId`. /// Reverts if `_to` is a zero address or if it refers to smart contract but does not implement IERC721Receiver-onERC721Received. /// Emits the `Minted` event. - function mint(address _to) external returns (uint256) { + function mint(address _to) public virtual returns (uint256) { require(_to != address(0), "InverseAppProjectedNft: zero receiver address"); uint256 tokenId = currentTokenId; @@ -82,18 +82,20 @@ contract InverseAppProjectedNft is IInverseAppProjectedNft, ERC721, Ownable { /// @dev Burns token of ID `_tokenId`. Callable only by the owner of the specified token. /// Reverts if `_tokenId` does not exist. - function burn(uint256 _tokenId) external onlyTokenOwner(_tokenId) { + function burn(uint256 _tokenId) public virtual onlyTokenOwner(_tokenId) { totalSupply--; _burn(_tokenId); } /// @dev Returns the `baseURI` of this NFT. - function _baseURI() internal view override returns (string memory) { + function _baseURI() internal view virtual override returns (string memory) { return baseURI; } /// @dev Returns the token URI of specified `tokenId` using the default set base URI. - function tokenURI(uint256 tokenId) public view override returns (string memory) { + function tokenURI( + uint256 tokenId + ) public view virtual override(ERC721, IERC721Metadata) returns (string memory) { return tokenURI(tokenId, _baseURI()); } @@ -101,12 +103,15 @@ contract InverseAppProjectedNft is IInverseAppProjectedNft, ERC721, Ownable { function tokenURI( uint256 tokenId, string memory customBaseUri - ) public view returns (string memory) { + ) public view virtual returns (string memory) { _requireOwned(tokenId); MintEntry memory entry = tokenToMint[tokenId]; string memory URI = bytes(customBaseUri).length > 0 ? string.concat( customBaseUri, + "eip155:", + block.chainid.toString(), + "/", Strings.toHexString(uint160(entry.minter), 20), "/", entry.userTokenId.toString() @@ -115,10 +120,18 @@ contract InverseAppProjectedNft is IInverseAppProjectedNft, ERC721, Ownable { return string(abi.encodePacked(URI, baseExtension)); } + /// @dev Returns the token URI of specified `tokenId` using a call to contract implementing `ITokenUri`. + function tokenURI( + uint256 tokenId, + ITokenUri customUriInterface + ) public view returns (string memory) { + return customUriInterface.tokenURI(tokenId); + } + /// @dev Sets `_URI` as the `baseURI` of the NFT. /// Callable only by the contract owner. /// Emits the `SetBaseURI` event. - function setBaseURI(string memory _URI) external onlyOwner { + function setBaseURI(string memory _URI) public virtual onlyOwner { string memory oldURI = baseURI; baseURI = _URI; emit SetBaseURI(oldURI, _URI); @@ -126,7 +139,7 @@ contract InverseAppProjectedNft is IInverseAppProjectedNft, ERC721, Ownable { /// @dev Sets `_newBaseExtension` as the `baseExtension` of the NFT. /// Callable only by the contract owner. - function setBaseExtension(string memory _newBaseExtension) public onlyOwner { + function setBaseExtension(string memory _newBaseExtension) public virtual onlyOwner { string memory oldBaseExtension = baseExtension; baseExtension = _newBaseExtension; emit SetBaseURI(oldBaseExtension, _newBaseExtension); diff --git a/packages/contracts/evm-contracts/contracts/token/InverseBaseProjectedNft.sol b/packages/contracts/evm-contracts/contracts/token/InverseBaseProjectedNft.sol index c441989f7..c1d9bffcb 100644 --- a/packages/contracts/evm-contracts/contracts/token/InverseBaseProjectedNft.sol +++ b/packages/contracts/evm-contracts/contracts/token/InverseBaseProjectedNft.sol @@ -3,12 +3,12 @@ pragma solidity ^0.8.13; import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import {IERC721Metadata} from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; -import {IERC4906} from "@openzeppelin/contracts/interfaces/IERC4906.sol"; import {IInverseProjectedNft} from "./IInverseProjectedNft.sol"; import {IInverseBaseProjectedNft} from "./IInverseBaseProjectedNft.sol"; +import {ITokenUri} from "./ITokenUri.sol"; /// @dev A standard ERC721 that accepts calldata in the mint function for any initialization data needed in a Paima dApp. contract InverseBaseProjectedNft is IInverseBaseProjectedNft, ERC721, Ownable { @@ -43,7 +43,7 @@ contract InverseBaseProjectedNft is IInverseBaseProjectedNft, ERC721, Ownable { /// @dev Returns true if this contract implements the interface defined by `interfaceId`. See EIP165. function supportsInterface( bytes4 interfaceId - ) public view override(IERC165, ERC721) returns (bool) { + ) public view virtual override(IERC165, ERC721) returns (bool) { return interfaceId == type(IInverseProjectedNft).interfaceId || interfaceId == type(IInverseBaseProjectedNft).interfaceId || @@ -55,7 +55,7 @@ contract InverseBaseProjectedNft is IInverseBaseProjectedNft, ERC721, Ownable { /// Increases the `totalSupply` and `currentTokenId`. /// Reverts if `_to` is a zero address or if it refers to smart contract but does not implement IERC721Receiver-onERC721Received. /// Emits the `Minted` event. - function mint(address _to, string calldata initialData) external returns (uint256) { + function mint(address _to, string calldata initialData) public virtual returns (uint256) { require(_to != address(0), "InverseBaseProjectedNft: zero receiver address"); uint256 tokenId = currentTokenId; @@ -70,18 +70,20 @@ contract InverseBaseProjectedNft is IInverseBaseProjectedNft, ERC721, Ownable { /// @dev Burns token of ID `_tokenId`. Callable only by the owner of the specified token. /// Reverts if `_tokenId` does not exist. - function burn(uint256 _tokenId) external onlyTokenOwner(_tokenId) { + function burn(uint256 _tokenId) public virtual onlyTokenOwner(_tokenId) { totalSupply--; _burn(_tokenId); } /// @dev Returns the `baseURI` of this NFT. - function _baseURI() internal view override returns (string memory) { + function _baseURI() internal view virtual override returns (string memory) { return baseURI; } /// @dev Returns the token URI of specified `tokenId` using the default set base URI. - function tokenURI(uint256 tokenId) public view override returns (string memory) { + function tokenURI( + uint256 tokenId + ) public view virtual override(ERC721, IERC721Metadata) returns (string memory) { return tokenURI(tokenId, _baseURI()); } @@ -89,18 +91,32 @@ contract InverseBaseProjectedNft is IInverseBaseProjectedNft, ERC721, Ownable { function tokenURI( uint256 tokenId, string memory customBaseUri - ) public view returns (string memory) { + ) public view virtual returns (string memory) { _requireOwned(tokenId); string memory URI = bytes(customBaseUri).length > 0 - ? string.concat(customBaseUri, tokenId.toString()) + ? string.concat( + customBaseUri, + "eip155:", + block.chainid.toString(), + "/", + tokenId.toString() + ) : ""; return string(abi.encodePacked(URI, baseExtension)); } + /// @dev Returns the token URI of specified `tokenId` using a call to contract implementing `ITokenUri`. + function tokenURI( + uint256 tokenId, + ITokenUri customUriInterface + ) public view returns (string memory) { + return customUriInterface.tokenURI(tokenId); + } + /// @dev Sets `_URI` as the `baseURI` of the NFT. /// Callable only by the contract owner. /// Emits the `SetBaseURI` event. - function setBaseURI(string memory _URI) external onlyOwner { + function setBaseURI(string memory _URI) public virtual onlyOwner { string memory oldURI = baseURI; baseURI = _URI; emit SetBaseURI(oldURI, _URI); @@ -108,7 +124,7 @@ contract InverseBaseProjectedNft is IInverseBaseProjectedNft, ERC721, Ownable { /// @dev Sets `_newBaseExtension` as the `baseExtension` of the NFT. /// Callable only by the contract owner. - function setBaseExtension(string memory _newBaseExtension) public onlyOwner { + function setBaseExtension(string memory _newBaseExtension) public virtual onlyOwner { string memory oldBaseExtension = baseExtension; baseExtension = _newBaseExtension; emit SetBaseURI(oldBaseExtension, _newBaseExtension); diff --git a/packages/contracts/evm-contracts/test/InverseAppProjectedNft.t.sol b/packages/contracts/evm-contracts/test/InverseAppProjectedNft.t.sol index 7cbceb817..522da61a3 100644 --- a/packages/contracts/evm-contracts/test/InverseAppProjectedNft.t.sol +++ b/packages/contracts/evm-contracts/test/InverseAppProjectedNft.t.sol @@ -4,14 +4,27 @@ pragma solidity ^0.8.18; import "../test-lib/cheatcodes.sol"; import "../test-lib/console.sol"; import "../test-lib/ctest.sol"; -import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; -import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; -import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; -import "@openzeppelin/contracts/interfaces/IERC4906.sol"; -import "../contracts/token/InverseAppProjectedNft.sol"; -import "../contracts/token/IInverseProjectedNft.sol"; - -contract InverseAppProjectedNftTest is CTest { +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; +import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import {IERC4906} from "@openzeppelin/contracts/interfaces/IERC4906.sol"; +import {InverseAppProjectedNft} from "../contracts/token/InverseAppProjectedNft.sol"; +import {IInverseAppProjectedNft} from "../contracts/token/IInverseAppProjectedNft.sol"; +import {ITokenUri} from "../contracts/token/ITokenUri.sol"; + +contract MockTokenUri is ITokenUri { + using Strings for uint256; + + function tokenURI(uint256 tokenId) external view override returns (string memory) { + return string.concat("mock://", tokenId.toString()); + } +} + +contract InverseAppProjectedNftTest is CTest, ERC721Holder { + using Strings for uint256; + CheatCodes vm = CheatCodes(HEVM_ADDRESS); InverseAppProjectedNft public nft; uint256 ownedTokenId; @@ -44,7 +57,7 @@ contract InverseAppProjectedNftTest is CTest { assertEq( result, string.concat( - "192.168.0.1/", + "192.168.0.1/eip155:31337/", Strings.toHexString(uint160(address(this)), 20), "/", "1.json" @@ -57,7 +70,7 @@ contract InverseAppProjectedNftTest is CTest { assertEq( result, string.concat( - "1.1.0.0/", + "1.1.0.0/eip155:31337/", Strings.toHexString(uint160(address(this)), 20), "/", "1.json" @@ -65,6 +78,12 @@ contract InverseAppProjectedNftTest is CTest { ); } + function test_TokenUriUsingCustomUriInterface() public { + ITokenUri tokenUriInterface = new MockTokenUri(); + string memory result = nft.tokenURI(ownedTokenId, tokenUriInterface); + assertEq(result, string.concat("mock://", ownedTokenId.toString())); + } + function test_SupportsInterface() public { assertTrue(nft.supportsInterface(type(IERC165).interfaceId)); assertTrue(nft.supportsInterface(type(IERC721).interfaceId)); @@ -108,13 +127,4 @@ contract InverseAppProjectedNftTest is CTest { vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, alice)); nft.setBaseExtension("test"); } - - function onERC721Received( - address, - address, - uint256, - bytes calldata - ) external pure returns (bytes4) { - return IERC721Receiver.onERC721Received.selector; - } } diff --git a/packages/contracts/evm-contracts/test/InverseBaseProjectedNft.t.sol b/packages/contracts/evm-contracts/test/InverseBaseProjectedNft.t.sol index ea8e7a413..b9dd702cb 100644 --- a/packages/contracts/evm-contracts/test/InverseBaseProjectedNft.t.sol +++ b/packages/contracts/evm-contracts/test/InverseBaseProjectedNft.t.sol @@ -4,14 +4,27 @@ pragma solidity ^0.8.18; import "../test-lib/cheatcodes.sol"; import "../test-lib/console.sol"; import "../test-lib/ctest.sol"; -import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; -import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; -import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; -import "@openzeppelin/contracts/interfaces/IERC4906.sol"; -import "../contracts/token/InverseBaseProjectedNft.sol"; -import "../contracts/token/IInverseProjectedNft.sol"; - -contract InverseBaseProjectedNftTest is CTest { +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; +import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import {IERC4906} from "@openzeppelin/contracts/interfaces/IERC4906.sol"; +import {InverseBaseProjectedNft} from "../contracts/token/InverseBaseProjectedNft.sol"; +import {IInverseBaseProjectedNft} from "../contracts/token/IInverseBaseProjectedNft.sol"; +import {ITokenUri} from "../contracts/token/ITokenUri.sol"; + +contract MockTokenUri is ITokenUri { + using Strings for uint256; + + function tokenURI(uint256 tokenId) external view override returns (string memory) { + return string.concat("mock://", tokenId.toString()); + } +} + +contract InverseBaseProjectedNftTest is CTest, ERC721Holder { + using Strings for uint256; + CheatCodes vm = CheatCodes(HEVM_ADDRESS); InverseBaseProjectedNft public nft; uint256 ownedTokenId; @@ -41,12 +54,18 @@ contract InverseBaseProjectedNftTest is CTest { function test_TokenUriUsesBaseUriByDefault() public { string memory result = nft.tokenURI(ownedTokenId); - assertEq(result, "192.168.0.1/1.json"); + assertEq(result, "192.168.0.1/eip155:31337/1.json"); } function test_TokenUriUsingCustomBaseUri() public { string memory result = nft.tokenURI(ownedTokenId, "1.1.0.0/"); - assertEq(result, "1.1.0.0/1.json"); + assertEq(result, "1.1.0.0/eip155:31337/1.json"); + } + + function test_TokenUriUsingCustomUriInterface() public { + ITokenUri tokenUriInterface = new MockTokenUri(); + string memory result = nft.tokenURI(ownedTokenId, tokenUriInterface); + assertEq(result, string.concat("mock://", ownedTokenId.toString())); } function test_SupportsInterface() public { @@ -92,13 +111,4 @@ contract InverseBaseProjectedNftTest is CTest { vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, alice)); nft.setBaseExtension("test"); } - - function onERC721Received( - address, - address, - uint256, - bytes calldata - ) external pure returns (bytes4) { - return IERC721Receiver.onERC721Received.selector; - } }