From 73a0a98a890f67b274746f78eac98095439defdc Mon Sep 17 00:00:00 2001 From: Kingster Date: Mon, 25 Mar 2024 07:44:11 -0700 Subject: [PATCH] add functions to set and get all metadata --- .../modules/metadata/ICoreMetadataModule.sol | 22 ++++++ .../metadata/ICoreMetadataViewModule.sol | 21 +++++- .../modules/metadata/CoreMetadataModule.sol | 49 ++++++++---- .../metadata/CoreMetadataViewModule.sol | 17 ++++- .../modules/metadata/CoreMetadataModule.t.sol | 74 ++++++++++++++++++- .../metadata/CoreMetadataViewModule.t.sol | 29 ++++++-- 6 files changed, 188 insertions(+), 24 deletions(-) diff --git a/contracts/interfaces/modules/metadata/ICoreMetadataModule.sol b/contracts/interfaces/modules/metadata/ICoreMetadataModule.sol index 82767afba..16443b83c 100644 --- a/contracts/interfaces/modules/metadata/ICoreMetadataModule.sol +++ b/contracts/interfaces/modules/metadata/ICoreMetadataModule.sol @@ -7,6 +7,15 @@ import { IModule } from "../../../../contracts/interfaces/modules/base/IModule.s /// @notice Manages the core metadata for IP assets within the Story Protocol. /// @dev This contract allows setting and updating core metadata attributes for IP assets. interface ICoreMetadataModule is IModule { + /// @notice Emitted when the name for an IP asset is set. + event IPNameSet(address indexed ipId, string name); + + /// @notice Emitted when the description for an IP asset is set. + event IPDescriptionSet(address indexed ipId, string description); + + /// @notice Emitted when the content hash for an IP asset is set. + event IPContentHashSet(address indexed ipId, bytes32 contentHash); + /// @notice Sets the name for an IP asset. /// @dev Can only be called once per IP asset to prevent overwriting. /// @param ipAccount The address of the IP asset. @@ -24,4 +33,17 @@ interface ICoreMetadataModule is IModule { /// @param ipAccount The address of the IP asset. /// @param contentHash The content hash to set for the IP asset. function setIpContentHash(address ipAccount, bytes32 contentHash) external; + + /// @notice Sets all core metadata for an IP asset. + /// @dev Can only be called once per IP asset to prevent overwriting. + /// @param ipAccount The address of the IP asset. + /// @param name The name to set for the IP asset. + /// @param description The description to set for the IP asset. + /// @param contentHash The content hash to set for the IP asset. + function setIpMetadata( + address ipAccount, + string memory name, + string memory description, + bytes32 contentHash + ) external; } diff --git a/contracts/interfaces/modules/metadata/ICoreMetadataViewModule.sol b/contracts/interfaces/modules/metadata/ICoreMetadataViewModule.sol index eb03f5cf8..a6eab29ac 100644 --- a/contracts/interfaces/modules/metadata/ICoreMetadataViewModule.sol +++ b/contracts/interfaces/modules/metadata/ICoreMetadataViewModule.sol @@ -9,6 +9,17 @@ import { IViewModule } from "../base/IViewModule.sol"; /// The view module consolidates core metadata for IPAccounts from both IPAssetRegistry and CoreMetadataModule. /// @dev The "name" from CoreMetadataModule overrides the "name" from IPAssetRegistry if set. interface ICoreMetadataViewModule is IViewModule { + + /// @notice Core metadata struct for IPAccounts. + struct CoreMetadata { + string name; + string description; + uint256 registrationDate; + bytes32 contentHash; + string uri; + address owner; + } + /// @notice Retrieves the name of the IPAccount, preferring the name from CoreMetadataModule if available. /// @param ipId The address of the IPAccount. /// @return The name of the IPAccount. @@ -39,10 +50,16 @@ interface ICoreMetadataViewModule is IViewModule { /// @return The address of the owner of the IPAccount. function getOwner(address ipId) external view returns (address); - /// @notice Generates a JSON string of all metadata for the IPAccount. + /// @notice Retrieves all core metadata of the IPAccount. + /// @param ipId The address of the IPAccount. + /// @return The CoreMetadata struct of the IPAccount. + function getCoreMetadata(address ipId) external view returns (CoreMetadata memory); + + /// @notice Generates a JSON string formatted according to the standard NFT metadata schema for the IPAccount, + //// including all relevant metadata fields. /// @dev This function consolidates metadata from both IPAssetRegistry /// and CoreMetadataModule, with "name" from CoreMetadataModule taking precedence. /// @param ipId The address of the IPAccount. /// @return A JSON string representing all metadata of the IPAccount. - function tokenURI(address ipId) external view returns (string memory); + function getJsonString(address ipId) external view returns (string memory); } diff --git a/contracts/modules/metadata/CoreMetadataModule.sol b/contracts/modules/metadata/CoreMetadataModule.sol index 5659408ba..63b60c7b3 100644 --- a/contracts/modules/metadata/CoreMetadataModule.sol +++ b/contracts/modules/metadata/CoreMetadataModule.sol @@ -36,32 +36,55 @@ contract CoreMetadataModule is BaseModule, AccessControlled, ICoreMetadataModule ) AccessControlled(accessController, ipAccountRegistry) {} /// @inheritdoc ICoreMetadataModule - function setIpName( + function setIpName(address ipAccount, string memory ipName) external verifyPermission(ipAccount) { + _setIpName(ipAccount, ipName); + } + + /// @inheritdoc ICoreMetadataModule + function setIpDescription(address ipAccount, string memory description) external verifyPermission(ipAccount) { + _setIpDescription(ipAccount, description); + } + + /// @inheritdoc ICoreMetadataModule + function setIpContentHash(address ipAccount, bytes32 contentHash) external verifyPermission(ipAccount) { + _setIpContentHash(ipAccount, contentHash); + } + + function setIpMetadata( address ipAccount, - string memory ipName - ) external verifyPermission(ipAccount) onlyOnce(ipAccount, "IP_NAME") { + string memory ipName, + string memory description, + bytes32 contentHash + ) external verifyPermission(ipAccount) { + _setIpName(ipAccount, ipName); + _setIpDescription(ipAccount, description); + _setIpContentHash(ipAccount, contentHash); + } + + /// @dev Implements the IERC165 interface. + function supportsInterface(bytes4 interfaceId) public view virtual override(BaseModule, IERC165) returns (bool) { + return interfaceId == type(ICoreMetadataModule).interfaceId || super.supportsInterface(interfaceId); + } + + function _setIpName(address ipAccount, string memory ipName) internal onlyOnce(ipAccount, "IP_NAME") { IIPAccount(payable(ipAccount)).setString("IP_NAME", ipName); + emit IPNameSet(ipAccount, ipName); } - /// @inheritdoc ICoreMetadataModule - function setIpDescription( + function _setIpDescription( address ipAccount, string memory description - ) external verifyPermission(ipAccount) onlyOnce(ipAccount, "IP_DESCRIPTION") { + ) internal onlyOnce(ipAccount, "IP_DESCRIPTION") { IIPAccount(payable(ipAccount)).setString("IP_DESCRIPTION", description); + emit IPDescriptionSet(ipAccount, description); } - /// @inheritdoc ICoreMetadataModule - function setIpContentHash(address ipAccount, bytes32 contentHash) external verifyPermission(ipAccount) { + function _setIpContentHash(address ipAccount, bytes32 contentHash) internal { if (IIPAccount(payable(ipAccount)).getBytes32("IP_CONTENT_HASH") != bytes32(0)) { revert Errors.CoreMetadataModule__MetadataAlreadySet(); } IIPAccount(payable(ipAccount)).setBytes32("IP_CONTENT_HASH", contentHash); - } - - /// @dev Implements the IERC165 interface. - function supportsInterface(bytes4 interfaceId) public view virtual override(BaseModule, IERC165) returns (bool) { - return interfaceId == type(ICoreMetadataModule).interfaceId || super.supportsInterface(interfaceId); + emit IPContentHashSet(ipAccount, contentHash); } /// @dev Checks if a string is empty. diff --git a/contracts/modules/metadata/CoreMetadataViewModule.sol b/contracts/modules/metadata/CoreMetadataViewModule.sol index 7fa0078c4..9ba21691f 100644 --- a/contracts/modules/metadata/CoreMetadataViewModule.sol +++ b/contracts/modules/metadata/CoreMetadataViewModule.sol @@ -34,6 +34,19 @@ contract CoreMetadataViewModule is BaseModule, ICoreMetadataViewModule { coreMetadataModule = IModuleRegistry(MODULE_REGISTRY).getModule(CORE_METADATA_MODULE_KEY); } + /// @inheritdoc ICoreMetadataViewModule + function getCoreMetadata(address ipId) external view returns (CoreMetadata memory) { + return + CoreMetadata({ + name: getName(ipId), + description: getDescription(ipId), + registrationDate: getRegistrationDate(ipId), + contentHash: getContentHash(ipId), + uri: getUri(ipId), + owner: getOwner(ipId) + }); + } + /// @inheritdoc ICoreMetadataViewModule function getName(address ipId) public view returns (string memory) { string memory ipName = IIPAccount(payable(ipId)).getString(coreMetadataModule, "IP_NAME"); @@ -59,7 +72,7 @@ contract CoreMetadataViewModule is BaseModule, ICoreMetadataViewModule { } /// @inheritdoc ICoreMetadataViewModule - function getUri(address ipId) external view returns (string memory) { + function getUri(address ipId) public view returns (string memory) { return IIPAccount(payable(ipId)).getString(IP_ASSET_REGISTRY, "URI"); } @@ -69,7 +82,7 @@ contract CoreMetadataViewModule is BaseModule, ICoreMetadataViewModule { } /// @inheritdoc ICoreMetadataViewModule - function tokenURI(address ipId) external view returns (string memory) { + function getJsonString(address ipId) external view returns (string memory) { string memory baseJson = string( /* solhint-disable */ abi.encodePacked( diff --git a/test/foundry/modules/metadata/CoreMetadataModule.t.sol b/test/foundry/modules/metadata/CoreMetadataModule.t.sol index 6faf6068e..cf1466c20 100644 --- a/test/foundry/modules/metadata/CoreMetadataModule.t.sol +++ b/test/foundry/modules/metadata/CoreMetadataModule.t.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.23; import { IIPAccount } from "../../../../contracts/interfaces/IIPAccount.sol"; import { CoreMetadataModule } from "../../../../contracts/modules/metadata/CoreMetadataModule.sol"; +import { ICoreMetadataModule } from "../../../../contracts/interfaces/modules/metadata/ICoreMetadataModule.sol"; import { Errors } from "../../../../contracts/lib/Errors.sol"; import { CORE_METADATA_MODULE_KEY } from "../../../../contracts/lib/modules/Module.sol"; import { BaseTest } from "../../utils/BaseTest.t.sol"; @@ -34,6 +35,9 @@ contract CoreMetadataModuleTest is BaseTest { } function test_CoreMetadata_Name() public { + vm.expectEmit(); + emit ICoreMetadataModule.IPNameSet(address(ipAccount), "My IP"); + vm.prank(alice); coreMetadataModule.setIpName(address(ipAccount), "My IP"); assertEq(ipAccount.getString(address(coreMetadataModule), "IP_NAME"), "My IP"); @@ -84,6 +88,9 @@ contract CoreMetadataModuleTest is BaseTest { } function test_CoreMetadata_Description() public { + vm.expectEmit(); + emit ICoreMetadataModule.IPDescriptionSet(address(ipAccount), "My Description"); + vm.prank(alice); coreMetadataModule.setIpDescription(address(ipAccount), "My Description"); assertEq(ipAccount.getString(address(coreMetadataModule), "IP_DESCRIPTION"), "My Description"); @@ -134,6 +141,9 @@ contract CoreMetadataModuleTest is BaseTest { } function test_CoreMetadata_ContentHash() public { + vm.expectEmit(); + emit ICoreMetadataModule.IPContentHashSet(address(ipAccount), bytes32("0x1234")); + vm.prank(alice); coreMetadataModule.setIpContentHash(address(ipAccount), bytes32("0x1234")); assertEq(ipAccount.getBytes32(address(coreMetadataModule), "IP_CONTENT_HASH"), bytes32("0x1234")); @@ -183,7 +193,7 @@ contract CoreMetadataModuleTest is BaseTest { coreMetadataModule.setIpContentHash(address(ipAccount), bytes32("0x1234")); } - function test_CoreMetadata_All() public { + function test_CoreMetadata_Batch() public { vm.startPrank(alice); coreMetadataModule.setIpName(address(ipAccount), "My IP"); coreMetadataModule.setIpDescription(address(ipAccount), "My Description"); @@ -193,4 +203,66 @@ contract CoreMetadataModuleTest is BaseTest { assertEq(ipAccount.getString(address(coreMetadataModule), "IP_DESCRIPTION"), "My Description"); assertEq(ipAccount.getBytes32(address(coreMetadataModule), "IP_CONTENT_HASH"), bytes32("0x1234")); } + + function test_CoreMetadata_All() public { + vm.prank(alice); + coreMetadataModule.setIpMetadata( + address(ipAccount), + "My IP", + "My Description", + bytes32("0x1234") + ); + assertEq(ipAccount.getString(address(coreMetadataModule), "IP_NAME"), "My IP"); + assertEq(ipAccount.getString(address(coreMetadataModule), "IP_DESCRIPTION"), "My Description"); + assertEq(ipAccount.getBytes32(address(coreMetadataModule), "IP_CONTENT_HASH"), bytes32("0x1234")); + } + + function test_CoreMetadata_AllTwice() public { + vm.prank(alice); + coreMetadataModule.setIpMetadata( + address(ipAccount), + "My IP", + "My Description", + bytes32("0x1234") + ); + + vm.expectRevert(Errors.CoreMetadataModule__MetadataAlreadySet.selector); + vm.prank(alice); + coreMetadataModule.setIpMetadata( + address(ipAccount), + "My New IP", + "My New Description", + bytes32("0x5678") + ); + } + + function test_CoreMetadata_All_InvalidIpAccount() public { + vm.expectRevert(abi.encodeWithSelector(Errors.AccessControlled__NotIpAccount.selector, address(0x1234))); + vm.prank(alice); + coreMetadataModule.setIpMetadata( + address(0x1234), + "My IP", + "My Description", + bytes32("0x1234") + ); + } + + function test_CoreMetadata_All_InvalidCaller() public { + vm.expectRevert( + abi.encodeWithSelector( + Errors.AccessController__PermissionDenied.selector, + address(ipAccount), + bob, + address(coreMetadataModule), + coreMetadataModule.setIpMetadata.selector + ) + ); + vm.prank(bob); + coreMetadataModule.setIpMetadata( + address(ipAccount), + "My IP", + "My Description", + bytes32("0x1234") + ); + } } diff --git a/test/foundry/modules/metadata/CoreMetadataViewModule.t.sol b/test/foundry/modules/metadata/CoreMetadataViewModule.t.sol index b290196a1..1cb936952 100644 --- a/test/foundry/modules/metadata/CoreMetadataViewModule.t.sol +++ b/test/foundry/modules/metadata/CoreMetadataViewModule.t.sol @@ -70,7 +70,7 @@ contract CoreMetadataViewModuleTest is BaseTest { assertEq(coreMetadataViewModule.getContentHash(address(ipAccount)), bytes32(0)); } - function test_CoreMetadataViewModule_TokenURI() public { + function test_CoreMetadataViewModule_JsonString() public { vm.prank(alice); coreMetadataModule.setIpName(address(ipAccount), "My IP"); vm.prank(alice); @@ -78,17 +78,34 @@ contract CoreMetadataViewModuleTest is BaseTest { vm.prank(alice); coreMetadataModule.setIpContentHash(address(ipAccount), bytes32("0x1234")); assertEq( - _getExpectedTokenURI("My IP", "My Description", bytes32("0x1234")), - coreMetadataViewModule.tokenURI(address(ipAccount)) + _getExpectedJsonString("My IP", "My Description", bytes32("0x1234")), + coreMetadataViewModule.getJsonString(address(ipAccount)) ); } - function test_CoreMetadataViewModule_TokenURI_without_CoreMetadata() public { + function test_CoreMetadataViewModule_GetCoreMetadataStrut() public { + vm.prank(alice); + coreMetadataModule.setIpMetadata(address(ipAccount), "My IP", "My Description", bytes32("0x1234")); + CoreMetadataViewModule.CoreMetadata memory coreMetadata = coreMetadataViewModule.getCoreMetadata( + address(ipAccount) + ); + assertEq(coreMetadata.name, "My IP"); + assertEq(coreMetadata.description, "My Description"); + assertEq(coreMetadata.contentHash, bytes32("0x1234")); + assertEq(coreMetadata.registrationDate, block.timestamp); + assertEq(coreMetadata.owner, alice); + assertEq(coreMetadata.uri, "https://storyprotocol.xyz/erc721/99"); + } + + function test_CoreMetadataViewModule_GetJsonStr_without_CoreMetadata() public { string memory name = string.concat(block.chainid.toString(), ": Ape #99"); - assertEq(_getExpectedTokenURI(name, "", bytes32(0)), coreMetadataViewModule.tokenURI(address(ipAccount))); + assertEq( + _getExpectedJsonString(name, "", bytes32(0)), + coreMetadataViewModule.getJsonString(address(ipAccount)) + ); } - function _getExpectedTokenURI( + function _getExpectedJsonString( string memory name, string memory description, bytes32 contentHash