diff --git a/contracts/interfaces/modules/IRegistrationModule.sol b/contracts/interfaces/modules/IRegistrationModule.sol index 6e632006e..024d7f97e 100644 --- a/contracts/interfaces/modules/IRegistrationModule.sol +++ b/contracts/interfaces/modules/IRegistrationModule.sol @@ -13,7 +13,7 @@ interface IRegistrationModule { address tokenContract, uint256 tokenId, string memory ipName, - string memory ipDescription, - bytes32 hash + bytes32 hash, + string calldata externalURL ) external; } diff --git a/contracts/interfaces/registries/IIPRecordRegistry.sol b/contracts/interfaces/registries/IIPRecordRegistry.sol index b1844b3ad..a0c9ebfce 100644 --- a/contracts/interfaces/registries/IIPRecordRegistry.sol +++ b/contracts/interfaces/registries/IIPRecordRegistry.sol @@ -11,12 +11,14 @@ interface IIPRecordRegistry { /// @param tokenContract The address of the IP. /// @param tokenId The token identifier of the IP. /// @param resolver The address of the resolver linked to the IP. + /// @param provider The address of the metadata provider linked to the IP. event IPRegistered( address ipId, uint256 indexed chainId, address indexed tokenContract, uint256 indexed tokenId, - address resolver + address resolver, + address provider ); /// @notice Emits when an IP account is created for an IP. @@ -39,6 +41,14 @@ interface IIPRecordRegistry { address resolver ); + /// @notice Emits when a metadata provider is set for an IP. + /// @param ipId The canonical identifier of the specified IP. + /// @param metadataProvider Address of the metadata provider associated with the IP. + event MetadataProviderSet( + address ipId, + address metadataProvider + ); + /// @notice Gets the canonical IP identifier associated with an IP (NFT). /// @dev This is the same as the address of the IP account bound to the IP. /// @param chainId The chain identifier of where the IP resides. @@ -87,18 +97,26 @@ interface IIPRecordRegistry { uint256 tokenId ) external view returns (address); + /// @notice Gets the metadata provider linked to an IP based on its ID. + /// @param id The canonical identifier for the IP. + /// @return The metadata that was bound to this IP at creation time. + function metadataProvider(address id) external view returns (address); + /// @notice Registers an NFT as IP, creating a corresponding IP record. /// @dev This is only callable by an authorized registration module. /// @param chainId The chain identifier of where the IP resides. /// @param tokenContract The address of the IP. /// @param tokenId The token identifier of the IP. + /// @param resolverAddr The address of the resolver to associate with the IP. /// @param createAccount Whether to create an IP account in the process. + /// @param metadataProvider The metadata provider to associate with the IP. function register( uint256 chainId, address tokenContract, uint256 tokenId, address resolverAddr, - bool createAccount + bool createAccount, + address metadataProvider ) external returns (address); /// @notice Creates the IP account for the specified IP. diff --git a/contracts/interfaces/registries/metadata/IMetadataProvider.sol b/contracts/interfaces/registries/metadata/IMetadataProvider.sol new file mode 100644 index 000000000..1ced771f5 --- /dev/null +++ b/contracts/interfaces/registries/metadata/IMetadataProvider.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: UNLICENSED +// See https://github.com/storyprotocol/protocol-contracts/blob/main/StoryProtocol-AlphaTestingAgreement-17942166.3.pdf +pragma solidity ^0.8.23; + +/// @title Metadata Provider Interface +interface IMetadataProvider { + + /// @notice Gets the metadata associated with an IP asset. + /// @param ipId The address identifier of the IP asset. + function getMetadata(address ipId) external view returns (bytes memory); +} diff --git a/contracts/interfaces/resolvers/IIPMetadataResolver.sol b/contracts/interfaces/resolvers/IIPMetadataResolver.sol deleted file mode 100644 index ab5b95c10..000000000 --- a/contracts/interfaces/resolvers/IIPMetadataResolver.sol +++ /dev/null @@ -1,76 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -// See https://github.com/storyprotocol/protocol-contracts/blob/main/StoryProtocol-AlphaTestingAgreement-17942166.3.pdf -pragma solidity ^0.8.21; - -import { IResolver } from "contracts/interfaces/resolvers/IResolver.sol"; -import { IP } from "contracts/lib/IP.sol"; - -/// @notice Resolver Interface -interface IIPMetadataResolver is IResolver { - event IPMetadataResolverSetRecord(address indexed ipId, IP.MetadataRecord data); - - event IPMetadataResolverSetName(address indexed ipId, string name); - - event IPMetadataResolverSetDescription(address indexed ipId, string description); - - event IPMetadataResolverSetHash(address indexed ipId, bytes32 hash); - - event IPMetadataResolverSetURI(address indexed ipId, string uri); - - /// @notice Fetches core metadata attributed to a specific IP. - function metadata(address ipId) external view returns (IP.Metadata memory); - - /// @notice Fetches the canonical name associated with the specified IP. - /// @param ipId The canonical ID of the specified IP. - function name(address ipId) external view returns (string memory); - - /// @notice Fetches the description associated with the specified IP. - /// @param ipId The canonical ID of the specified IP. - /// @return The string descriptor of the IP. - function description(address ipId) external view returns (string memory); - - /// @notice Fetches the keccak-256 hash associated with the specified IP. - /// @param ipId The canonical ID of the specified IP. - /// @return The bytes32 content hash of the IP. - function hash(address ipId) external view returns (bytes32); - - /// @notice Fetches the date of registration of the IP. - /// @param ipId The canonical ID of the specified IP. - function registrationDate(address ipId) external view returns (uint64); - - /// @notice Fetches the initial registrant of the IP. - /// @param ipId The canonical ID of the specified IP. - function registrant(address ipId) external view returns (address); - - /// @notice Fetches the current owner of the IP. - /// @param ipId The canonical ID of the specified IP. - function owner(address ipId) external view returns (address); - - /// @notice Fetches an IP owner defined URI associated with the IP. - /// @param ipId The canonical ID of the specified IP. - function uri(address ipId) external view returns (string memory); - - /// @notice Sets the core metadata associated with an IP. - /// @param ipId The canonical ID of the specified IP. - /// @param data Metadata to be stored for the IP in the metadata resolver. - function setMetadata(address ipId, IP.MetadataRecord calldata data) external; - - /// @notice Sets the name associated with an IP. - /// @param ipId The canonical ID of the specified IP. - /// @param name The string name to associate with the IP. - function setName(address ipId, string calldata name) external; - - /// @notice Sets the description associated with an IP. - /// @param ipId The canonical ID of the specified IP. - /// @param description The string description to associate with the IP. - function setDescription(address ipId, string calldata description) external; - - /// @notice Sets the keccak-256 hash associated with an IP. - /// @param ipId The canonical ID of the specified IP. - /// @param hash The keccak-256 hash to associate with the IP. - function setHash(address ipId, bytes32 hash) external; - - /// @notice Sets an IP owner defined URI to associate with the IP. - /// @param ipId The canonical ID of the specified IP. - function setURI(address ipId, string calldata uri) external; -} diff --git a/contracts/interfaces/resolvers/IKeyValueResolver.sol b/contracts/interfaces/resolvers/IKeyValueResolver.sol new file mode 100644 index 000000000..eeb80c844 --- /dev/null +++ b/contracts/interfaces/resolvers/IKeyValueResolver.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: UNLICENSED +// See https://github.com/storyprotocol/protocol-contracts/blob/main/StoryProtocol-AlphaTestingAgreement-17942166.3.pdf +pragma solidity ^0.8.23; + +/// @title Key Value Resolver Interface +interface IKeyValueResolver { + + /// @notice Emits when a new key-value pair is set for the resolver. + event KeyValueSet( + address indexed ipId, + string indexed key, + string value + ); + + /// @notice Retrieves the string value associated with a key for an IP asset. + function value( + address ipId, + string calldata key + ) external view returns (string memory); +} diff --git a/contracts/lib/Errors.sol b/contracts/lib/Errors.sol index cc95f096b..9b47c05b7 100644 --- a/contracts/lib/Errors.sol +++ b/contracts/lib/Errors.sol @@ -41,6 +41,13 @@ library Errors { /// @notice Caller not authorized to perform the IP resolver function call. error IPResolver_Unauthorized(); + //////////////////////////////////////////////////////////////////////////// + // Metadata Provider /// + //////////////////////////////////////////////////////////////////////////// + + /// @notice Caller does not access to set metadata storage for the provider. + error MetadataProvider_Unauthorized(); + //////////////////////////////////////////////////////////////////////////// // LicenseRegistry // //////////////////////////////////////////////////////////////////////////// @@ -132,5 +139,4 @@ library Errors { error TaggingModule__SrcIpIdDoesNotHaveSrcTag(); error TaggingModule__DstIpIdDoesNotHaveDstTag(); error TaggingModule__RelationTypeDoesNotExist(); - } diff --git a/contracts/lib/IP.sol b/contracts/lib/IP.sol index 341ddef4a..f566439dc 100644 --- a/contracts/lib/IP.sol +++ b/contracts/lib/IP.sol @@ -1,47 +1,21 @@ // SPDX-License-Identifier: UNLICENSED // See https://github.com/storyprotocol/protocol-contracts/blob/main/StoryProtocol-AlphaTestingAgreement-17942166.3.pdf -pragma solidity ^0.8.21; +pragma solidity ^0.8.23; /// @title IP Library /// @notice Library for constants, structs, and helper functions used for IP. library IP { - /// @notice Core metadata associated with an IP. - /// @dev This is what is fetched when `metadata()` is called from an IP - /// resolver, and includes aggregated attributes fetched from various - /// modules in addition to that which is stored on the resolver itself. + /// @notice Core metadata to associate with each IP. struct Metadata { - // The current owner of the IP. - address owner; // The name associated with the IP. string name; - // A description associated with the IP. - string description; // A keccak-256 hash of the IP content. bytes32 hash; // The date which the IP was registered. uint64 registrationDate; // The address of the initial IP registrant. address registrant; - // The token URI associated with the IP. - string uri; - } - - /// @notice Core metadata exclusively saved by the IP resolver. - /// @dev This only encompasses metadata which is stored on the IP metadata - /// resolver itself, and does not include those attributes which may - /// be fetched from different modules (e.g. the licensing modules). - struct MetadataRecord { - // The name associated with the IP. - string name; - // A description associated with the IP. - string description; - // A keccak-256 hash of the IP content. - bytes32 hash; - // The date which the IP was registered. - uint64 registrationDate; - // The address of the initial IP registrant. - address registrant; - // The token URI associated with the IP. + // An external URI associated with the IP. string uri; } } diff --git a/contracts/lib/modules/Module.sol b/contracts/lib/modules/Module.sol index a36b2102f..684772d0c 100644 --- a/contracts/lib/modules/Module.sol +++ b/contracts/lib/modules/Module.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: UNLICENSED // See https://github.com/storyprotocol/protocol-contracts/blob/main/StoryProtocol-AlphaTestingAgreement-17942166.3.pdf -pragma solidity ^0.8.21; +pragma solidity ^0.8.23; // String values for core protocol modules. -string constant METADATA_RESOLVER_MODULE_KEY = "METADATA_RESOLVER_MODULE"; +string constant IP_RESOLVER_MODULE_KEY = "IP_RESOLVER_MODULE"; // String values for core protocol modules. string constant REGISTRATION_MODULE_KEY = "REGISTRATION_MODULE"; diff --git a/contracts/mocks/MockERC721.sol b/contracts/mocks/MockERC721.sol index efd6ef33f..d73a85fbb 100644 --- a/contracts/mocks/MockERC721.sol +++ b/contracts/mocks/MockERC721.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.23; -import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import { ERC721 } from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; contract MockERC721 is ERC721 { uint256 private _counter; diff --git a/contracts/modules/RegistrationModule.sol b/contracts/modules/RegistrationModule.sol index fefe47784..39ada40ef 100644 --- a/contracts/modules/RegistrationModule.sol +++ b/contracts/modules/RegistrationModule.sol @@ -2,11 +2,11 @@ // See https://github.com/storyprotocol/protocol-contracts/blob/main/StoryProtocol-AlphaTestingAgreement-17942166.3.pdf pragma solidity ^0.8.23; -// external import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; -// contracts + +import { IPResolver } from "contracts/resolvers/IPResolver.sol"; +import { IPMetadataProvider } from "contracts/registries/metadata/IPMetadataProvider.sol"; import { IRegistrationModule } from "contracts/interfaces/modules/IRegistrationModule.sol"; -import { IIPMetadataResolver } from "contracts/interfaces/resolvers/IIPMetadataResolver.sol"; import { REGISTRATION_MODULE_KEY } from "contracts/lib/modules/Module.sol"; import { Errors } from "contracts/lib/Errors.sol"; import { IP } from "contracts/lib/IP.sol"; @@ -19,7 +19,10 @@ import { BaseModule } from "contracts/modules/BaseModule.sol"; /// and terms specified by the IP registrant (IP account owner). contract RegistrationModule is BaseModule, IRegistrationModule { /// @notice The metadata resolver used by the registration module. - IIPMetadataResolver public resolver; + IPResolver public resolver; + + /// @notice Metadata storage provider contract. + IPMetadataProvider public metadataProvider; /// @notice Initializes the registration module contract. /// @param controller The access controller used for IP authorization. @@ -32,9 +35,11 @@ contract RegistrationModule is BaseModule, IRegistrationModule { address recordRegistry, address accountRegistry, address licenseRegistry, - address resolverAddr + address resolverAddr, + address metadataProviderAddr ) BaseModule(controller, recordRegistry, accountRegistry, licenseRegistry) { - resolver = IIPMetadataResolver(resolverAddr); + metadataProvider = IPMetadataProvider(metadataProviderAddr); + resolver = IPResolver(resolverAddr); } /// @notice Registers a root-level IP into the protocol. Root-level IPs can @@ -60,7 +65,14 @@ contract RegistrationModule is BaseModule, IRegistrationModule { } // Perform core IP registration and IP account creation. - address ipId = IP_RECORD_REGISTRY.register(block.chainid, tokenContract, tokenId, address(resolver), true); + address ipId = IP_RECORD_REGISTRY.register( + block.chainid, + tokenContract, + tokenId, + address(resolver), + true, + address(metadataProvider) + ); // Perform core IP policy creation. if (policyId != 0) { @@ -79,15 +91,17 @@ contract RegistrationModule is BaseModule, IRegistrationModule { /// @param tokenContract The address of the NFT bound to the derivative IP. /// @param tokenId The token id of the NFT bound to the derivative IP. /// @param ipName The name assigned to the new IP. - /// @param ipDescription A string description to assign to the IP. /// @param contentHash The content hash of the IP being registered. + /// @param externalURL An external URI to link to the IP. + /// TODO: Replace all metadata with a generic bytes parameter type, and do + /// encoding on the periphery contract level instead. function registerDerivativeIp( uint256 licenseId, address tokenContract, uint256 tokenId, string memory ipName, - string memory ipDescription, - bytes32 contentHash + bytes32 contentHash, + string calldata externalURL ) external { // Check that the caller is authorized to perform the registration. // TODO: Perform additional registration authorization logic, allowing @@ -96,32 +110,37 @@ contract RegistrationModule is BaseModule, IRegistrationModule { revert Errors.RegistrationModule__InvalidOwner(); } - // Perform core IP registration and IP account creation. - address ipId = IP_RECORD_REGISTRY.register(block.chainid, tokenContract, tokenId, address(resolver), true); - ACCESS_CONTROLLER.setPermission( - ipId, - address(this), + address ipId = IP_RECORD_REGISTRY.register( + block.chainid, + tokenContract, + tokenId, address(resolver), - IIPMetadataResolver.setMetadata.selector, - 1 + true, + address(metadataProvider) ); + // ACCESS_CONTROLLER.setPermission( + // ipId, + // address(this), + // address(resolver), + // IPResolver.setMetadata.selector, + // 1 + // ); - // Perform core IP derivative licensing - the license must be owned by the caller. - // TODO: return resulting policy index - LICENSE_REGISTRY.linkIpToParent(licenseId, ipId, msg.sender); - - // Perform metadata attribution setting. - resolver.setMetadata( - ipId, - IP.MetadataRecord({ + // Perform core IP registration and IP account creation. + bytes memory metadata = abi.encode( + IP.Metadata({ name: ipName, - description: ipDescription, hash: contentHash, registrationDate: uint64(block.timestamp), registrant: msg.sender, - uri: "" + uri: externalURL }) ); + metadataProvider.setMetadata(ipId, metadata); + + // Perform core IP derivative licensing - the license must be owned by the caller. + // TODO: return resulting policy index + LICENSE_REGISTRY.linkIpToParent(licenseId, ipId, msg.sender); emit DerivativeIPRegistered(msg.sender, ipId, licenseId); } diff --git a/contracts/registries/IPRecordRegistry.sol b/contracts/registries/IPRecordRegistry.sol index 599161aa2..90c51a38b 100644 --- a/contracts/registries/IPRecordRegistry.sol +++ b/contracts/registries/IPRecordRegistry.sol @@ -2,6 +2,7 @@ // See https://github.com/storyprotocol/protocol-contracts/blob/main/StoryProtocol-AlphaTestingAgreement-17942166.3.pdf pragma solidity ^0.8.21; +import { IMetadataProvider } from "contracts/interfaces/registries/metadata/IMetadataProvider.sol"; import { REGISTRATION_MODULE_KEY } from "contracts/lib/modules/Module.sol"; import { IIPRecordRegistry } from "contracts/interfaces/registries/IIPRecordRegistry.sol"; import { IIPAccountRegistry } from "contracts/interfaces/registries/IIPAccountRegistry.sol"; @@ -19,6 +20,14 @@ import { Errors } from "contracts/lib/Errors.sol"; /// IMPORTANT: The IP account address, besides being used for protocol /// auth, is also the canonical IP identifier for the IP NFT. contract IPRecordRegistry is IIPRecordRegistry { + /// @notice Attributes for the IP record type. + struct Record { + // Metadata provider for Story Protocol canonicalized metadata. + address metadataProvider; + // IP translator for custom IP record types. + address resolver; + } + /// @notice Gets the factory contract used for IP account creation. IIPAccountRegistry public immutable IP_ACCOUNT_REGISTRY; @@ -28,8 +37,8 @@ contract IPRecordRegistry is IIPRecordRegistry { /// @notice Tracks the total number of IP records in existence. uint256 public totalSupply = 0; - /// @dev Maps an IP, identified by its IP ID, to a metadata resolver. - mapping(address => address) internal _resolvers; + /// @dev Maps an IP, identified by its IP ID, to an IP record. + mapping(address => Record) internal _records; /// @notice Restricts calls to only originate from a protocol-authorized caller. modifier onlyRegistrationModule() { @@ -61,7 +70,7 @@ contract IPRecordRegistry is IIPRecordRegistry { /// @param id The canonical identifier for the IP. /// @return Whether the IP was registered into the protocol. function isRegistered(address id) external view returns (bool) { - return _resolvers[id] != address(0); + return _records[id].resolver != address(0); } /// @notice Checks whether an IP was registered based on its NFT attributes. @@ -69,16 +78,24 @@ contract IPRecordRegistry is IIPRecordRegistry { /// @param tokenContract The address of the NFT. /// @param tokenId The token identifier of the NFT. /// @return Whether the NFT was registered into the protocol as IP. + /// TODO: Deprecate this in favor of solely using IP IDs for registration identification. function isRegistered(uint256 chainId, address tokenContract, uint256 tokenId) external view returns (bool) { address id = ipId(chainId, tokenContract, tokenId); - return _resolvers[id] != address(0); + return _records[id].resolver != address(0); } /// @notice Gets the resolver bound to an IP based on its ID. /// @param id The canonical identifier for the IP. /// @return The IP resolver address if registered, else the zero address. function resolver(address id) external view returns (address) { - return _resolvers[id]; + return _records[id].resolver; + } + + /// @notice Gets the metadata linked to an IP based on its ID. + /// @param id The canonical identifier for the IP. + /// @return The metadata that was bound to this IP at creation time. + function metadataProvider(address id) external view returns (address) { + return _records[id].metadataProvider; } /// @notice Gets the resolver bound to an IP based on its NFT attributes. @@ -86,25 +103,29 @@ contract IPRecordRegistry is IIPRecordRegistry { /// @param tokenContract The address of the NFT. /// @param tokenId The token identifier of the NFT. /// @return The IP resolver address if registered, else the zero address. + /// TODO: Deprecate this in favor of solely using IP IDs for resolver identification. function resolver(uint256 chainId, address tokenContract, uint256 tokenId) external view returns (address) { address id = ipId(chainId, tokenContract, tokenId); - return _resolvers[id]; + return _records[id].resolver; } /// @notice Registers an NFT as an IP, creating a corresponding IP record. /// @param chainId The chain identifier of where the NFT resides. /// @param tokenContract The address of the NFT. /// @param tokenId The token identifier of the NFT. + /// @param resolverAddr The address of the resolver to associate with the IP. /// @param createAccount Whether to create an IP account when registering. + /// @param provider The metadata provider to associate with the IP. function register( uint256 chainId, address tokenContract, uint256 tokenId, address resolverAddr, - bool createAccount + bool createAccount, + address provider ) external onlyRegistrationModule returns (address account) { address id = ipId(chainId, tokenContract, tokenId); - if (_resolvers[id] != address(0)) { + if (_records[id].resolver != address(0)) { revert Errors.IPRecordRegistry_AlreadyRegistered(); } @@ -116,8 +137,9 @@ contract IPRecordRegistry is IIPRecordRegistry { _createIPAccount(chainId, tokenContract, tokenId); } _setResolver(id, resolverAddr); + _setMetadataProvider(id, provider); totalSupply++; - emit IPRegistered(id, chainId, tokenContract, tokenId, resolverAddr); + emit IPRegistered(id, chainId, tokenContract, tokenId, resolverAddr, provider); } /// @notice Creates the IP account for the specified IP. @@ -128,6 +150,8 @@ contract IPRecordRegistry is IIPRecordRegistry { /// @param chainId The chain identifier of where the NFT resides. /// @param tokenContract The address of the NFT. /// @param tokenId The token identifier of the NFT. + /// TODO: Deprecate this in favor of relying on creation via a periphery + /// contract or through the IPAccountRegistry directly. function createIPAccount(uint256 chainId, address tokenContract, uint256 tokenId) external returns (address) { address account = IP_ACCOUNT_REGISTRY.ipAccount(chainId, tokenContract, tokenId); // TODO: Finalize disambiguation between IP accounts and IP identifiers. @@ -137,11 +161,24 @@ contract IPRecordRegistry is IIPRecordRegistry { return _createIPAccount(chainId, tokenContract, tokenId); } + /// @notice Sets the underlying metadata provider for an IP asset. + /// @param id The canonical ID of the IP. + /// @param provider Address of the metadata provider associated with the IP. + function setMetadataProvider(address id, address provider) external onlyRegistrationModule { + // Metadata may not be set unless the IP was registered into the protocol. + if (_records[id].resolver == address(0)) { + revert Errors.IPRecordRegistry_NotYetRegistered(); + } + _setMetadataProvider(id, provider); + } + /// @notice Sets the resolver for an IP based on its NFT attributes. /// @param chainId The chain identifier of where the NFT resides. /// @param tokenContract The address of the NFT. /// @param tokenId The token identifier of the NFT. /// @param resolverAddr The address of the resolver being set. + /// TODO: Deprecate this in favor of relying solely on IP ids for settings + /// and instead including this in a periphery contract. function setResolver( uint256 chainId, address tokenContract, @@ -160,7 +197,7 @@ contract IPRecordRegistry is IIPRecordRegistry { revert Errors.IPRecordRegistry_ResolverInvalid(); } // Resolvers may not be set unless the IP was registered into the protocol. - if (_resolvers[id] == address(0)) { + if (_records[id].resolver == address(0)) { revert Errors.IPRecordRegistry_NotYetRegistered(); } _setResolver(id, resolverAddr); @@ -183,7 +220,15 @@ contract IPRecordRegistry is IIPRecordRegistry { /// @param id The canonical identifier for the specified IP. /// @param resolverAddr The address of the IP resolver. function _setResolver(address id, address resolverAddr) internal { - _resolvers[id] = resolverAddr; + _records[id].resolver = resolverAddr; emit IPResolverSet(id, resolverAddr); } + + /// @dev Sets the metadata for the specified IP. + /// @param id The canonical identifier for the specified IP. + /// @param provider The metadata provider to associated with the IP. + function _setMetadataProvider(address id, address provider) internal { + _records[id].metadataProvider = provider; + emit MetadataProviderSet(id, provider); + } } diff --git a/contracts/registries/LicenseRegistry.sol b/contracts/registries/LicenseRegistry.sol index d130dabe3..eef3f622a 100644 --- a/contracts/registries/LicenseRegistry.sol +++ b/contracts/registries/LicenseRegistry.sol @@ -114,7 +114,10 @@ contract LicenseRegistry is ERC1155, ILicenseRegistry { } /// Returns framework for id. Reverts if not found - function frameworkParams(uint256 frameworkId, Licensing.ParamVerifierType pvt) public view returns (Licensing.Parameter[] memory) { + function frameworkParams( + uint256 frameworkId, + Licensing.ParamVerifierType pvt + ) public view returns (Licensing.Parameter[] memory) { Licensing.Framework storage fw = _framework(frameworkId); return fw.parameters[pvt]; } @@ -307,10 +310,7 @@ contract LicenseRegistry is ERC1155, ILicenseRegistry { } Licensing.Policy memory pol = policy(policyId); _verifyParams(Licensing.ParamVerifierType.Mint, pol, receiver, amount); - Licensing.License memory licenseData = Licensing.License({ - policyId: policyId, - licensorIpIds: licensorIpIds - }); + Licensing.License memory licenseData = Licensing.License({ policyId: policyId, licensorIpIds: licensorIpIds }); (uint256 lId, bool isNew) = _addIdOrGetExisting(abi.encode(licenseData), _hashedLicenses, _totalLicenses); licenseId = lId; if (isNew) { @@ -348,7 +348,6 @@ contract LicenseRegistry is ERC1155, ILicenseRegistry { address childIpId, address holder ) external onlyLicensee(licenseId, holder) { - // TODO: check if childIpId exists and is owned by holder Licensing.License memory licenseData = _licenses[licenseId]; address[] memory parents = licenseData.licensorIpIds; @@ -359,7 +358,7 @@ contract LicenseRegistry is ERC1155, ILicenseRegistry { Licensing.Policy memory pol = policy(licenseData.policyId); _verifyParams(Licensing.ParamVerifierType.LinkParent, pol, holder, 1); - + // Add policy to kid // TODO: return index of policy in ipId? _addPolictyIdToIp(childIpId, licenseData.policyId, true); @@ -398,26 +397,34 @@ contract LicenseRegistry is ERC1155, ILicenseRegistry { return _licenses[licenseId]; } - function _update(address from, address to, uint256[] memory ids, uint256[] memory values) virtual override internal { + function _update( + address from, + address to, + uint256[] memory ids, + uint256[] memory values + ) internal virtual override { // We are interested in transfers, minting and burning are checked in mintLicense and linkIpToParent respectively. - - - + if (from != address(0) && to != address(0)) { uint256 length = ids.length; for (uint256 i = 0; i < length; i++) { _verifyParams(Licensing.ParamVerifierType.Transfer, policyForLicense(ids[i]), to, values[i]); - } + } } super._update(from, to, ids, values); } - function _verifyParams(Licensing.ParamVerifierType pvt, Licensing.Policy memory pol, address holder, uint256 amount) internal { + function _verifyParams( + Licensing.ParamVerifierType pvt, + Licensing.Policy memory pol, + address holder, + uint256 amount + ) internal { Licensing.Framework storage fw = _framework(pol.frameworkId); Licensing.Parameter[] storage params = fw.parameters[pvt]; uint256 paramsLength = params.length; bytes[] memory values = pol.getValues(pvt); - + for (uint256 i = 0; i < paramsLength; i++) { Licensing.Parameter memory param = params[i]; // Empty bytes => use default value specified in license framework creation params. @@ -439,6 +446,5 @@ contract LicenseRegistry is ERC1155, ILicenseRegistry { } } - // TODO: tokenUri from parameters, from a metadata resolver contract } diff --git a/contracts/registries/metadata/IPAssetRenderer.sol b/contracts/registries/metadata/IPAssetRenderer.sol new file mode 100644 index 000000000..8a1df8b8b --- /dev/null +++ b/contracts/registries/metadata/IPAssetRenderer.sol @@ -0,0 +1,174 @@ +// SPDX-License-Identifier: UNLICENSED +// See https://github.com/storyprotocol/protocol-contracts/blob/main/StoryProtocol-AlphaTestingAgreement-17942166.3.pdf +pragma solidity ^0.8.23; + +import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; +import { Base64 } from "@openzeppelin/contracts/utils/Base64.sol"; +import { IP } from "contracts/lib/IP.sol"; +import { IIPAccount } from "contracts/interfaces/IIPAccount.sol"; +import { IPRecordRegistry } from "contracts/registries/IPRecordRegistry.sol"; +import { IMetadataProvider } from "contracts/interfaces/registries/metadata/IMetadataProvider.sol"; +import { LicenseRegistry } from "contracts/registries/LicenseRegistry.sol"; +import { TaggingModule } from "contracts/modules/tagging/TaggingModule.sol"; +import { RoyaltyModule } from "contracts/modules/royalty-module/RoyaltyModule.sol"; + +/// @title IP Asset Renderer +/// @notice The IP asset renderer is responsible for rendering canonical +/// metadata associated with each IP asset. This includes generation +/// of attributes, on-chain SVGs, and external URLs. Note that the +/// underlying data being rendered is strictly immutable. +contract IPAssetRenderer { + /// @notice The global IP record registry. + IPRecordRegistry public immutable IP_RECORD_REGISTRY; + + /// @notice The global licensing registry. + LicenseRegistry public immutable LICENSE_REGISTRY; + + // Modules storing attribution related to IPs. + TaggingModule public immutable TAGGING_MODULE; + RoyaltyModule public immutable ROYALTY_MODULE; + + /// @notice Initializes the IP asset renderer. + /// TODO: Add different customization options - e.g. font, colorways, etc. + /// TODO: Add an external URL for generating SP-branded links for each IP. + constructor( + address recordRegistry, + address licenseRegistry, + address taggingModule, + address royaltyModule + ) { + IP_RECORD_REGISTRY = IPRecordRegistry(recordRegistry); + LICENSE_REGISTRY = LicenseRegistry(licenseRegistry); + TAGGING_MODULE = TaggingModule(taggingModule); + ROYALTY_MODULE = RoyaltyModule(royaltyModule); + } + + // TODO: Add contract URI support for metadata about the entire IP registry. + + // TODO: Add rendering functions around licensing information. + + // TODO: Add rendering functions around royalties information. + + // TODO: Add rendering functions around tagging information. + + /// @notice Fetches the canonical name associated with the specified IP. + /// @param ipId The canonical ID of the specified IP. + function name(address ipId) external view returns (string memory) { + IP.Metadata memory metadata = _metadata(ipId); + return metadata.name; + } + + /// @notice Fetches the canonical description associated with the IP. + /// @param ipId The canonical ID of the specified IP. + /// @return The string descriptor of the IP. + /// TODO: Add more information related to licensing or royalties. + /// TODO: Update the description to an SP base URL if external URL not set. + function description(address ipId) public view returns (string memory) { + IP.Metadata memory metadata = _metadata(ipId); + return string.concat( + metadata.name, + ", IP #", + Strings.toHexString(ipId), + ", is currently owned by", + Strings.toHexString(owner(ipId)), + ". To learn more about this IP, visit ", + metadata.uri + ); + } + + /// @notice Fetches the keccak-256 content hash associated with the specified IP. + /// @param ipId The canonical ID of the specified IP. + /// @return The bytes32 content hash of the IP. + function hash(address ipId) external view returns (bytes32) { + IP.Metadata memory metadata = _metadata(ipId); + return metadata.hash; + } + + /// @notice Fetches the date of registration of the IP. + /// @param ipId The canonical ID of the specified IP. + function registrationDate(address ipId) external view returns (uint64) { + IP.Metadata memory metadata = _metadata(ipId); + return metadata.registrationDate; + } + + /// @notice Fetches the initial registrant of the IP. + /// @param ipId The canonical ID of the specified IP. + function registrant(address ipId) external view returns (address) { + IP.Metadata memory metadata = _metadata(ipId); + return metadata.registrant; + } + + /// @notice Fetches the external URL associated with the IP. + /// @param ipId The canonical ID of the specified IP. + function uri(address ipId) external view returns (string memory) { + IP.Metadata memory metadata = _metadata(ipId); + return metadata.uri; + } + + /// @notice Fetches the current owner of the IP. + /// @param ipId The canonical ID of the specified IP. + function owner(address ipId) public view returns (address) { + return IIPAccount(payable(ipId)).owner(); + } + + /// @notice Generates a JSON of all metadata attribution related to the IP. + /// TODO: Make this ERC-721 compatible, so that the IP registry may act as + /// an account-bound ERC-721 that points to this function for metadata. + /// TODO: Add SVG support. + /// TODO: Add licensing, royalties, and tagging information support. + function tokenURI(address ipId) external view returns (string memory) { + IP.Metadata memory metadata = _metadata(ipId); + string memory baseJson = string( + /* solhint-disable */ + abi.encodePacked( + '{"name": "IP Asset #', + Strings.toHexString(ipId), + '", "description": "', + description(ipId), + '", "attributes": [' + ) + /* solhint-enable */ + ); + + string memory ipAttributes = string( + /* solhint-disable */ + abi.encodePacked( + '{"trait_type": "Name", "value": "', + metadata.name, + '"},' + '{"trait_type": "Owner", "value": "', + Strings.toHexString(owner(ipId)), + '"},' + '{"trait_type": "Registrant", "value": "', + Strings.toHexString(uint160(metadata.registrant), 20), + '"},', + '{"trait_type": "Hash", "value": "', + Strings.toHexString(uint256(metadata.hash), 32), + '"},', + '{"trait_type": "Registration Date", "value": "', + Strings.toString(metadata.registrationDate), + '"}' + ) + /* solhint-enable */ + ); + + return + string( + abi.encodePacked( + "data:application/json;base64,", + Base64.encode(bytes(string(abi.encodePacked(baseJson, ipAttributes, "]}")))) + ) + ); + } + + /// TODO: Add SVG generation support for branding within token metadata. + + /// @dev Internal function for fetching the metadata tied to an IP record. + function _metadata(address ipId) internal view returns (IP.Metadata memory metadata) { + IMetadataProvider provider = IMetadataProvider(IP_RECORD_REGISTRY.metadataProvider(ipId)); + bytes memory data = provider.getMetadata(ipId); + if (data.length != 0) { + metadata = abi.decode(data, (IP.Metadata)); + } + } +} diff --git a/contracts/registries/metadata/IPMetadataProvider.sol b/contracts/registries/metadata/IPMetadataProvider.sol new file mode 100644 index 000000000..8ae14a6cd --- /dev/null +++ b/contracts/registries/metadata/IPMetadataProvider.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: UNLICENSED +// See https://github.com/storyprotocol/protocol-contracts/blob/main/StoryProtocol-AlphaTestingAgreement-17942166.3.pdf +pragma solidity ^0.8.23; + +import { REGISTRATION_MODULE_KEY } from "contracts/lib/modules/Module.sol"; +import { IMetadataProvider } from "contracts/interfaces/registries/metadata/IMetadataProvider.sol"; +import { IModuleRegistry } from "contracts/interfaces/registries/IModuleRegistry.sol"; +import { Errors } from "contracts/lib/Errors.sol"; + +/// @title IP Metadata Provider Contract +/// @notice Base contract used for customization of canonical IP metadata. +contract IPMetadataProvider is IMetadataProvider { + + /// @notice Gets the protocol-wide module registry. + IModuleRegistry public immutable MODULE_REGISTRY; + + /// @notice Maps IPs to their metadata based on their IP IDs. + mapping(address ip => bytes metadata) internal _ipMetadata; + + /// @notice Restricts calls to only originate from a protocol-authorized caller. + modifier onlyRegistrationModule() { + if (address(MODULE_REGISTRY.getModule(REGISTRATION_MODULE_KEY)) != msg.sender) { + revert Errors.MetadataProvider_Unauthorized(); + } + _; + } + + /// @notice Initializes the metadata provider contract. + /// @param moduleRegistry Gets the protocol-wide module registry. + constructor(address moduleRegistry) { + MODULE_REGISTRY = IModuleRegistry(moduleRegistry); + } + + /// @notice Gets the IP metadata associated with an IP asset based on its IP ID. + /// @param ipId The IP id of the target IP asset. + function getMetadata(address ipId) external view virtual override returns (bytes memory) { + return _ipMetadata[ipId]; + } + + /// @notice Sets the IP metadata associated with an IP asset based on its IP ID. + /// @param ipId The IP id of the IP asset to set metadata for. + /// @param metadata The metadata in bytes to set for the IP asset. + function setMetadata(address ipId, bytes memory metadata) external onlyRegistrationModule { + _ipMetadata[ipId] = metadata; + } +} diff --git a/contracts/resolvers/IPMetadataResolver.sol b/contracts/resolvers/IPMetadataResolver.sol deleted file mode 100644 index fb76323e7..000000000 --- a/contracts/resolvers/IPMetadataResolver.sol +++ /dev/null @@ -1,209 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -// See https://github.com/storyprotocol/protocol-contracts/blob/main/StoryProtocol-AlphaTestingAgreement-17942166.3.pdf -pragma solidity ^0.8.21; - -import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; -import { Base64 } from "@openzeppelin/contracts/utils/Base64.sol"; -import { IResolver } from "contracts/interfaces/resolvers/IResolver.sol"; -import { ResolverBase } from "./ResolverBase.sol"; -import { BaseModule } from "contracts/modules/BaseModule.sol"; -import { IModule } from "contracts/interfaces/modules/base/IModule.sol"; -import { IIPMetadataResolver } from "contracts/interfaces/resolvers/IIPMetadataResolver.sol"; -import { IIPAccount } from "contracts/interfaces/IIPAccount.sol"; -import { Errors } from "contracts/lib/Errors.sol"; -import { IP } from "contracts/lib/IP.sol"; -import { METADATA_RESOLVER_MODULE_KEY } from "contracts/lib/modules/Module.sol"; - -/// @title IP Metadata Resolver -/// @notice Canonical IP resolver contract used for Story Protocol. This will -/// likely change to a separate contract that extends IPMetadataResolver -/// in the near future. -contract IPMetadataResolver is IIPMetadataResolver, ResolverBase { - /// @dev Maps IP to their metadata records based on their canonical IDs. - mapping(address => IP.MetadataRecord) public _records; - - /// @notice Initializes the IP metadata resolver. - /// @param accessController The access controller used for IP authorization. - /// @param ipRecordRegistry The address of the IP record registry. - /// @param ipAccountRegistry The address of the IP account registry. - /// @param licenseRegistry The address of the license registry. - constructor( - address accessController, - address ipRecordRegistry, - address ipAccountRegistry, - address licenseRegistry - ) ResolverBase(accessController, ipRecordRegistry, ipAccountRegistry, licenseRegistry) {} - - /// @notice Fetches all metadata associated with the specified IP. - /// @param ipId The canonical ID of the specified IP. - function metadata(address ipId) public view returns (IP.Metadata memory) { - IP.MetadataRecord memory record = _records[ipId]; - return - IP.Metadata({ - owner: owner(ipId), - name: record.name, - description: record.description, - hash: record.hash, - registrationDate: record.registrationDate, - registrant: record.registrant, - uri: uri(ipId) - }); - } - - /// @notice Fetches the canonical name associated with the specified IP. - /// @param ipId The canonical ID of the specified IP. - function name(address ipId) external view returns (string memory) { - return _records[ipId].name; - } - - /// @notice Fetches the description associated with the specified IP. - /// @param ipId The canonical ID of the specified IP. - /// @return The string descriptor of the IP. - function description(address ipId) external view returns (string memory) { - return _records[ipId].description; - } - - /// @notice Fetches the keccak-256 hash associated with the specified IP. - /// @param ipId The canonical ID of the specified IP. - /// @return The bytes32 content hash of the IP. - function hash(address ipId) external view returns (bytes32) { - return _records[ipId].hash; - } - - /// @notice Fetches the date of registration of the IP. - /// @param ipId The canonical ID of the specified IP. - function registrationDate(address ipId) external view returns (uint64) { - return _records[ipId].registrationDate; - } - - /// @notice Fetches the initial registrant of the IP. - /// @param ipId The canonical ID of the specified IP. - function registrant(address ipId) external view returns (address) { - return _records[ipId].registrant; - } - - /// @notice Fetches the current owner of the IP. - /// @param ipId The canonical ID of the specified IP. - function owner(address ipId) public view returns (address) { - if (!IP_RECORD_REGISTRY.isRegistered(ipId)) { - return address(0); - } - return IIPAccount(payable(ipId)).owner(); - } - - /// @notice Fetches an IP owner defined URI associated with the IP. - /// @param ipId The canonical ID of the specified IP. - function uri(address ipId) public view returns (string memory) { - if (!IP_RECORD_REGISTRY.isRegistered(ipId)) { - return ""; - } - - IP.MetadataRecord memory record = _records[ipId]; - string memory ipUri = record.uri; - - if (bytes(ipUri).length > 0) { - return ipUri; - } - - return _defaultTokenURI(ipId, record); - } - - /// @notice Sets metadata associated with an IP. - /// @param ipId The canonical ID of the specified IP. - /// @param newMetadata The new metadata to set for the IP. - function setMetadata(address ipId, IP.MetadataRecord calldata newMetadata) external onlyAuthorized(ipId) { - _records[ipId] = newMetadata; - emit IPMetadataResolverSetRecord(ipId, newMetadata); - } - - /// @notice Sets the name associated with an IP. - /// @param ipId The canonical ID of the specified IP. - /// @param newName The new string name to associate with the IP. - function setName(address ipId, string calldata newName) external onlyAuthorized(ipId) { - _records[ipId].name = newName; - emit IPMetadataResolverSetName(ipId, newName); - } - - /// @notice Sets the description associated with an IP. - /// @param ipId The canonical ID of the specified IP. - /// @param newDescription The string description to associate with the IP. - function setDescription(address ipId, string calldata newDescription) external onlyAuthorized(ipId) { - _records[ipId].description = newDescription; - emit IPMetadataResolverSetDescription(ipId, newDescription); - } - - /// @notice Sets the keccak-256 hash associated with an IP. - /// @param ipId The canonical ID of the specified IP. - /// @param newHash The keccak-256 hash to associate with the IP. - function setHash(address ipId, bytes32 newHash) external onlyAuthorized(ipId) { - _records[ipId].hash = newHash; - emit IPMetadataResolverSetHash(ipId, newHash); - } - - /// @notice Sets an IP owner defined URI to associate with the IP. - /// @param ipId The canonical ID of the specified IP. - /// @param newURI The new token URI to set for the IP. - function setURI(address ipId, string calldata newURI) external onlyAuthorized(ipId) { - _records[ipId].uri = newURI; - emit IPMetadataResolverSetURI(ipId, newURI); - } - - /// @notice Checks whether the resolver interface is supported. - /// @param id The resolver interface identifier. - /// @return Whether the resolver interface is supported. - function supportsInterface(bytes4 id) public view virtual override(IResolver, ResolverBase) returns (bool) { - return id == type(IIPMetadataResolver).interfaceId || super.supportsInterface(id); - } - - /// @notice Gets the protocol-wide module identifier for this module. - function name() public pure override(BaseModule, IModule) returns (string memory) { - return METADATA_RESOLVER_MODULE_KEY; - } - - /// @dev Internal function for generating a default IP URI if not provided. - /// @param ipId The canonical ID of the specified IP. - /// @param record The IP record associated with the IP. - function _defaultTokenURI(address ipId, IP.MetadataRecord memory record) internal view returns (string memory) { - string memory baseJson = string( - /* solhint-disable */ - abi.encodePacked( - '{"name": "IP Asset #', - Strings.toHexString(ipId), - '", "description": "', - record.description, - '", "attributes": [' - ) - /* solhint-enable */ - ); - - string memory ipAttributes = string( - /* solhint-disable */ - abi.encodePacked( - '{"trait_type": "Name", "value": "', - record.name, - '"},' - '{"trait_type": "Owner", "value": "', - Strings.toHexString(uint160(owner(ipId)), 20), - '"},' - '{"trait_type": "Registrant", "value": "', - Strings.toHexString(uint160(record.registrant), 20), - '"},', - '{"trait_type": "Hash", "value": "', - Strings.toHexString(uint256(record.hash), 32), - '"},', - '{"trait_type": "Registration Date", "value": "', - Strings.toString(record.registrationDate), - '"}' - ) - /* solhint-enable */ - ); - - return - string( - abi.encodePacked( - "data:application/json;base64,", - Base64.encode(bytes(string(abi.encodePacked(baseJson, ipAttributes, "]}")))) - ) - ); - } -} diff --git a/contracts/resolvers/IPResolver.sol b/contracts/resolvers/IPResolver.sol new file mode 100644 index 000000000..bfb468d2a --- /dev/null +++ b/contracts/resolvers/IPResolver.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: UNLICENSED +// See https://github.com/storyprotocol/protocol-contracts/blob/main/StoryProtocol-AlphaTestingAgreement-17942166.3.pdf +pragma solidity ^0.8.23; + +import { IResolver } from "contracts/interfaces/resolvers/IResolver.sol"; +import { ResolverBase } from "./ResolverBase.sol"; +import { BaseModule } from "contracts/modules/BaseModule.sol"; +import { IModule } from "contracts/interfaces/modules/base/IModule.sol"; +import { IKeyValueResolver } from "contracts/interfaces/resolvers/IKeyValueResolver.sol"; +import { IIPAccount } from "contracts/interfaces/IIPAccount.sol"; +import { KeyValueResolver } from "contracts/resolvers/KeyValueResolver.sol"; +import { Errors } from "contracts/lib/Errors.sol"; +import { IP } from "contracts/lib/IP.sol"; +import { IP_RESOLVER_MODULE_KEY } from "contracts/lib/modules/Module.sol"; + +/// @title IP Resolver +/// @notice Canonical IP resolver contract used for Story Protocol. +/// TODO: Add support for interface resolvers, where one can add a contract +/// and supported interface (address, interfaceId) to tie to an IP asset. +/// TODO: Add support for multicall, so multiple records may be set at once. +contract IPResolver is KeyValueResolver { + /// @notice Initializes the IP metadata resolver. + /// @param accessController The access controller used for IP authorization. + /// @param ipRecordRegistry The address of the IP record registry. + /// @param ipAccountRegistry The address of the IP account registry. + /// @param licenseRegistry The address of the license registry. + constructor( + address accessController, + address ipRecordRegistry, + address ipAccountRegistry, + address licenseRegistry + ) ResolverBase(accessController, ipRecordRegistry, ipAccountRegistry, licenseRegistry) {} + + /// @notice Checks whether the resolver interface is supported. + /// @param id The resolver interface identifier. + /// @return Whether the resolver interface is supported. + function supportsInterface(bytes4 id) public view virtual override returns (bool) { + return super.supportsInterface(id); + } + + /// @notice Gets the protocol-wide module identifier for this module. + function name() public pure override(BaseModule, IModule) returns (string memory) { + return IP_RESOLVER_MODULE_KEY; + } +} diff --git a/contracts/resolvers/KeyValueResolver.sol b/contracts/resolvers/KeyValueResolver.sol new file mode 100644 index 000000000..892b21d71 --- /dev/null +++ b/contracts/resolvers/KeyValueResolver.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: UNLICENSED +// See https://github.com/storyprotocol/protocol-contracts/blob/main/StoryProtocol-AlphaTestingAgreement-17942166.3.pdf +pragma solidity ^0.8.23; + +import { IKeyValueResolver } from "contracts/interfaces/resolvers/IKeyValueResolver.sol"; +import { ResolverBase } from "contracts/resolvers/ResolverBase.sol"; + +/// @title Key Value Resolver +/// @notice Resolver used for returning values associated with keys. This is the +/// preferred approach for adding additional attribution to IP that the +/// IP originator thinks is beneficial to have on chain. +abstract contract KeyValueResolver is IKeyValueResolver, ResolverBase { + /// @dev Stores key-value pairs associated with each IP. + mapping(address => mapping(string => string)) internal _values; + + /// @notice Sets the string value for a specified key of an IP ID. + /// @param ipId The canonical identifier of the IP asset. + /// @param k The string parameter key to update. + /// @param v The value to set for the specified key. + function setValue(address ipId, string calldata k, string calldata v) external virtual onlyAuthorized(ipId) { + _values[ipId][k] = v; + emit KeyValueSet(ipId, k, v); + } + + /// @notice Retrieves the string value associated with a key for an IP asset. + /// @param k The string parameter key to query. + function value(address ipId, string calldata k) external view virtual returns (string memory) { + return _values[ipId][k]; + } + + /// @notice Checks whether the resolver interface is supported. + /// @param id The resolver interface identifier. + /// @return Whether the resolver interface is supported. + function supportsInterface(bytes4 id) public view virtual override returns (bool) { + return id == type(IKeyValueResolver).interfaceId || super.supportsInterface(id); + } +} diff --git a/contracts/resolvers/ResolverBase.sol b/contracts/resolvers/ResolverBase.sol index 99e5a6050..215962648 100644 --- a/contracts/resolvers/ResolverBase.sol +++ b/contracts/resolvers/ResolverBase.sol @@ -7,13 +7,17 @@ import { IResolver } from "contracts/interfaces/resolvers/IResolver.sol"; /// @notice IP Resolver Base Contract abstract contract ResolverBase is IResolver, BaseModule { - /// @notice Initializes the base module contract. /// @param controller The access controller used for IP authorization. /// @param recordRegistry The address of the IP record registry. /// @param accountRegistry The address of the IP account registry. /// @param licenseRegistry The address of the license registry. - constructor(address controller, address recordRegistry, address accountRegistry, address licenseRegistry) BaseModule(controller, recordRegistry, accountRegistry, licenseRegistry) {} + constructor( + address controller, + address recordRegistry, + address accountRegistry, + address licenseRegistry + ) BaseModule(controller, recordRegistry, accountRegistry, licenseRegistry) {} /// @notice Checks whether the resolver interface is supported. /// @param id The resolver interface identifier. diff --git a/contracts/utils/ShortStringOps.sol b/contracts/utils/ShortStringOps.sol index 35cf81edb..71257444f 100644 --- a/contracts/utils/ShortStringOps.sol +++ b/contracts/utils/ShortStringOps.sol @@ -43,6 +43,4 @@ library ShortStringOps { } } -library EnumerableShortStringSet { - -} \ No newline at end of file +library EnumerableShortStringSet {} diff --git a/script/foundry/deployment/Main.s.sol b/script/foundry/deployment/Main.s.sol index 3823e2333..67d3875cb 100644 --- a/script/foundry/deployment/Main.s.sol +++ b/script/foundry/deployment/Main.s.sol @@ -13,15 +13,17 @@ import { AccessController } from "contracts/AccessController.sol"; import { IPAccountImpl } from "contracts/IPAccountImpl.sol"; import { IIPAccount } from "contracts/interfaces/IIPAccount.sol"; import { Errors } from "contracts/lib/Errors.sol"; +import { IPMetadataProvider } from "contracts/registries/metadata/IPMetadataProvider.sol"; import { IPAccountRegistry } from "contracts/registries/IPAccountRegistry.sol"; import { IPRecordRegistry } from "contracts/registries/IPRecordRegistry.sol"; +import { IPAssetRenderer } from "contracts/registries/metadata/IPAssetRenderer.sol"; import { ModuleRegistry } from "contracts/registries/ModuleRegistry.sol"; import { LicenseRegistry } from "contracts/registries/LicenseRegistry.sol"; import { RegistrationModule } from "contracts/modules/RegistrationModule.sol"; import { TaggingModule } from "contracts/modules/tagging/TaggingModule.sol"; import { RoyaltyModule } from "contracts/modules/royalty-module/RoyaltyModule.sol"; import { DisputeModule } from "contracts/modules/dispute-module/DisputeModule.sol"; -import { IPMetadataResolver } from "contracts/resolvers/IPMetadataResolver.sol"; +import { IPResolver } from "contracts/resolvers/IPResolver.sol"; // script import { StringUtil } from "script/foundry/utils/StringUtil.sol"; @@ -35,6 +37,8 @@ contract Main is Script, BroadcastManager, JsonDeploymentHandler { address public constant ERC6551_REGISTRY = address(0x000000006551c19487814612e58FE06813775758); AccessController public accessController; + IPAssetRenderer public renderer; + IPMetadataProvider public metadataProvider; IPAccountRegistry public ipAccountRegistry; IPRecordRegistry public ipRecordRegistry; LicenseRegistry public licenseRegistry; @@ -50,7 +54,7 @@ contract Main is Script, BroadcastManager, JsonDeploymentHandler { TaggingModule taggingModule; RoyaltyModule royaltyModule; DisputeModule disputeModule; - IPMetadataResolver ipMetadataResolver; + IPResolver ipResolver; constructor() JsonDeploymentHandler("main") {} @@ -107,15 +111,20 @@ contract Main is Script, BroadcastManager, JsonDeploymentHandler { ipRecordRegistry = new IPRecordRegistry(address(moduleRegistry), address(ipAccountRegistry)); _postdeploy(contractKey, address(ipRecordRegistry)); - contractKey = "IPMetadataResolver"; + contractKey = "IPResolver"; _predeploy(contractKey); - ipMetadataResolver = new IPMetadataResolver( + ipResolver = new IPResolver( address(accessController), address(ipRecordRegistry), address(ipAccountRegistry), address(licenseRegistry) ); - _postdeploy(contractKey, address(ipMetadataResolver)); + _postdeploy(contractKey, address(ipResolver)); + + contractKey = "MetadataProvider"; + _predeploy(contractKey); + metadataProvider = new IPMetadataProvider(address(moduleRegistry)); + _postdeploy(contractKey, address(metadataProvider)); contractKey = "RegistrationModule"; _predeploy(contractKey); @@ -124,7 +133,8 @@ contract Main is Script, BroadcastManager, JsonDeploymentHandler { address(ipRecordRegistry), address(ipAccountRegistry), address(licenseRegistry), - address(ipMetadataResolver) + address(ipResolver), + address(metadataProvider) ); _postdeploy(contractKey, address(registrationModule)); @@ -143,6 +153,17 @@ contract Main is Script, BroadcastManager, JsonDeploymentHandler { disputeModule = new DisputeModule(); _postdeploy(contractKey, address(disputeModule)); + contractKey = "IPAssetRenderer"; + _predeploy(contractKey); + renderer = new IPAssetRenderer( + address(ipRecordRegistry), + address(licenseRegistry), + address(taggingModule), + address(royaltyModule) + ); + _postdeploy(contractKey, address(renderer)); + + // mockModule = new MockModule(address(ipAccountRegistry), address(moduleRegistry), "MockModule"); } diff --git a/test/foundry/IPRecordRegistry.t.sol b/test/foundry/IPRecordRegistry.t.sol index 7f23c2581..b0989356b 100644 --- a/test/foundry/IPRecordRegistry.t.sol +++ b/test/foundry/IPRecordRegistry.t.sol @@ -5,6 +5,7 @@ import { BaseTest } from "./utils/BaseTest.sol"; import { IModuleRegistry } from "contracts/interfaces/registries/IModuleRegistry.sol"; import { IIPRecordRegistry } from "contracts/interfaces/registries/IIPRecordRegistry.sol"; import { IPAccountChecker } from "contracts/lib/registries/IPAccountChecker.sol"; +import { IPMetadataProvider } from "contracts/registries/metadata/IPMetadataProvider.sol"; import { IPAccountRegistry } from "contracts/registries/IPAccountRegistry.sol"; import { ERC6551Registry } from "lib/reference/src/ERC6551Registry.sol"; import { IPRecordRegistry } from "contracts/registries/IPRecordRegistry.sol"; @@ -34,6 +35,9 @@ contract IPRecordRegistryTest is BaseTest { /// @notice The IP account registry used for account creation. IPAccountRegistry public ipAccountRegistry; + /// @notice The IP metadata provider associated with an IP. + IPMetadataProvider public metadataProvider; + /// @notice Mock NFT address for IP registration testing. address public tokenAddress; @@ -54,6 +58,7 @@ contract IPRecordRegistryTest is BaseTest { moduleRegistry = IModuleRegistry( address(new MockModuleRegistry(registrationModule)) ); + metadataProvider = new IPMetadataProvider(address(moduleRegistry)); ipAccountImpl = address(new IPAccountImpl()); ipAccountRegistry = new IPAccountRegistry( erc6551Registry, @@ -124,7 +129,8 @@ contract IPRecordRegistryTest is BaseTest { block.chainid, tokenAddress, tokenId, - resolver + resolver, + address(metadataProvider) ); vm.prank(registrationModule); registry.register( @@ -132,7 +138,8 @@ contract IPRecordRegistryTest is BaseTest { tokenAddress, tokenId, resolver, - true + true, + address(metadataProvider) ); /// Ensures IP record post-registration conditions are met. @@ -168,7 +175,8 @@ contract IPRecordRegistryTest is BaseTest { block.chainid, tokenAddress, tokenId, - resolver + resolver, + address(metadataProvider) ); vm.prank(registrationModule); registry.register( @@ -176,7 +184,8 @@ contract IPRecordRegistryTest is BaseTest { tokenAddress, tokenId, resolver, - false + false, + address(metadataProvider) ); /// Ensures IP record post-registration conditions are met. @@ -188,7 +197,7 @@ contract IPRecordRegistryTest is BaseTest { /// @notice Tests registration of IP records works with existing IP accounts. function test_IPRecordRegistry_RegisterExistingAccount() public { - address ipId = registry.createIPAccount(block.chainid, tokenAddress, tokenId); + registry.createIPAccount(block.chainid, tokenAddress, tokenId); assertTrue(IPAccountChecker.isRegistered(ipAccountRegistry, block.chainid, tokenAddress, tokenId)); vm.prank(registrationModule); registry.register( @@ -196,7 +205,8 @@ contract IPRecordRegistryTest is BaseTest { tokenAddress, tokenId, resolver, - true + true, + address(metadataProvider) ); } @@ -204,22 +214,22 @@ contract IPRecordRegistryTest is BaseTest { /// @notice Tests registration of IP reverts when an IP has already been registered. function test_IPRecordRegistry_Register_Reverts_ExistingRegistration() public { vm.startPrank(registrationModule); - registry.register(block.chainid, tokenAddress, tokenId, resolver, false); + registry.register(block.chainid, tokenAddress, tokenId, resolver, false, address(metadataProvider)); vm.expectRevert(Errors.IPRecordRegistry_AlreadyRegistered.selector); - registry.register(block.chainid, tokenAddress, tokenId, resolver, false); + registry.register(block.chainid, tokenAddress, tokenId, resolver, false, address(metadataProvider)); } /// @notice Tests registration of IP reverts if not called by a registration module. function test_IPRecordRegistry_Register_Reverts_InvalidRegistrationModule() public { vm.expectRevert(Errors.IPRecordRegistry_Unauthorized.selector); vm.prank(alice); - registry.register(block.chainid, tokenAddress, tokenId, resolver, false); + registry.register(block.chainid, tokenAddress, tokenId, resolver, false, address(metadataProvider)); } /// @notice Tests generic IP account creation works. function test_IPRecordRegistry_CreateIPAccount() public { assertTrue(!IPAccountChecker.isRegistered(ipAccountRegistry, block.chainid, tokenAddress, tokenId)); - address ipId = registry.createIPAccount(block.chainid, tokenAddress, tokenId); + registry.createIPAccount(block.chainid, tokenAddress, tokenId); assertTrue(IPAccountChecker.isRegistered(ipAccountRegistry, block.chainid, tokenAddress, tokenId)); } @@ -240,7 +250,7 @@ contract IPRecordRegistryTest is BaseTest { ipAccountRegistry.IP_ACCOUNT_SALT() ); vm.startPrank(registrationModule); - registry.register(block.chainid, tokenAddress, tokenId, resolver, false); + registry.register(block.chainid, tokenAddress, tokenId, resolver, false, address(metadataProvider)); vm.expectEmit(true, true, true, true); emit IIPRecordRegistry.IPResolverSet( diff --git a/test/foundry/modules/ModuleBase.t.sol b/test/foundry/modules/ModuleBase.t.sol index d1f40eb6e..e4e7b8470 100644 --- a/test/foundry/modules/ModuleBase.t.sol +++ b/test/foundry/modules/ModuleBase.t.sol @@ -16,7 +16,6 @@ import { IPAccountRegistry } from "contracts/registries/IPAccountRegistry.sol"; import { MockModuleRegistry } from "test/foundry/mocks/MockModuleRegistry.sol"; import { IIPRecordRegistry } from "contracts/interfaces/registries/IIPRecordRegistry.sol"; import { IPAccountImpl} from "contracts/IPAccountImpl.sol"; -import { IIPMetadataResolver } from "contracts/interfaces/resolvers/IIPMetadataResolver.sol"; import { MockERC721 } from "test/foundry/mocks/MockERC721.sol"; import { IP } from "contracts/lib/IP.sol"; import { Errors } from "contracts/lib/Errors.sol"; diff --git a/test/foundry/modules/RegistrationModule.t.sol b/test/foundry/modules/RegistrationModule.t.sol index 30edb9bcf..364fedf98 100644 --- a/test/foundry/modules/RegistrationModule.t.sol +++ b/test/foundry/modules/RegistrationModule.t.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.23; import { MockAccessController } from "test/foundry/mocks/MockAccessController.sol"; import { BaseTest } from "test/foundry/utils/BaseTest.sol"; import { IModule } from "contracts/interfaces/modules/base/IModule.sol"; +import { IPMetadataProvider } from "contracts/registries/metadata/IPMetadataProvider.sol"; import { ModuleBaseTest } from "./ModuleBase.t.sol"; import { IPAccountChecker } from "contracts/lib/registries/IPAccountChecker.sol"; import { IAccessController } from "contracts/interfaces/IAccessController.sol"; @@ -12,19 +13,19 @@ import { ModuleRegistry } from "contracts/registries/ModuleRegistry.sol"; import { IModuleRegistry } from "contracts/interfaces/registries/IModuleRegistry.sol"; import { IPRecordRegistry } from "contracts/registries/IPRecordRegistry.sol"; import { IPAccountRegistry } from "contracts/registries/IPAccountRegistry.sol"; -import { IPMetadataResolver } from "contracts/resolvers/IPMetadataResolver.sol"; +import { IPAssetRenderer } from "contracts/registries/metadata/IPAssetRenderer.sol"; +import { IPResolver } from "contracts/resolvers/IPResolver.sol"; import { RegistrationModule } from "contracts/modules/RegistrationModule.sol"; import { MockModuleRegistry } from "test/foundry/mocks/MockModuleRegistry.sol"; import { IIPRecordRegistry } from "contracts/interfaces/registries/IIPRecordRegistry.sol"; import { IPAccountImpl} from "contracts/IPAccountImpl.sol"; -import { IIPMetadataResolver } from "contracts/interfaces/resolvers/IIPMetadataResolver.sol"; import { MockERC721 } from "test/foundry/mocks/MockERC721.sol"; import { IParamVerifier } from "contracts/interfaces/licensing/IParamVerifier.sol"; import { MockIParamVerifier } from "test/foundry/mocks/licensing/MockParamVerifier.sol"; import { Licensing } from "contracts/lib/Licensing.sol"; import { IP } from "contracts/lib/IP.sol"; import { Errors } from "contracts/lib/Errors.sol"; -import { METADATA_RESOLVER_MODULE_KEY, REGISTRATION_MODULE_KEY } from "contracts/lib/modules/Module.sol"; +import { IP_RESOLVER_MODULE_KEY, REGISTRATION_MODULE_KEY } from "contracts/lib/modules/Module.sol"; import { IIPAccount } from "contracts/interfaces/IIPAccount.sol"; /// @title IP Registration Module Test Contract @@ -33,14 +34,20 @@ contract RegistrationModuleTest is ModuleBaseTest { // Default IP record attributes. string public constant RECORD_NAME = "IPRecord"; - string public constant RECORD_DESCRIPTION = "IPs all the way down."; bytes32 public constant RECORD_HASH = ""; + string public constant RECORD_URL = "https://ipasset.xyz"; + + /// @notice IP metadata rendering contract. + IPAssetRenderer public renderer; + + /// @notice The Story Protocol default IP resolver. + IPResolver public resolver; /// @notice The registration module SUT. RegistrationModule public registrationModule; - /// @notice Gets the IP metadata resolver tied to the registration module. - IIPMetadataResolver public resolver; + /// @notice Gets the contract responsible for IP metadata provisioning. + IPMetadataProvider public metadataProvider; /// @notice Mock NFT address for IP registration testing. address public tokenAddress; @@ -58,15 +65,22 @@ contract RegistrationModuleTest is ModuleBaseTest { function setUp() public virtual override(ModuleBaseTest) { ModuleBaseTest.setUp(); _initLicensing(); - resolver = new IPMetadataResolver( + resolver = new IPResolver( address(accessController), address(ipRecordRegistry), address(ipAccountRegistry), address(licenseRegistry) ); + metadataProvider = new IPMetadataProvider(address(moduleRegistry)); registrationModule = RegistrationModule(_deployModule()); + renderer = new IPAssetRenderer( + address(ipRecordRegistry), + address(licenseRegistry), + vm.addr(0x1111), // TODO: Incorporate tagging module into renderer. + vm.addr(0x2111) // TODO: Incorporate royalty module into renderer. + ); moduleRegistry.registerModule(REGISTRATION_MODULE_KEY, address(registrationModule)); - moduleRegistry.registerModule(METADATA_RESOLVER_MODULE_KEY, address(resolver)); + moduleRegistry.registerModule(IP_RESOLVER_MODULE_KEY, address(resolver)); MockERC721 erc721 = new MockERC721(); tokenAddress = address(erc721); tokenId = erc721.mintId(alice, 99); @@ -79,7 +93,6 @@ contract RegistrationModuleTest is ModuleBaseTest { assertEq(address(registrationModule.IP_ACCOUNT_REGISTRY()), address(ipAccountRegistry)); assertEq(address(registrationModule.IP_RECORD_REGISTRY()), address(ipRecordRegistry)); assertEq(address(registrationModule.LICENSE_REGISTRY()), address(licenseRegistry)); - assertEq(address(registrationModule.resolver()), address(resolver)); } /// @notice Checks whether root IP registration operates as expected. @@ -102,6 +115,7 @@ contract RegistrationModuleTest is ModuleBaseTest { /// Ensure registered IP postconditiosn are met. assertEq(ipRecordRegistry.resolver(ipId), address(resolver)); + assertEq(ipRecordRegistry.metadataProvider(ipId), address(metadataProvider)); assertEq(totalSupply + 1, ipRecordRegistry.totalSupply()); assertTrue(ipRecordRegistry.isRegistered(ipId)); assertTrue(ipRecordRegistry.isRegistered(block.chainid, tokenAddress, tokenId)); @@ -148,8 +162,8 @@ contract RegistrationModuleTest is ModuleBaseTest { tokenAddress, tokenId2, RECORD_NAME, - RECORD_DESCRIPTION, - RECORD_HASH + RECORD_HASH, + RECORD_URL ); @@ -158,10 +172,9 @@ contract RegistrationModuleTest is ModuleBaseTest { assertEq(totalSupply + 1, ipRecordRegistry.totalSupply()); assertTrue(ipRecordRegistry.isRegistered(ipId2)); assertTrue(ipRecordRegistry.isRegistered(block.chainid, tokenAddress, tokenId2)); - assertEq(resolver.name(ipId2), RECORD_NAME); - assertEq(resolver.description(ipId2), RECORD_DESCRIPTION); - assertEq(resolver.hash(ipId2), RECORD_HASH); - assertEq(resolver.owner(ipId2), bob); + assertEq(renderer.name(ipId2), RECORD_NAME); + assertEq(renderer.hash(ipId2), RECORD_HASH); + assertEq(renderer.owner(ipId2), bob); } /// @notice Checks that registration reverts when called by an invalid owner. @@ -172,8 +185,8 @@ contract RegistrationModuleTest is ModuleBaseTest { tokenAddress, tokenId, RECORD_NAME, - RECORD_DESCRIPTION, - RECORD_HASH + RECORD_HASH, + RECORD_URL ); } @@ -185,7 +198,8 @@ contract RegistrationModuleTest is ModuleBaseTest { address(ipRecordRegistry), address(ipAccountRegistry), address(licenseRegistry), - address(resolver) + address(resolver), + address(metadataProvider) ) ); } diff --git a/test/foundry/registries/metadata/IPAssetRenderer.t.sol b/test/foundry/registries/metadata/IPAssetRenderer.t.sol new file mode 100644 index 000000000..b9214d0e5 --- /dev/null +++ b/test/foundry/registries/metadata/IPAssetRenderer.t.sol @@ -0,0 +1,229 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; +import { Base64 } from "@openzeppelin/contracts/utils/Base64.sol"; + +import { ResolverBaseTest } from "test/foundry/resolvers/ResolverBase.t.sol"; +import { IPResolver } from "contracts/resolvers/IPResolver.sol"; +import { IResolver } from "contracts/interfaces/resolvers/IResolver.sol"; +import { AccessController } from "contracts/AccessController.sol"; +import { ModuleRegistry } from "contracts/registries/ModuleRegistry.sol"; +import { ERC6551Registry } from "lib/reference/src/ERC6551Registry.sol"; +import { IModuleRegistry } from "contracts/interfaces/registries/IModuleRegistry.sol"; +import { IPRecordRegistry } from "contracts/registries/IPRecordRegistry.sol"; +import { RegistrationModule } from "contracts/modules/RegistrationModule.sol"; +import { IPAccountRegistry } from "contracts/registries/IPAccountRegistry.sol"; +import { LicenseRegistry } from "contracts/registries/LicenseRegistry.sol"; +import { MockModuleRegistry } from "test/foundry/mocks/MockModuleRegistry.sol"; +import { IPMetadataProvider } from "contracts/registries/metadata/IPMetadataProvider.sol"; +import { RegistrationModule } from "contracts/modules/RegistrationModule.sol"; +import { IIPRecordRegistry } from "contracts/interfaces/registries/IIPRecordRegistry.sol"; +import { IPAssetRenderer } from "contracts/registries/metadata/IPAssetRenderer.sol"; +import { BaseTest } from "test/foundry/utils/BaseTest.sol"; +import { IPAccountImpl} from "contracts/IPAccountImpl.sol"; +import { MockERC721 } from "test/foundry/mocks/MockERC721.sol"; +import { ModuleBaseTest } from "test/foundry/modules/ModuleBase.t.sol"; +import { IP } from "contracts/lib/IP.sol"; +import { Errors } from "contracts/lib/Errors.sol"; +import { IP_RESOLVER_MODULE_KEY, REGISTRATION_MODULE_KEY } from "contracts/lib/modules/Module.sol"; + +/// @title IP Asset Renderer Test Contract +/// @notice Tests IP asset rendering functionality. +/// TODO: Make this inherit module base to avoid code duplication. +contract IPAssetRendererTest is BaseTest { + + // Module placeholders + // TODO: Mock these out. + address taggingModule = vm.addr(0x1111); + address royaltyModule = vm.addr(0x2222); + + /// @notice Gets the metadata provider used for IP registration. + IPMetadataProvider metadataProvider; + + /// @notice Used for registration in the IP record registry. + RegistrationModule public registrationModule; + + /// @notice Gets the protocol-wide license registry. + LicenseRegistry public licenseRegistry; + + /// @notice Gets the protocol-wide IP record registry. + IPRecordRegistry public ipRecordRegistry; + + /// @notice Gets the protocol-wide module registry. + ModuleRegistry public moduleRegistry; + + /// @notice Gets the protocol-wide IP account registry. + IPAccountRegistry public ipAccountRegistry; + + // Default IP record attributes. + string public constant IP_NAME = "IPRecord"; + string public constant IP_DESCRIPTION = "IPs all the way down."; + bytes32 public constant IP_HASH = ""; + string public constant IP_EXTERNAL_URL = "https://storyprotocol.xyz"; + uint64 public constant IP_REGISTRATION_DATE = uint64(99); + + /// @notice The access controller address. + AccessController public accessController; + + /// @notice The token contract SUT. + IPAssetRenderer public renderer; + + /// @notice The IP resolver. + IPResolver public resolver; + + /// @notice Mock IP identifier for resolver testing. + address public ipId; + + /// @notice Initializes the base token contract for testing. + function setUp() public virtual override(BaseTest) { + BaseTest.setUp(); + // TODO: Create an IP record registry mock instead. + licenseRegistry = new LicenseRegistry(""); + accessController = new AccessController(); + moduleRegistry = new ModuleRegistry(); + MockERC721 erc721 = new MockERC721(); + ipAccountRegistry = new IPAccountRegistry( + address(new ERC6551Registry()), + address(accessController), + address(new IPAccountImpl()) + ); + accessController.initialize(address(ipAccountRegistry), address(moduleRegistry)); + ipRecordRegistry = new IPRecordRegistry( + address(moduleRegistry), + address(ipAccountRegistry) + ); + + vm.prank(alice); + uint256 tokenId = erc721.mintId(alice, 99); + + metadataProvider = new IPMetadataProvider(address(moduleRegistry)); + resolver = new IPResolver( + address(accessController), + address(ipRecordRegistry), + address(ipAccountRegistry), + address(licenseRegistry) + ); + + // TODO: Mock out the registration module and module registry. + registrationModule = new RegistrationModule( + address(accessController), + address(ipRecordRegistry), + address(ipAccountRegistry), + address(licenseRegistry), + address(metadataProvider), + address(resolver) + ); + renderer = new IPAssetRenderer( + address(ipRecordRegistry), + address(licenseRegistry), + taggingModule, + royaltyModule + ); + moduleRegistry.registerModule(REGISTRATION_MODULE_KEY, address(registrationModule)); + vm.prank(address(registrationModule)); + ipId = ipRecordRegistry.register( + block.chainid, + address(erc721), + tokenId, + address(resolver), + true, + address(metadataProvider) + ); + + bytes memory metadata = abi.encode( + IP.Metadata({ + name: IP_NAME, + hash: IP_HASH, + registrationDate: IP_REGISTRATION_DATE, + registrant: alice, + uri: IP_EXTERNAL_URL + + }) + ); + vm.prank(address(registrationModule)); + metadataProvider.setMetadata(ipId, metadata); + } + + /// @notice Tests that the constructor works as expected. + function test_IPAssetRenderer_Constructor() public virtual { + assertEq(address(renderer.IP_RECORD_REGISTRY()), address(ipRecordRegistry)); + } + + /// @notice Tests that renderer can properly resolve names. + function test_IPAssetRenderer_Name() public virtual { + assertEq(renderer.name(ipId), IP_NAME); + } + + /// @notice Tests that the renderer can properly resolve descriptions. + function test_IPAssetRenderer_Description() public virtual { + assertEq( + renderer.description(ipId), + string.concat( + IP_NAME, + ", IP #", + Strings.toHexString(ipId), + ", is currently owned by", + Strings.toHexString(alice), + ". To learn more about this IP, visit ", + IP_EXTERNAL_URL + ) + ); + } + + /// @notice Tests that renderer can properly resolve hashes. + function test_IPAssetRenderer_Hash() public virtual { + assertEq(renderer.hash(ipId), IP_HASH); + } + + /// @notice Tests that renderer can properly resolve registration dates. + function test_IPAssetRenderer_RegistrationDate() public virtual { + assertEq(uint256(renderer.registrationDate(ipId)), uint256(IP_REGISTRATION_DATE)); + } + + /// @notice Tests that renderer can properly resolve registrants. + function test_IPAssetRenderer_Registrant() public virtual { + assertEq(renderer.registrant(ipId), alice); + } + + /// @notice Tests that renderer can properly resolve URLs. + function test_IPAssetRenderer_ExternalURL() public virtual { + assertEq(renderer.uri(ipId), IP_EXTERNAL_URL); + } + + /// @notice Tests that renderer can properly owners. + function test_IPAssetRenderer_Owner() public virtual { + assertEq(renderer.owner(ipId), alice); + } + + /// @notice Tests that the renderer can get the right token URI. + function test_IPAssetRenderer_TokenURI() public virtual { + string memory ownerStr = Strings.toHexString(uint160(address(alice))); + string memory description = string.concat( + IP_NAME, + ", IP #", + Strings.toHexString(ipId), + ", is currently owned by", + Strings.toHexString(alice), + ". To learn more about this IP, visit ", + IP_EXTERNAL_URL + ); + + string memory ipIdStr = Strings.toHexString(uint160(ipId)); + string memory uriEncoding = string(abi.encodePacked( + '{"name": "IP Asset #', ipIdStr, '", "description": "', description, '", "attributes": [', + '{"trait_type": "Name", "value": "IPRecord"},', + '{"trait_type": "Owner", "value": "', ownerStr, '"},' + '{"trait_type": "Registrant", "value": "', ownerStr, '"},', + '{"trait_type": "Hash", "value": "0x0000000000000000000000000000000000000000000000000000000000000000"},', + '{"trait_type": "Registration Date", "value": "', Strings.toString(IP_REGISTRATION_DATE), '"}', + ']}' + )); + string memory expectedURI = string(abi.encodePacked( + "data:application/json;base64,", + Base64.encode(bytes(string(abi.encodePacked(uriEncoding)))) + )); + assertEq(expectedURI, renderer.tokenURI(ipId)); + } + +} diff --git a/test/foundry/registries/metadata/IPMetadataProvider.t.sol b/test/foundry/registries/metadata/IPMetadataProvider.t.sol new file mode 100644 index 000000000..1cf9ed017 --- /dev/null +++ b/test/foundry/registries/metadata/IPMetadataProvider.t.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import { BaseTest } from "test/foundry/utils/BaseTest.sol"; +import { MockModuleRegistry } from "test/foundry/mocks/MockModuleRegistry.sol"; +import { IPMetadataProvider } from "contracts/registries/metadata/IPMetadataProvider.sol"; +import { IPRecordRegistry } from "contracts/registries/IPRecordRegistry.sol"; +import { Errors } from "contracts/lib/Errors.sol"; + +/// @title IP Metadata Provider Testing Contract +/// @notice Contract for metadata provider settings. +contract IPMetadataProviderTest is BaseTest { + + /// @notice Test bytes metadata settings. + bytes public TEST_METADATA = bytes("DEADBEEF"); + + /// @notice Placeholder for module registry. + MockModuleRegistry public registry; + + /// @notice Placeholder or registration module. + address public registrationModule = vm.addr(0x69); + + /// @notice Placeholder for an IP id. + address public ipId = vm.addr(0x1234); + + /// @notice The IP metadata provider SUT. + IPMetadataProvider public metadataProvider; + + /// @notice Initializes the IP metadata provider contract. + function setUp() public virtual override { + BaseTest.setUp(); + registry = new MockModuleRegistry(registrationModule); + metadataProvider = new IPMetadataProvider(address(registry)); + } + + /// @notice Tests IP metadata provider initialization. + function test_IPMetadataProvider_Constructor() public { + assertEq(address(metadataProvider.MODULE_REGISTRY()), address(registry)); + } + + /// @notice Tests metadata is properly stored. + function test_IPMetadataProvider_Metadata() public { + vm.prank(registrationModule); + metadataProvider.setMetadata(ipId, TEST_METADATA); + bytes memory expectedMetadata = bytes("DEADBEEF"); + assertEq( + metadataProvider.getMetadata(ipId), + expectedMetadata + ); + } + + /// @notice Checks that metadata setting reverts if not called by the registry. + function test_IPMetadataProvider_SetMetadata_Reverts_Unauthorized() public { + vm.expectRevert(Errors.MetadataProvider_Unauthorized.selector); + metadataProvider.setMetadata(ipId, TEST_METADATA); + } +} diff --git a/test/foundry/resolvers/IPMetadataResolver.t.sol b/test/foundry/resolvers/IPMetadataResolver.t.sol deleted file mode 100644 index dc7168ee4..000000000 --- a/test/foundry/resolvers/IPMetadataResolver.t.sol +++ /dev/null @@ -1,243 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.23; - -import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; -import { Base64 } from "@openzeppelin/contracts/utils/Base64.sol"; - -import { ResolverBaseTest } from "test/foundry/resolvers/ResolverBase.t.sol"; -import { IPMetadataResolver } from "contracts/resolvers/IPMetadataResolver.sol"; -import { IResolver } from "contracts/interfaces/resolvers/IResolver.sol"; -import { ERC6551Registry } from "lib/reference/src/ERC6551Registry.sol"; -import { IModuleRegistry } from "contracts/interfaces/registries/IModuleRegistry.sol"; -import { IPRecordRegistry } from "contracts/registries/IPRecordRegistry.sol"; -import { RegistrationModule } from "contracts/modules/RegistrationModule.sol"; -import { IPAccountRegistry } from "contracts/registries/IPAccountRegistry.sol"; -import { MockModuleRegistry } from "test/foundry/mocks/MockModuleRegistry.sol"; -import { IIPRecordRegistry } from "contracts/interfaces/registries/IIPRecordRegistry.sol"; -import { IPAccountImpl} from "contracts/IPAccountImpl.sol"; -import { IIPMetadataResolver } from "contracts/interfaces/resolvers/IIPMetadataResolver.sol"; -import { MockERC721 } from "test/foundry/mocks/MockERC721.sol"; -import { ModuleBaseTest } from "test/foundry/modules/ModuleBase.t.sol"; -import { IP } from "contracts/lib/IP.sol"; -import { Errors } from "contracts/lib/Errors.sol"; -import { METADATA_RESOLVER_MODULE_KEY, REGISTRATION_MODULE_KEY } from "contracts/lib/modules/Module.sol"; - -/// @title IP Metadata Resolver Test Contract -/// @notice Tests IP metadata resolver functionality. -contract IPMetadataResolverTest is ResolverBaseTest { - - // Default IP record attributes. - string public constant RECORD_NAME = "IPRecord"; - string public constant RECORD_DESCRIPTION = "IPs all the way down."; - bytes32 public constant RECORD_HASH = ""; - uint64 public constant RECORD_REGISTRATION_DATE = 999999; - string public constant RECORD_URI = "https://storyprotocol.xyz"; - - /// @notice The registration module. - address public registrationModule; - - /// @notice The token contract SUT. - IIPMetadataResolver public ipResolver; - - /// @notice Mock IP identifier for resolver testing. - address public ipId; - - /// @notice Initializes the base token contract for testing. - function setUp() public virtual override(ResolverBaseTest) { - ResolverBaseTest.setUp(); - MockERC721 erc721 = new MockERC721(); - vm.prank(alice); - ipResolver = IIPMetadataResolver(_deployModule()); - uint256 tokenId = erc721.mintId(alice, 99); - // TODO: Mock this correctly - registrationModule = address(new RegistrationModule( - address(accessController), - address(ipRecordRegistry), - address(ipAccountRegistry), - address(licenseRegistry), - address(ipResolver) - )); - moduleRegistry.registerModule(REGISTRATION_MODULE_KEY, registrationModule); - moduleRegistry.registerModule(METADATA_RESOLVER_MODULE_KEY, address(ipResolver)); - vm.prank(registrationModule); - ipId = ipRecordRegistry.register( - block.chainid, - address(erc721), - tokenId, - address(ipResolver), - true - ); - } - - /// @notice Tests that the IP resolver interface is supported. - function test_IPMetadataResolver_SupportsInterface() public virtual { - assertTrue(ipResolver.supportsInterface(type(IIPMetadataResolver).interfaceId)); - } - - /// @notice Tests that metadata may be properly set for the resolver. - function test_IPMetadataResolver_SetMetadata() public { - vm.prank(ipId); - accessController.setPermission(ipId, alice, address(ipResolver), IIPMetadataResolver.setMetadata.selector, 1); - vm.prank(alice); - ipResolver.setMetadata( - ipId, - IP.MetadataRecord({ - name: RECORD_NAME, - description: RECORD_DESCRIPTION, - hash: RECORD_HASH, - registrationDate: RECORD_REGISTRATION_DATE, - registrant: alice, - uri: RECORD_URI - }) - ); - assertEq(ipResolver.name(ipId), RECORD_NAME); - assertEq(ipResolver.description(ipId), RECORD_DESCRIPTION); - assertEq(ipResolver.hash(ipId), RECORD_HASH); - assertEq(ipResolver.registrationDate(ipId), RECORD_REGISTRATION_DATE); - assertEq(ipResolver.registrant(ipId), alice); - assertEq(ipResolver.owner(ipId), alice); - assertEq(ipResolver.uri(ipId), RECORD_URI); - - // Also check the metadata getter returns as expected. - IP.Metadata memory metadata = ipResolver.metadata(ipId); - assertEq(metadata.name, RECORD_NAME); - assertEq(metadata.description, RECORD_DESCRIPTION); - assertEq(metadata.hash, RECORD_HASH); - assertEq(metadata.registrationDate, RECORD_REGISTRATION_DATE); - assertEq(metadata.registrant, alice); - assertEq(metadata.uri, RECORD_URI); - assertEq(metadata.owner, alice); - } - - /// @notice Checks that an unauthorized call to setMetadata reverts. - function test_IPMetadataResolver_SetMetadata_Reverts_Unauthorized() public { - vm.expectRevert(Errors.Module_Unauthorized.selector); - ipResolver.setMetadata( - ipId, - IP.MetadataRecord({ - name: RECORD_NAME, - description: RECORD_DESCRIPTION, - hash: RECORD_HASH, - registrationDate: RECORD_REGISTRATION_DATE, - registrant: alice, - uri: RECORD_URI - }) - ); - } - - /// @notice Tests that the name may be properly set for the resolver. - function test_IPMetadataResolver_SetName() public { - vm.prank(ipId); - accessController.setPermission(ipId, alice, address(ipResolver), IIPMetadataResolver.setName.selector, 1); - vm.prank(alice); - ipResolver.setName(ipId, RECORD_NAME); - assertEq(RECORD_NAME, ipResolver.name(ipId)); - } - - /// @notice Checks that an unauthorized call to setName reverts. - function test_IPMetadataResolver_SetName_Reverts_Unauthorized() public { - vm.expectRevert(Errors.Module_Unauthorized.selector); - ipResolver.setName(ipId, RECORD_NAME); - } - - /// @notice Tests that the description may be properly set for the resolver. - function test_IPMetadataResolver_SetDescriptionx() public { - vm.prank(ipId); - accessController.setPermission(ipId, alice, address(ipResolver), IIPMetadataResolver.setDescription.selector, 1); - vm.prank(alice); - ipResolver.setDescription(ipId, RECORD_DESCRIPTION); - assertEq(RECORD_DESCRIPTION, ipResolver.description(ipId)); - } - - /// @notice Checks that an unauthorized call to setDescription reverts. - function test_IPMetadataResolver_SetDescription_Reverts_Unauthorized() public { - vm.expectRevert(Errors.Module_Unauthorized.selector); - ipResolver.setDescription(ipId, RECORD_DESCRIPTION); - } - - /// @notice Tests that the hash may be properly set for the resolver. - function test_IPMetadataResolver_SetHash() public { - vm.prank(ipId); - accessController.setPermission(ipId, alice, address(ipResolver), IIPMetadataResolver.setHash.selector, 1); - vm.prank(alice); - ipResolver.setHash(ipId, RECORD_HASH); - assertEq(RECORD_HASH, ipResolver.hash(ipId)); - } - - /// @notice Checks that an unauthorized call to setHash reverts. - function test_IPMetadataResolver_SetHash_Reverts_Unauthorized() public { - vm.expectRevert(Errors.Module_Unauthorized.selector); - ipResolver.setHash(ipId, RECORD_HASH); - } - - /// @notice Checks that owner queries return the zero address if there is no - /// IP account attached to the IP or if it was not registered. - function test_IPMetadataResolver_Owner_NonExistent() public { - // TODO: Make more granular testing for the above two conditions. - assertEq(address(0), ipResolver.owner(address(0))); - } - - /// @notice Checks setting token URI works as expected. - function test_IPMetadataResolver_SetTokenURI() public { - vm.prank(ipId); - accessController.setPermission(ipId, alice, address(ipResolver), IIPMetadataResolver.setURI.selector, 1); - vm.prank(alice); - ipResolver.setURI(ipId, RECORD_URI); - assertEq(ipResolver.uri(ipId), RECORD_URI); - } - - /// @notice Checks the default token URI renders as expected. - function test_IPMetadataResolver_TokenURI_DefaultRender() public { - // Check default empty string value for unregistered IP. - assertEq(ipResolver.uri(address(0)), ""); - - // Check default string value for registered IP. - assertTrue(accessController.checkPermission(ipId, alice, address(ipResolver), IIPMetadataResolver.setMetadata.selector)); - vm.prank(alice); - ipResolver.setMetadata( - ipId, - IP.MetadataRecord({ - name: RECORD_NAME, - description: RECORD_DESCRIPTION, - hash: RECORD_HASH, - registrationDate: RECORD_REGISTRATION_DATE, - registrant: alice, - uri: "" // Blank indicates the default record should be used. - }) - ); - string memory ownerStr = Strings.toHexString(uint160(address(alice))); - string memory ipIdStr = Strings.toHexString(uint160(ipId)); - string memory uriEncoding = string(abi.encodePacked( - '{"name": "IP Asset #', ipIdStr, '", "description": "IPs all the way down.", "attributes": [', - '{"trait_type": "Name", "value": "IPRecord"},', - '{"trait_type": "Owner", "value": "', ownerStr, '"},' - '{"trait_type": "Registrant", "value": "', ownerStr, '"},', - '{"trait_type": "Hash", "value": "0x0000000000000000000000000000000000000000000000000000000000000000"},', - '{"trait_type": "Registration Date", "value": "', Strings.toString(RECORD_REGISTRATION_DATE), '"}', - ']}' - )); - string memory expectedURI = string(abi.encodePacked( - "data:application/json;base64,", - Base64.encode(bytes(string(abi.encodePacked(uriEncoding)))) - )); - assertEq(expectedURI, ipResolver.uri(ipId)); - } - - /// @dev Gets the expected name for the module. - function _expectedName() internal virtual view override returns (string memory) { - return "METADATA_RESOLVER_MODULE"; - } - - /// @dev Deploys a new IP Metadata Resolver. - function _deployModule() internal override returns (address) { - return address( - new IPMetadataResolver( - address(accessController), - address(ipRecordRegistry), - address(ipAccountRegistry), - address(licenseRegistry) - ) - ); - } - -} diff --git a/test/foundry/resolvers/IPResolver.t.sol b/test/foundry/resolvers/IPResolver.t.sol new file mode 100644 index 000000000..cdeab148f --- /dev/null +++ b/test/foundry/resolvers/IPResolver.t.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import { ResolverBaseTest } from "test/foundry/resolvers/ResolverBase.t.sol"; +import { IPResolver } from "contracts/resolvers/IPResolver.sol"; +import { KeyValueResolver } from "contracts/resolvers/KeyValueResolver.sol"; +import { IKeyValueResolver } from "contracts/interfaces/resolvers/IKeyValueResolver.sol"; +import { IPMetadataProvider } from "contracts/registries/metadata/IPMetadataProvider.sol"; +import { IResolver } from "contracts/interfaces/resolvers/IResolver.sol"; +import { ERC6551Registry } from "lib/reference/src/ERC6551Registry.sol"; +import { IModuleRegistry } from "contracts/interfaces/registries/IModuleRegistry.sol"; +import { IPRecordRegistry } from "contracts/registries/IPRecordRegistry.sol"; +import { RegistrationModule } from "contracts/modules/RegistrationModule.sol"; +import { IPAccountRegistry } from "contracts/registries/IPAccountRegistry.sol"; +import { MockModuleRegistry } from "test/foundry/mocks/MockModuleRegistry.sol"; +import { IIPRecordRegistry } from "contracts/interfaces/registries/IIPRecordRegistry.sol"; +import { IPAccountImpl} from "contracts/IPAccountImpl.sol"; +import { MockERC721 } from "test/foundry/mocks/MockERC721.sol"; +import { ModuleBaseTest } from "test/foundry/modules/ModuleBase.t.sol"; +import { IP } from "contracts/lib/IP.sol"; +import { Errors } from "contracts/lib/Errors.sol"; +import { IP_RESOLVER_MODULE_KEY, REGISTRATION_MODULE_KEY } from "contracts/lib/modules/Module.sol"; + +/// @title IP Resolver Test Contract +/// @notice Tests IP metadata resolver functionality. +contract IPResolverTest is ResolverBaseTest { + + // Test record attributes. + string public constant TEST_KEY = "Key"; + string public constant TEST_VALUE = "Value"; + + /// @notice The registration module. + address public registrationModule; + + /// @notice The token contract SUT. + IPResolver public ipResolver; + + /// @notice Mock IP identifier for resolver testing. + address public ipId; + + /// @notice Initializes the base token contract for testing. + function setUp() public virtual override(ResolverBaseTest) { + ResolverBaseTest.setUp(); + MockERC721 erc721 = new MockERC721(); + vm.prank(alice); + ipResolver = IPResolver(_deployModule()); + IPMetadataProvider metadataProvider = new IPMetadataProvider(address(moduleRegistry)); + uint256 tokenId = erc721.mintId(alice, 99); + // TODO: Mock this correctly + registrationModule = address(new RegistrationModule( + address(accessController), + address(ipRecordRegistry), + address(ipAccountRegistry), + address(licenseRegistry), + address(ipResolver), + address(metadataProvider) + )); + moduleRegistry.registerModule(REGISTRATION_MODULE_KEY, registrationModule); + moduleRegistry.registerModule(IP_RESOLVER_MODULE_KEY, address(ipResolver)); + vm.prank(registrationModule); + ipId = ipRecordRegistry.register( + block.chainid, + address(erc721), + tokenId, + address(ipResolver), + true, + address(metadataProvider) + ); + } + + /// @notice Tests that the IP resolver interface is supported. + function test_IPMetadataResolver_SupportsInterface() public virtual { + assertTrue(ipResolver.supportsInterface(type(IKeyValueResolver).interfaceId)); + } + + /// @notice Tests that key-value pair string attribution may be properly set. + function test_IPMetadataResolver_SetValue() public { + vm.prank(ipId); + accessController.setPermission(ipId, alice, address(ipResolver), KeyValueResolver.setValue.selector, 1); + vm.prank(alice); + ipResolver.setValue( + ipId, + TEST_KEY, + TEST_VALUE + ); + assertEq(ipResolver.value(ipId, TEST_KEY), TEST_VALUE); + } + + /// @dev Gets the expected name for the module. + function _expectedName() internal virtual view override returns (string memory) { + return "IP_RESOLVER_MODULE"; + } + + /// @dev Deploys a new IP Metadata Resolver. + function _deployModule() internal override returns (address) { + return address( + new IPResolver( + address(accessController), + address(ipRecordRegistry), + address(ipAccountRegistry), + address(licenseRegistry) + ) + ); + } + +}