From eb4c1c02abf23ac03cb3f8ccac5c6bbe6d9b12bf Mon Sep 17 00:00:00 2001 From: Leeren Chang Date: Fri, 19 Jan 2024 21:33:52 -0800 Subject: [PATCH 1/5] Fix merge --- .../registries/IIPRecordRegistry.sol | 131 +++++++++++ .../resolvers/IIPMetadataResolver.sol | 77 +++++++ contracts/interfaces/resolvers/IResolver.sol | 11 + contracts/lib/Errors.sol | 31 ++- contracts/lib/IP.sol | 55 +++++ contracts/lib/modules/Module.sol | 6 + contracts/registries/IPRecordRegistry.sol | 213 ++++++++++++++++++ contracts/resolvers/IPMetadataResolver.sol | 204 +++++++++++++++++ contracts/resolvers/ResolverBase.sol | 37 +++ 9 files changed, 761 insertions(+), 4 deletions(-) create mode 100644 contracts/interfaces/registries/IIPRecordRegistry.sol create mode 100644 contracts/interfaces/resolvers/IIPMetadataResolver.sol create mode 100644 contracts/interfaces/resolvers/IResolver.sol create mode 100644 contracts/lib/IP.sol create mode 100644 contracts/lib/modules/Module.sol create mode 100644 contracts/registries/IPRecordRegistry.sol create mode 100644 contracts/resolvers/IPMetadataResolver.sol create mode 100644 contracts/resolvers/ResolverBase.sol diff --git a/contracts/interfaces/registries/IIPRecordRegistry.sol b/contracts/interfaces/registries/IIPRecordRegistry.sol new file mode 100644 index 000000000..5519dc517 --- /dev/null +++ b/contracts/interfaces/registries/IIPRecordRegistry.sol @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: UNLICENSED +// See https://github.com/storyprotocol/protocol-contracts/blob/main/StoryProtocol-AlphaTestingAgreement-17942166.3.pdf +pragma solidity ^0.8.21; + +/// @title IP Record Registry Interface +interface IIPRecordRegistry { + + /// @notice Emits when an IP is officially registered into the protocol. + /// @param ipId The canonical identifier for the IP. + /// @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 resolver The address of the resolver linked to the IP. + event IPRegistered( + address ipId, + uint256 indexed chainId, + address indexed tokenContract, + uint256 indexed tokenId, + address resolver + ); + + /// @notice Emits when an IP account is created for an IP. + /// @param ipId The canonical identifier for the IP. + /// @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. + event IPAccountSet( + address ipId, + uint256 indexed chainId, + address indexed tokenContract, + uint256 indexed tokenId + ); + + /// @notice Emits when an IP resolver is bound to an IP. + /// @param ipId The canonical identifier of the specified IP. + /// @param resolver The address of the new resolver bound to the IP. + event IPResolverSet( + address ipId, + address resolver + ); + + /// @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. + /// @param tokenContract The address of the IP. + /// @param tokenId The token identifier of the IP. + /// @return The address of the associated IP account. + function ipId( + uint256 chainId, + address tokenContract, + uint256 tokenId + ) external view returns (address); + + /// @notice Checks whether an IP was registered based on its ID. + /// @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); + + /// @notice Retrieves whether or not an IP was registered. + /// @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. + /// @return Whether the IP was registered into the protocol. + function isRegistered( + uint256 chainId, + address tokenContract, + uint256 tokenId + ) external view returns (bool); + + /// @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); + + /// @notice Gets the resolver bound to an IP based on its NFT attributes. + /// @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. + /// @return The IP resolver address if registered, else the zero address. + function resolver( + uint256 chainId, + address tokenContract, + uint256 tokenId + ) 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 createAccount Whether to create an IP account in the process. + function register( + uint256 chainId, + address tokenContract, + uint256 tokenId, + address resolverAddr, + bool createAccount + ) external; + + /// @notice Creates the IP account for the specified IP. + /// @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. + function createIPAccount( + uint256 chainId, + address tokenContract, + uint256 tokenId + ) external returns (address); + + /// @notice Sets the resolver for an IP based on its canonical ID. + /// @param id The canonical ID of the IP. + /// @param resolverAddr The address of the resolver being set. + function setResolver(address id, address resolverAddr) external; + + /// @notice Sets the resolver for an IP based on its NFT attributes. + /// @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. + function setResolver( + uint256 chainId, + address tokenContract, + uint256 tokenId, + address resolver + ) external; + +} diff --git a/contracts/interfaces/resolvers/IIPMetadataResolver.sol b/contracts/interfaces/resolvers/IIPMetadataResolver.sol new file mode 100644 index 000000000..3a2ef32a4 --- /dev/null +++ b/contracts/interfaces/resolvers/IIPMetadataResolver.sol @@ -0,0 +1,77 @@ +// 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 { + + /// @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 category associated with the specified IP. + /// @param ipId The canonical ID of the specified IP. + function category(address ipId) external view returns (IP.Category); + + /// @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 the token URI associated with the IP. + /// @param ipId The canonical ID of the specified IP. + function tokenURI(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 category associated with an IP. + /// @param ipId The canonical ID of the specified IP. + /// @param category The IP category to associate with the IP. + function setCategory(address ipId, IP.Category category) 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 a token URI to associated with the IP. + /// @param ipId The canonical ID of the specified IP. + function setTokenURI(address ipId, string calldata tokenURI) external; + +} diff --git a/contracts/interfaces/resolvers/IResolver.sol b/contracts/interfaces/resolvers/IResolver.sol new file mode 100644 index 000000000..4b7449a51 --- /dev/null +++ b/contracts/interfaces/resolvers/IResolver.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.21; + +/// @notice Resolver Interface +interface IResolver { + + /// @notice Checks whether the resolver IP interface is supported. + function supportsInterface(bytes4 id) view external returns (bool); + +} diff --git a/contracts/lib/Errors.sol b/contracts/lib/Errors.sol index d3bfa0492..6c122beaa 100644 --- a/contracts/lib/Errors.sol +++ b/contracts/lib/Errors.sol @@ -1,11 +1,34 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.20; +// SPDX-License-Identifier: UNLICENSED +// See https://github.com/storyprotocol/protocol-contracts/blob/main/StoryProtocol-AlphaTestingAgreement-17942166.3.pdf +pragma solidity ^0.8.19; /// @title Errors Library /// @notice Library for all Story Protocol contract errors. library Errors { + //////////////////////////////////////////////////////////////////////////// + // IPRecordRegistry // + //////////////////////////////////////////////////////////////////////////// + + /// @notice The IP record has already been registered. + error IPRecordRegistry_AlreadyRegistered(); + + /// @notice The IP account has already been created. + error IPRecordRegistry_IPAccountAlreadyCreated(); + + /// @notice The IP record has not yet been registered. + error IPRecordRegistry_NotYetRegistered(); + + /// @notice Caller not authorized to perform the IP registry function call. + error IPRecordRegistry_Unauthorized(); + + //////////////////////////////////////////////////////////////////////////// + // IPResolver /// + //////////////////////////////////////////////////////////////////////////// + + /// @notice Caller not authorized to perform the IP resolver function call. + error IPResolver_Unauthorized(); + //////////////////////////////////////////////////////////////////////////// // LicenseRegistry // //////////////////////////////////////////////////////////////////////////// @@ -43,4 +66,4 @@ library Errors { error AccessController__CallerIsNotIPAccount(); error AccessController__PermissionIsNotValid(); -} \ No newline at end of file +} diff --git a/contracts/lib/IP.sol b/contracts/lib/IP.sol new file mode 100644 index 000000000..30d526cb6 --- /dev/null +++ b/contracts/lib/IP.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: UNLICENSED +// See https://github.com/storyprotocol/protocol-contracts/blob/main/StoryProtocol-AlphaTestingAgreement-17942166.3.pdf +pragma solidity ^0.8.21; + +/// @title IP Library +/// @notice Library for constants, structs, and helper functions used for IP. +library IP { + + /// @notice List of IP categories currently declared in Story Protocol.. + /// @custom:note Later on this may be canonicalized in the licensing module. + enum Category { + PATENT, + TRADEMARK, + COPYRIGHT + } + + /// @notice Core metadata associated with an IP. + struct Metadata { + address owner; // The current owner of the IP. + string name; // The name associated with the IP. + Category category; // The overarching category the IP belongs to. + string description; // A description associated with the IP. + bytes32 hash; // A keccak-256 hash of the IP content. + uint64 registrationDate; // The date which the IP was registered. + address registrant; // The address of the initial IP registrant. + string uri; // The token URI associated with the IP. + } + + /// @notice Core metadata exclusively saved by the IP resolver. + /// @dev Resolved attributes not referenced here are processed through + /// their corresponding data modules (e.g. licensing for license data). + struct MetadataRecord { + string name; // The name associated with the IP. + Category category; // The overarching category the IP belongs to. + string description; // A description associated with the IP. + bytes32 hash; // A keccak-256 hash of the IP content. + uint64 registrationDate; // The date which the IP was registered. + address registrant; // The address of the initial IP registrant. + string uri; // The token URI associated with the IP. + } + + + /// @notice Converts a custom IP category type to its string representation. + /// @param category The category of the IP. + function toString(Category category) internal pure returns (string memory) { + if (category == Category.PATENT) { + return "Patent"; + } else if (category == Category.TRADEMARK) { + return "Trademark"; + } else { + return "Copyright"; + } + } + +} diff --git a/contracts/lib/modules/Module.sol b/contracts/lib/modules/Module.sol new file mode 100644 index 000000000..25bbf6c79 --- /dev/null +++ b/contracts/lib/modules/Module.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: UNLICENSED +// See https://github.com/storyprotocol/protocol-contracts/blob/main/StoryProtocol-AlphaTestingAgreement-17942166.3.pdf +pragma solidity ^0.8.21; + +// String values for core protocol modules. +string constant REGISTRATION_MODULE_KEY = "REGISTRATION_MODULE"; diff --git a/contracts/registries/IPRecordRegistry.sol b/contracts/registries/IPRecordRegistry.sol new file mode 100644 index 000000000..84dabd806 --- /dev/null +++ b/contracts/registries/IPRecordRegistry.sol @@ -0,0 +1,213 @@ +// SPDX-License-Identifier: UNLICENSED +// See https://github.com/storyprotocol/protocol-contracts/blob/main/StoryProtocol-AlphaTestingAgreement-17942166.3.pdf +pragma solidity ^0.8.21; + +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"; +import { IModuleRegistry } from "contracts/interfaces/registries/IModuleRegistry.sol"; +import { Errors } from "contracts/lib/Errors.sol"; + +/// @title IP Record Registry +/// @notice This contract acts as the source of truth for all IP registered in +/// Story Protocol. An IP is identified by its contract address, token +/// id, and coin type, meaning any NFT may be conceptualized as an IP. +/// Once an IP is registered into the protocol, a corresponding IP +/// record is generated, which references an IP resolver for metadata +/// attribution and an IP account for protocol authorization. Only +/// approved registration modules may register IP into this registry. +/// 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 Gets the factory contract used for IP account creation. + IIPAccountRegistry public immutable IP_ACCOUNT_REGISTRY; + + /// @notice Gets the protocol-wide module registry. + IModuleRegistry public immutable MODULE_REGISTRY; + + /// @notice Tracks the total number of IP records in existence. + uint256 public totalSupply = 0; + + /// @notice Tracks the address of the IP account factory. + address public ipAccountRegistry; + + /// @dev Maps an IP, identified by its IP ID, to a metadata resolver. + mapping(address => address) internal _resolvers; + + /// @notice Restricts calls to only originate from the registration module. + modifier onlyRegistrationModule() { + if (address(MODULE_REGISTRY.protocolModule(REGISTRATION_MODULE_KEY)) != msg.sender) { + revert Errors.IPRecordRegistry_Unauthorized(); + } + _; + } + + /// @notice Initializes the IP Record Registry. + /// @param moduleRegistry The address of the protocol module registry. + constructor(address moduleRegistry) { + MODULE_REGISTRY = IModuleRegistry(moduleRegistry); + } + + /// @notice Gets the canonical IP identifier associated with an IP NFT. + /// @dev This is equivalent to the address of its bound IP account. + /// @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. + /// @return The IP's canonical address identifier. + function ipId( + uint256 chainId, + address tokenContract, + uint256 tokenId + ) public view returns (address) { + return IP_ACCOUNT_REGISTRY.ipAccount(chainId, tokenContract, tokenId); + } + + /// @notice Checks whether an IP was registered based on its ID. + /// @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); + } + + /// @notice Checks whether an IP was registered 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. + /// @return Whether the NFT was registered into the protocol as IP. + function isRegistered( + uint256 chainId, + address tokenContract, + uint256 tokenId + ) external view returns (bool) { + address id = ipId(chainId, tokenContract, tokenId); + return _resolvers[id] != 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]; + } + + /// @notice Gets the resolver bound to 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. + /// @return The IP resolver address if registered, else the zero address. + function resolver( + uint256 chainId, + address tokenContract, + uint256 tokenId + ) external view returns (address) { + address id = ipId(chainId, tokenContract, tokenId); + return _resolvers[id]; + } + + /// @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 createAccount Whether to create an IP account when registering. + function register( + uint256 chainId, + address tokenContract, + uint256 tokenId, + address resolverAddr, + bool createAccount + ) external onlyRegistrationModule { + address id = ipId(chainId, tokenContract, tokenId); + if (_resolvers[id] != address(0)) { + revert Errors.IPRecordRegistry_AlreadyRegistered(); + } + + // This is to emphasize the semantic differences between utilizing the + // IP account as an identifier versus as an account used for auth. + address account = id; + + if (account.code.length == 0 && createAccount) { + _createIPAccount(chainId, tokenContract, tokenId); + } + _setResolver(id, resolverAddr); + totalSupply++; + emit IPRegistered(id, chainId, tokenContract, tokenId, resolverAddr); + } + + /// @notice Creates the IP account for the specified IP. + /// @custom:note For now, we assume that every IP is uniquely tied to an IP + /// account deployed by the IP account registry. However, this + /// may change in the future, hence the distinguishing between + /// IP accounts as identifiers vs. authentication primitives. + /// @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. + function createIPAccount( + uint256 chainId, + address tokenContract, + uint256 tokenId + ) external returns (address) { + address account = IP_ACCOUNT_REGISTRY.ipAccount(chainId, tokenContract, tokenId); + if (account.code.length != 0) { + revert Errors.IPRecordRegistry_IPAccountAlreadyCreated(); + } + return _createIPAccount(chainId, tokenContract, tokenId); + } + + /// @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. + function setResolver( + uint256 chainId, + address tokenContract, + uint256 tokenId, + address resolverAddr + ) external onlyRegistrationModule { + address id = ipId(chainId, tokenContract, tokenId); + setResolver(id, resolverAddr); + } + + /// @notice Sets the resolver for an IP based on its canonical ID. + /// @param id The canonical ID of the IP. + /// @param resolverAddr The address of the resolver being set. + function setResolver( + address id, + address resolverAddr + ) public onlyRegistrationModule { + if (_resolvers[id] != address(0)) { + revert Errors.IPRecordRegistry_NotYetRegistered(); + } + _setResolver(id, resolverAddr); + } + + /// @dev Creates an IP account for the specified IP (NFT). + /// @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. + function _createIPAccount( + uint256 chainId, + address tokenContract, + uint256 tokenId + ) internal returns (address account) { + account = IP_ACCOUNT_REGISTRY.registerIpAccount(chainId, tokenContract, tokenId); + emit IPAccountSet(account, chainId, tokenContract, tokenId); + } + + /// @dev Sets the resolver for the specified IP. + /// @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; + emit IPResolverSet(id, resolverAddr); + } + +} diff --git a/contracts/resolvers/IPMetadataResolver.sol b/contracts/resolvers/IPMetadataResolver.sol new file mode 100644 index 000000000..d751865ea --- /dev/null +++ b/contracts/resolvers/IPMetadataResolver.sol @@ -0,0 +1,204 @@ +// 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 { 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"; + +/// @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. + constructor(address accessController) ResolverBase(accessController) {} + + /// @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, + category: record.category, + description: record.description, + hash: record.hash, + registrationDate: record.registrationDate, + registrant: record.registrant, + uri: tokenURI(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 category associated with an IP. + /// @param ipId The canonical ID of the specified IP. + function category(address ipId) external view returns (IP.Category) { + return _records[ipId].category; + } + + /// @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) { + return IIPAccount(payable(ipId)).owner(); + } + + /// @notice Fetches the token URI associated with the IP. + /// @param ipId The canonical ID of the specified IP. + function tokenURI(address ipId) public view returns (string memory) { + IP.MetadataRecord memory record = _records[ipId]; + string memory uri = record.uri; + + if ( + bytes(uri).length > 0 || // Return URI if overridden. + _records[ipId].registrant == address(0) // Return "" if nonexistent. + ) { + return uri; + } + + 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; + } + + /// @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; + } + + /// @notice Sets the category associated with an IP. + /// @param ipId The canonical ID of the specified IP. + /// @param newCategory The IP category to associate with the IP. + function setCategory(address ipId, IP.Category newCategory) external onlyAuthorized(ipId) { + _records[ipId].category = newCategory; + } + + /// @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; + } + + /// @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; + } + + /// @notice Sets a token URI to associated with the IP. + /// @param ipId The canonical ID of the specified IP. + /// @param newTokenURI The new token URI to set for the IP. + function setTokenURI(address ipId, string calldata newTokenURI) external onlyAuthorized(ipId) { + _records[ipId].uri = newTokenURI; + } + + /// @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); + } + + /// @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": "Category", "value": "', + IP.toString(record.category), + '"},', + '{"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/ResolverBase.sol b/contracts/resolvers/ResolverBase.sol new file mode 100644 index 000000000..8a9adeba2 --- /dev/null +++ b/contracts/resolvers/ResolverBase.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.21; + +import { IAccessController } from "contracts/interfaces/IAccessController.sol"; +import { IResolver } from "contracts/interfaces/resolvers/IResolver.sol"; +import { Errors } from "contracts/lib/Errors.sol"; + +/// @notice IP Resolver Base Contract +abstract contract ResolverBase is IResolver { + + /// @notice Gets the protocol-wide module access controller. + IAccessController public immutable ACCESS_CONTROLLER; + + /// @notice Checks if IP identified by ipId is authorized to perform a call. + /// @param ipId The identifier for the IP being authorized. + modifier onlyAuthorized(address ipId) { + if (!ACCESS_CONTROLLER.checkPolicy(ipId, msg.sender, address(this), msg.sig)) { + revert Errors.IPResolver_Unauthorized(); + } + _; + } + + /// @notice Initializes the base IP resolver contract. + /// @param accessController The address of the module access controller. + constructor(address accessController) { + ACCESS_CONTROLLER = IAccessController(accessController); + } + + /// @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(IResolver).interfaceId; + } + +} From 32dbddebe3b71e694cbb3aa654ad682ac5c48dea Mon Sep 17 00:00:00 2001 From: Leeren Chang Date: Fri, 19 Jan 2024 21:35:14 -0800 Subject: [PATCH 2/5] lets go --- .../interfaces/erc6551/IERC6551Registry.sol | 34 ++ contracts/interfaces/resolvers/IResolver.sol | 3 + contracts/lib/Errors.sol | 6 + contracts/registries/IPAccountRegistry.sol | 9 +- contracts/registries/IPRecordRegistry.sol | 12 +- contracts/resolvers/IPMetadataResolver.sol | 20 +- contracts/resolvers/ResolverBase.sol | 24 +- test/foundry/IPAccountRegistry.t.sol | 2 +- test/foundry/IPRecordRegistry.t.sol | 291 ++++++++++++++++++ test/foundry/mocks/MockERC6551Registry.sol | 36 +-- test/foundry/mocks/MockERC721.sol | 5 +- test/foundry/mocks/MockModuleRegistry.sol | 21 ++ .../resolvers/IPMetadataResolver.t.sol | 256 +++++++++++++++ test/foundry/resolvers/ResolverBase.t.sol | 42 +++ test/foundry/utils/BaseTest.sol | 27 ++ 15 files changed, 738 insertions(+), 50 deletions(-) create mode 100644 contracts/interfaces/erc6551/IERC6551Registry.sol create mode 100644 test/foundry/IPRecordRegistry.t.sol create mode 100644 test/foundry/mocks/MockModuleRegistry.sol create mode 100644 test/foundry/resolvers/IPMetadataResolver.t.sol create mode 100644 test/foundry/resolvers/ResolverBase.t.sol create mode 100644 test/foundry/utils/BaseTest.sol diff --git a/contracts/interfaces/erc6551/IERC6551Registry.sol b/contracts/interfaces/erc6551/IERC6551Registry.sol new file mode 100644 index 000000000..4ef04972e --- /dev/null +++ b/contracts/interfaces/erc6551/IERC6551Registry.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IERC6551Registry { + + /// @notice Emits when a new ERC6551 account is created. + event AccountCreated( + address account, + address indexed implementation, + uint256 chainId, + address indexed tokenContract, + uint256 indexed tokenId, + uint256 salt + ); + + /// @notice Creates a new token-bound account for an NFT. + function createAccount( + address implementation, + uint256 chainId, + address tokenContract, + uint256 tokenId, + uint256 seed, + bytes calldata initData + ) external returns (address); + + /// @notice Retrieves the token-bound account address for an NFT. + function account( + address implementation, + uint256 chainId, + address tokenContract, + uint256 tokenId, + uint256 salt + ) external view returns (address); +} diff --git a/contracts/interfaces/resolvers/IResolver.sol b/contracts/interfaces/resolvers/IResolver.sol index 4b7449a51..ce031d473 100644 --- a/contracts/interfaces/resolvers/IResolver.sol +++ b/contracts/interfaces/resolvers/IResolver.sol @@ -5,6 +5,9 @@ pragma solidity ^0.8.21; /// @notice Resolver Interface interface IResolver { + /// @notice Gets the address of the access controller for the resolver. + function accessController() view external returns (address); + /// @notice Checks whether the resolver IP interface is supported. function supportsInterface(bytes4 id) view external returns (bool); diff --git a/contracts/lib/Errors.sol b/contracts/lib/Errors.sol index 6c122beaa..419f908ef 100644 --- a/contracts/lib/Errors.sol +++ b/contracts/lib/Errors.sol @@ -19,6 +19,9 @@ library Errors { /// @notice The IP record has not yet been registered. error IPRecordRegistry_NotYetRegistered(); + /// @notice The specified IP resolver is not valid. + error IPRecordRegistry_ResolverInvalid(); + /// @notice Caller not authorized to perform the IP registry function call. error IPRecordRegistry_Unauthorized(); @@ -26,6 +29,9 @@ library Errors { // IPResolver /// //////////////////////////////////////////////////////////////////////////// + /// @notice The targeted IP does not yet have an IP account. + error IPResolver_InvalidIP(); + /// @notice Caller not authorized to perform the IP resolver function call. error IPResolver_Unauthorized(); diff --git a/contracts/registries/IPAccountRegistry.sol b/contracts/registries/IPAccountRegistry.sol index 42c925f43..bebbac5de 100644 --- a/contracts/registries/IPAccountRegistry.sol +++ b/contracts/registries/IPAccountRegistry.sol @@ -9,10 +9,11 @@ import { IERC6551Registry } from "lib/reference/src/interfaces/IERC6551Registry. /// @notice This contract is responsible for managing the registration and tracking of IP Accounts. /// It leverages a public ERC6551 registry to deploy IPAccount contracts. contract IPAccountRegistry is IIPAccountRegistry { - address internal immutable IP_ACCOUNT_IMPL; - bytes32 internal immutable IP_ACCOUNT_SALT; - address internal immutable ERC6551_PUBLIC_REGISTRY; - address internal immutable ACCESS_CONTROLLER; + + address public immutable IP_ACCOUNT_IMPL; + bytes32 public immutable IP_ACCOUNT_SALT; + address public immutable ERC6551_PUBLIC_REGISTRY; + address public immutable ACCESS_CONTROLLER; error NonExistIpAccountImpl(); diff --git a/contracts/registries/IPRecordRegistry.sol b/contracts/registries/IPRecordRegistry.sol index 84dabd806..a30318285 100644 --- a/contracts/registries/IPRecordRegistry.sol +++ b/contracts/registries/IPRecordRegistry.sol @@ -29,9 +29,6 @@ contract IPRecordRegistry is IIPRecordRegistry { /// @notice Tracks the total number of IP records in existence. uint256 public totalSupply = 0; - /// @notice Tracks the address of the IP account factory. - address public ipAccountRegistry; - /// @dev Maps an IP, identified by its IP ID, to a metadata resolver. mapping(address => address) internal _resolvers; @@ -45,7 +42,9 @@ contract IPRecordRegistry is IIPRecordRegistry { /// @notice Initializes the IP Record Registry. /// @param moduleRegistry The address of the protocol module registry. - constructor(address moduleRegistry) { + /// @param ipAccountRegistry The address of the IP account registry. + constructor(address moduleRegistry, address ipAccountRegistry) { + IP_ACCOUNT_REGISTRY = IIPAccountRegistry(ipAccountRegistry); MODULE_REGISTRY = IModuleRegistry(moduleRegistry); } @@ -180,7 +179,10 @@ contract IPRecordRegistry is IIPRecordRegistry { address id, address resolverAddr ) public onlyRegistrationModule { - if (_resolvers[id] != address(0)) { + if (resolverAddr == address(0)) { + revert Errors.IPRecordRegistry_ResolverInvalid(); + } + if (_resolvers[id] == address(0)) { revert Errors.IPRecordRegistry_NotYetRegistered(); } _setResolver(id, resolverAddr); diff --git a/contracts/resolvers/IPMetadataResolver.sol b/contracts/resolvers/IPMetadataResolver.sol index d751865ea..a6e4846bf 100644 --- a/contracts/resolvers/IPMetadataResolver.sol +++ b/contracts/resolvers/IPMetadataResolver.sol @@ -22,7 +22,13 @@ contract IPMetadataResolver is IIPMetadataResolver, ResolverBase { /// @notice Initializes the IP metadata resolver. /// @param accessController The access controller used for IP authorization. - constructor(address accessController) ResolverBase(accessController) {} + /// @param ipRecordRegistry The address of the IP record registry. + /// @param ipAccountRegistry The address of the IP account registry. + constructor( + address accessController, + address ipRecordRegistry, + address ipAccountRegistry + ) ResolverBase(accessController, ipRecordRegistry, ipAccountRegistry) {} /// @notice Fetches all metadata associated with the specified IP. /// @param ipId The canonical ID of the specified IP. @@ -81,19 +87,23 @@ contract IPMetadataResolver is IIPMetadataResolver, ResolverBase { /// @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 the token URI associated with the IP. /// @param ipId The canonical ID of the specified IP. function tokenURI(address ipId) public view returns (string memory) { + if (!IP_RECORD_REGISTRY.isRegistered(ipId)) { + return ""; + } + IP.MetadataRecord memory record = _records[ipId]; string memory uri = record.uri; - if ( - bytes(uri).length > 0 || // Return URI if overridden. - _records[ipId].registrant == address(0) // Return "" if nonexistent. - ) { + if (bytes(uri).length > 0) { return uri; } diff --git a/contracts/resolvers/ResolverBase.sol b/contracts/resolvers/ResolverBase.sol index 8a9adeba2..438da1bdd 100644 --- a/contracts/resolvers/ResolverBase.sol +++ b/contracts/resolvers/ResolverBase.sol @@ -3,6 +3,8 @@ pragma solidity ^0.8.21; import { IAccessController } from "contracts/interfaces/IAccessController.sol"; +import { IPRecordRegistry } from "contracts/registries/IPRecordRegistry.sol"; +import { IPAccountRegistry } from "contracts/registries/IPAccountRegistry.sol"; import { IResolver } from "contracts/interfaces/resolvers/IResolver.sol"; import { Errors } from "contracts/lib/Errors.sol"; @@ -12,6 +14,12 @@ abstract contract ResolverBase is IResolver { /// @notice Gets the protocol-wide module access controller. IAccessController public immutable ACCESS_CONTROLLER; + /// @notice Gets the protocol-wide IP account registry. + IPAccountRegistry public immutable IP_ACCOUNT_REGISTRY; + + /// @notice Gets the protocol-wide IP record registry. + IPRecordRegistry public immutable IP_RECORD_REGISTRY; + /// @notice Checks if IP identified by ipId is authorized to perform a call. /// @param ipId The identifier for the IP being authorized. modifier onlyAuthorized(address ipId) { @@ -22,9 +30,19 @@ abstract contract ResolverBase is IResolver { } /// @notice Initializes the base IP resolver contract. - /// @param accessController The address of the module access controller. - constructor(address accessController) { - ACCESS_CONTROLLER = IAccessController(accessController); + /// @param controller The address of the module access controller. + /// @param recordRegistry The address of the IP record registry. + /// @param accountRegistry The address of the IP account registry. + constructor(address controller, address recordRegistry, address accountRegistry) { + ACCESS_CONTROLLER = IAccessController(controller); + IP_RECORD_REGISTRY = IPRecordRegistry(recordRegistry); + IP_ACCOUNT_REGISTRY = IPAccountRegistry(accountRegistry); + } + + /// @notice Gets the access controller responsible for resolver auth. + /// @return The address of the access controller. + function accessController() external view returns (address) { + return address(ACCESS_CONTROLLER); } /// @notice Checks whether the resolver interface is supported. diff --git a/test/foundry/IPAccountRegistry.t.sol b/test/foundry/IPAccountRegistry.t.sol index c3679d139..242e8ed94 100644 --- a/test/foundry/IPAccountRegistry.t.sol +++ b/test/foundry/IPAccountRegistry.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.21; +pragma solidity ^0.8.23; import "forge-std/Test.sol"; diff --git a/test/foundry/IPRecordRegistry.t.sol b/test/foundry/IPRecordRegistry.t.sol new file mode 100644 index 000000000..64d608689 --- /dev/null +++ b/test/foundry/IPRecordRegistry.t.sol @@ -0,0 +1,291 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import { BaseTest } from "./utils/BaseTest.sol"; +import { IModuleRegistry } from "contracts/interfaces/registries/IModuleRegistry.sol"; +import { IIPRecordRegistry } from "contracts/interfaces/registries/IIPRecordRegistry.sol"; +import { IPAccountRegistry } from "contracts/registries/IPAccountRegistry.sol"; +import { IPRecordRegistry } from "contracts/registries/IPRecordRegistry.sol"; +import { IPAccountImpl} from "contracts/IPAccountImpl.sol"; +import { MockAccessController } from "test/foundry/mocks/MockAccessController.sol"; +import { MockModuleRegistry } from "test/foundry/mocks/MockModuleRegistry.sol"; +import { MockERC721 } from "test/foundry/mocks/MockERC721.sol"; +import { MockERC6551Registry } from "test/foundry/mocks/MockERC6551Registry.sol"; +import { Errors } from "contracts/lib/Errors.sol"; + +/// @title IP Record Registry Testing Contract +/// @notice Contract for testing core IP registration. +contract IPRecordRegistryTest is BaseTest { + + /// @notice Placeholder for registration module. + address public registrationModule = vm.addr(0x1337); + + /// @notice Placeholder for test resolver addresses. + address public resolver = vm.addr(0x6969); + address public resolver2 = vm.addr(0x6978); + + /// @notice The IP record registry SUT. + IPRecordRegistry public registry; + + /// @notice The module registry used for protocol module identification. + IModuleRegistry public moduleRegistry; + + /// @notice The IP account registry used for account creation. + IPAccountRegistry public ipAccountRegistry; + + /// @notice Mock NFT address for IP registration testing. + address public tokenAddress; + + /// @notice Mock NFT tokenId for IP registration testing. + uint256 public tokenId; + + /// @notice ERC-6551 public registry. + address public erc6551Registry; + + /// @notice Mock IP account implementation address. + address public ipAccountImpl; + + /// @notice Initializes the IP record registry testing contract. + function setUp() public virtual override { + BaseTest.setUp(); + address accessController = address(new MockAccessController()); + erc6551Registry = address(new MockERC6551Registry()); + moduleRegistry = IModuleRegistry( + address(new MockModuleRegistry(registrationModule)) + ); + ipAccountImpl = address(new IPAccountImpl()); + ipAccountRegistry = new IPAccountRegistry( + erc6551Registry, + accessController, + ipAccountImpl + ); + registry = new IPRecordRegistry( + address(moduleRegistry), + address(ipAccountRegistry) + ); + MockERC721 erc721 = new MockERC721(); + tokenAddress = address(erc721); + tokenId = erc721.mint(alice, 99); + } + + /// @notice Tests IP record registry initialization. + function test_IPRecordRegistry_Constructor() public { + assertEq(address(registry.MODULE_REGISTRY()), address(moduleRegistry)); + assertEq(address(registry.IP_ACCOUNT_REGISTRY()), address(ipAccountRegistry)); + } + + /// @notice Tests retrieval of IP canonical IDs. + function test_IPRecordRegistry_IpId() public { + assertEq( + registry.ipId(block.chainid, tokenAddress, tokenId), + _getAccount( + ipAccountImpl, + block.chainid, + tokenAddress, + tokenId, + ipAccountRegistry.IP_ACCOUNT_SALT() + ) + ); + } + + /// @notice Tests registration of IP records. + function test_IPRecordRegistry_Register() public { + uint256 totalSupply = registry.totalSupply(); + address ipId = _getAccount( + ipAccountImpl, + block.chainid, + tokenAddress, + tokenId, + ipAccountRegistry.IP_ACCOUNT_SALT() + ); + + // Ensure unregistered IP preconditions are satisfied. + assertEq(registry.resolver(ipId), address(0)); + assertTrue(!registry.isRegistered(ipId)); + assertTrue(!registry.isRegistered(block.chainid, tokenAddress, tokenId)); + assertTrue(!ipAccountRegistry.isRegistered(block.chainid, tokenAddress, tokenId)); + + // Ensure all expected events are emitted. + vm.expectEmit(true, true, true, true); + emit IIPRecordRegistry.IPAccountSet( + ipId, + block.chainid, + tokenAddress, + tokenId + ); + vm.expectEmit(true, true, true, true); + emit IIPRecordRegistry.IPResolverSet(ipId, resolver); + vm.expectEmit(true, true, true, true); + emit IIPRecordRegistry.IPRegistered( + ipId, + block.chainid, + tokenAddress, + tokenId, + resolver + ); + vm.prank(registrationModule); + registry.register( + block.chainid, + tokenAddress, + tokenId, + resolver, + true + ); + + /// Ensures IP record post-registration conditions are met. + assertEq(registry.resolver(ipId), resolver); + assertEq(totalSupply + 1, registry.totalSupply()); + assertTrue(registry.isRegistered(ipId)); + assertTrue(registry.isRegistered(block.chainid, tokenAddress, tokenId)); + assertTrue(ipAccountRegistry.isRegistered(block.chainid, tokenAddress, tokenId)); + } + + /// @notice Tests registration of IP records with lazy account creation. + function test_IPRecordRegistry_RegisterWithoutAccount() public { + uint256 totalSupply = registry.totalSupply(); + address ipId = _getAccount( + ipAccountImpl, + block.chainid, + tokenAddress, + tokenId, + ipAccountRegistry.IP_ACCOUNT_SALT() + ); + + // Ensure unregistered IP preconditions are satisfied. + assertEq(registry.resolver(ipId), address(0)); + assertTrue(!registry.isRegistered(ipId)); + assertTrue(!ipAccountRegistry.isRegistered(block.chainid, tokenAddress, tokenId)); + + // Ensure all expected events are emitted. + vm.expectEmit(true, true, true, true); + emit IIPRecordRegistry.IPResolverSet(ipId, resolver); + vm.expectEmit(true, true, true, true); + emit IIPRecordRegistry.IPRegistered( + ipId, + block.chainid, + tokenAddress, + tokenId, + resolver + ); + vm.prank(registrationModule); + registry.register( + block.chainid, + tokenAddress, + tokenId, + resolver, + false + ); + + /// Ensures IP record post-registration conditions are met. + assertEq(registry.resolver(ipId), resolver); + assertEq(totalSupply + 1, registry.totalSupply()); + assertTrue(registry.isRegistered(ipId)); + assertTrue(!ipAccountRegistry.isRegistered(block.chainid, tokenAddress, tokenId)); + } + + /// @notice Tests registration of IP records works with existing IP accounts. + function test_IPRecordRegistry_RegisterExistingAccount() public { + address ipId = registry.createIPAccount(block.chainid, tokenAddress, tokenId); + assertTrue(ipAccountRegistry.isRegistered(block.chainid, tokenAddress, tokenId)); + vm.prank(registrationModule); + registry.register( + block.chainid, + tokenAddress, + tokenId, + resolver, + true + ); + + } + + /// @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); + vm.expectRevert(Errors.IPRecordRegistry_AlreadyRegistered.selector); + registry.register(block.chainid, tokenAddress, tokenId, resolver, false); + } + + /// @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); + } + + /// @notice Tests generic IP account creation works. + function test_IPRecordRegistry_CreateIPAccount() public { + assertTrue(!ipAccountRegistry.isRegistered(block.chainid, tokenAddress, tokenId)); + address ipId = registry.createIPAccount(block.chainid, tokenAddress, tokenId); + assertTrue(ipAccountRegistry.isRegistered(block.chainid, tokenAddress, tokenId)); + } + + /// @notice Tests IP account creation reverts if one already exists. + function test_IPRecordRegistry_CreateIPAccount_Reverts_AlreadyCreated() public { + registry.createIPAccount(block.chainid, tokenAddress, tokenId); + vm.expectRevert(Errors.IPRecordRegistry_IPAccountAlreadyCreated.selector); + registry.createIPAccount(block.chainid, tokenAddress, tokenId); + } + + /// @notice Tests IP resolver setting works. + function test_IPRecordRegistry_SetResolver() public { + address ipId = _getAccount( + ipAccountImpl, + block.chainid, + tokenAddress, + tokenId, + ipAccountRegistry.IP_ACCOUNT_SALT() + ); + vm.startPrank(registrationModule); + registry.register(block.chainid, tokenAddress, tokenId, resolver, false); + + vm.expectEmit(true, true, true, true); + emit IIPRecordRegistry.IPResolverSet( + ipId, + resolver2 + ); + registry.setResolver(block.chainid, tokenAddress, tokenId, resolver2); + assertEq(registry.resolver(block.chainid, tokenAddress, tokenId), resolver2); + + // Check that resolvers can be reassigned. + vm.expectEmit(true, true, true, true); + emit IIPRecordRegistry.IPResolverSet( + ipId, + resolver + ); + registry.setResolver(ipId, resolver); + assertEq(registry.resolver(ipId), resolver); + } + + /// @notice Tests IP resolver setting reverts if an IP is not yet registered. + function test_IPRecordRegistry_SetResolver_Reverts_NotYetRegistered() public { + vm.startPrank(registrationModule); + vm.expectRevert(Errors.IPRecordRegistry_NotYetRegistered.selector); + registry.setResolver(block.chainid, tokenAddress, tokenId, resolver); + } + + /// @notice Tests IP resolver setting reverts if the resolver is invalid. + function test_IPRecordRegistry_SetResolver_Reverts_ResolverInvalid() public { + vm.startPrank(registrationModule); + vm.expectRevert(Errors.IPRecordRegistry_ResolverInvalid.selector); + registry.setResolver(block.chainid, tokenAddress, tokenId, address(0)); + } + + /// @notice Helper function for generating an account address. + function _getAccount( + address impl, + uint256 chainId, + address contractAddress, + uint256 contractId, + uint256 salt + ) internal view returns (address) { + return MockERC6551Registry(erc6551Registry).account( + impl, + chainId, + contractAddress, + contractId, + ipAccountRegistry.IP_ACCOUNT_SALT() + ); + } + +} diff --git a/test/foundry/mocks/MockERC6551Registry.sol b/test/foundry/mocks/MockERC6551Registry.sol index e762f75d4..857a7a89f 100644 --- a/test/foundry/mocks/MockERC6551Registry.sol +++ b/test/foundry/mocks/MockERC6551Registry.sol @@ -1,32 +1,12 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.21; +pragma solidity ^0.8.23; + import { Create2 } from "@openzeppelin/contracts/utils/Create2.sol"; +import { IERC6551Registry } from 'contracts/interfaces/erc6551/IERC6551Registry.sol'; -contract MockERC6551Registry { - /** - * @dev The registry SHALL emit the AccountCreated event upon successful account creation - */ - event AccountCreated( - address account, - address indexed implementation, - uint256 chainId, - address indexed tokenContract, - uint256 indexed tokenId, - uint256 salt - ); +contract MockERC6551Registry is IERC6551Registry { - /** - * @dev Creates a token bound account for a non-fungible token - * - * If account has already been created, returns the account address without calling create2 - * - * If initData is not empty and account has not yet been created, calls account with - * provided initData after creation - * - * Emits AccountCreated event - * - * @return the address of the account - */ + /// @notice Creates a new token-bound account for an NFT. function createAccount( address implementation, uint256 chainId, @@ -63,11 +43,7 @@ contract MockERC6551Registry { return _account; } - /** - * @dev Returns the computed token bound account address for a non-fungible token - * - * @return The computed address of the token bound account - */ + /// @notice Retrieves the token-bound account address for an NFT. function account( address implementation, uint256 chainId, diff --git a/test/foundry/mocks/MockERC721.sol b/test/foundry/mocks/MockERC721.sol index 1db8755b1..d9e9c5ca3 100644 --- a/test/foundry/mocks/MockERC721.sol +++ b/test/foundry/mocks/MockERC721.sol @@ -1,12 +1,13 @@ // SPDX-License-Identifier: BUSDL-1.1 -pragma solidity ^0.8.13; +pragma solidity ^0.8.23; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; contract MockERC721 is ERC721 { constructor() ERC721("MockERC721", "M721") {} - function mint(address to, uint256 tokenId) external { + function mint(address to, uint256 tokenId) external returns(uint256) { _safeMint(to, tokenId); + return tokenId; } } diff --git a/test/foundry/mocks/MockModuleRegistry.sol b/test/foundry/mocks/MockModuleRegistry.sol new file mode 100644 index 000000000..4d17bbc53 --- /dev/null +++ b/test/foundry/mocks/MockModuleRegistry.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: BUSDL-1.1 +pragma solidity ^0.8.23; + +import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; +import { REGISTRATION_MODULE_KEY } from "contracts/lib/modules/Module.sol"; + +/// @title Mock Module Registry Contract +contract MockModuleRegistry { + + address public immutable REGISTRATION_MODULE; + + constructor(address registrationModule) { + REGISTRATION_MODULE = registrationModule; + } + + function protocolModule(string memory moduleKey) external view returns (address module) { + if (Strings.equal(moduleKey, REGISTRATION_MODULE_KEY)) { + module = REGISTRATION_MODULE; + } + } +} diff --git a/test/foundry/resolvers/IPMetadataResolver.t.sol b/test/foundry/resolvers/IPMetadataResolver.t.sol new file mode 100644 index 000000000..320f0afab --- /dev/null +++ b/test/foundry/resolvers/IPMetadataResolver.t.sol @@ -0,0 +1,256 @@ +// 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 { IModuleRegistry } from "contracts/interfaces/registries/IModuleRegistry.sol"; +import { IPRecordRegistry } from "contracts/registries/IPRecordRegistry.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 { MockERC6551Registry } from "test/foundry/mocks/MockERC6551Registry.sol"; +import { MockERC721 } from "test/foundry/mocks/MockERC721.sol"; +import { IP } from "contracts/lib/IP.sol"; +import { Errors } from "contracts/lib/Errors.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"; + IP.Category public constant RECORD_CATEGORY = IP.Category.COPYRIGHT; + 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 Placeholder for registration module. + address public registrationModule = vm.addr(0x1337); + + /// @notice The IP record registry. + IPRecordRegistry public registry; + + /// @notice The IP account registry. + IPAccountRegistry public ipAccountRegistry; + + /// @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(); + ipAccountRegistry = new IPAccountRegistry( + address(new MockERC6551Registry()), + address(accessController), + address(new IPAccountImpl()) + ); + registry = new IPRecordRegistry( + address(new MockModuleRegistry(registrationModule)), + address(ipAccountRegistry) + ); + MockERC721 erc721 = new MockERC721(); + vm.prank(alice); + ipResolver = IIPMetadataResolver(_deployResolver()); + uint256 tokenId = erc721.mint(alice, 99); + ipId = registry.ipId(block.chainid, address(erc721), tokenId); + vm.prank(registrationModule); + registry.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 { + accessController.setPolicy(ipId, alice, address(ipResolver), IIPMetadataResolver.setMetadata.selector, 1); + vm.prank(alice); + ipResolver.setMetadata( + ipId, + IP.MetadataRecord({ + name: RECORD_NAME, + category: RECORD_CATEGORY, + description: RECORD_DESCRIPTION, + hash: RECORD_HASH, + registrationDate: RECORD_REGISTRATION_DATE, + registrant: alice, + uri: RECORD_URI + }) + ); + assertEq(ipResolver.name(ipId), RECORD_NAME); + assertTrue(ipResolver.category(ipId) == RECORD_CATEGORY); + 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.tokenURI(ipId), RECORD_URI); + + // Also check the metadata getter returns as expected. + IP.Metadata memory metadata = ipResolver.metadata(ipId); + assertEq(metadata.name, RECORD_NAME); + assertTrue(metadata.category == RECORD_CATEGORY); + 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.IPResolver_Unauthorized.selector); + ipResolver.setMetadata( + ipId, + IP.MetadataRecord({ + name: RECORD_NAME, + category: RECORD_CATEGORY, + 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 { + accessController.setPolicy(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.IPResolver_Unauthorized.selector); + ipResolver.setName(ipId, RECORD_NAME); + } + + /// @notice Tests that the category may be properly set for the resolver. + function test_IPMetadataResolver_SetCategory() public { + accessController.setPolicy(ipId, alice, address(ipResolver), IIPMetadataResolver.setCategory.selector, 1); + vm.prank(alice); + ipResolver.setCategory(ipId, RECORD_CATEGORY); + assertTrue(RECORD_CATEGORY == ipResolver.category(ipId)); + } + + /// @notice Checks that an unauthorized call to setCategory reverts. + function test_IPMetadataResolver_SetCategory_Reverts_Unauthorized() public { + vm.expectRevert(Errors.IPResolver_Unauthorized.selector); + ipResolver.setCategory(ipId, RECORD_CATEGORY); + } + + /// @notice Tests that the description may be properly set for the resolver. + function test_IPMetadataResolver_SetDescription() public { + accessController.setPolicy(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.IPResolver_Unauthorized.selector); + ipResolver.setDescription(ipId, RECORD_DESCRIPTION); + } + + /// @notice Tests that the hash may be properly set for the resolver. + function test_IPMetadataResolver_SetHash() public { + accessController.setPolicy(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.IPResolver_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 { + accessController.setPolicy(ipId, alice, address(ipResolver), IIPMetadataResolver.setTokenURI.selector, 1); + vm.prank(alice); + ipResolver.setTokenURI(ipId, RECORD_URI); + assertEq(ipResolver.tokenURI(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.tokenURI(address(0)), ""); + + // Check default string value for registered IP. + accessController.setPolicy(ipId, alice, address(ipResolver), IIPMetadataResolver.setMetadata.selector, 1); + vm.prank(alice); + ipResolver.setMetadata( + ipId, + IP.MetadataRecord({ + name: RECORD_NAME, + category: RECORD_CATEGORY, + 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": "Category", "value": "Copyright"},', + '{"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.tokenURI(ipId)); + } + + /// @dev Deploys a new IP Metadata Resolver. + function _deployResolver() internal override returns (address) { + return address( + new IPMetadataResolver( + address(accessController), + address(registry), + address(ipAccountRegistry) + ) + ); + } + +} diff --git a/test/foundry/resolvers/ResolverBase.t.sol b/test/foundry/resolvers/ResolverBase.t.sol new file mode 100644 index 000000000..b3c6995b1 --- /dev/null +++ b/test/foundry/resolvers/ResolverBase.t.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import "forge-std/console2.sol"; + +import { MockAccessController } from "test/foundry/mocks/MockAccessController.sol"; +import { BaseTest } from "test/foundry/utils/BaseTest.sol"; +import { IResolver } from "contracts/interfaces/resolvers/IResolver.sol"; +import { IAccessController } from "contracts/interfaces/IAccessController.sol"; +import { Errors } from "contracts/lib/Errors.sol"; + +/// @title Resolver Base Test Contract +/// @notice Base contract for testing standard resolver functionality. +abstract contract ResolverBaseTest is BaseTest { + + /// @notice The access controller address. + IAccessController public accessController; + + /// @notice The resolver SUT. + IResolver public baseResolver; + + /// @notice Initializes the base ERC20 contract for testing. + function setUp() public virtual override(BaseTest) { + BaseTest.setUp(); + accessController = IAccessController(address(new MockAccessController())); + baseResolver = IResolver(_deployResolver()); + } + + /// @notice Tests that the default resolver constructor runs successfully. + function test_Resolver_Constructor() public { + assertEq(baseResolver.accessController(), address(accessController)); + } + + /// @notice Tests that the base resolver interface is supported. + function test_Resolver_SupportsInterface() public virtual { + assertTrue(baseResolver.supportsInterface(type(IResolver).interfaceId)); + } + + /// @dev Deploys the resolver SUT. + function _deployResolver() internal virtual returns (address); + +} diff --git a/test/foundry/utils/BaseTest.sol b/test/foundry/utils/BaseTest.sol new file mode 100644 index 000000000..e68e5f6ea --- /dev/null +++ b/test/foundry/utils/BaseTest.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import "forge-std/Test.sol"; + +/// @title Base Test Contract +/// @notice This contract provides a set of protocol-unrelated testing utilities +/// that may be extended by testing contracts. +contract BaseTest is Test { + + // Test public keys EOAs for deriving reusable EOA addresses. + uint256 internal alicePk = 0xa11ce; + uint256 internal bobPk = 0xb0b; + uint256 internal calPk = 0xca1; + + // Test EOA addresses that may be reused for testing. + address payable internal alice = payable(vm.addr(alicePk)); + address payable internal bob = payable(vm.addr(bobPk)); + address payable internal cal = payable(vm.addr(calPk)); + + /// @notice Sets up the base test contract. + function setUp() public virtual { + vm.label(alice, "alice"); + vm.label(bob, "bob"); + vm.label(cal, "cal"); + } +} From eafd446c75af08a4fdeac4dc3b9aa568543482ab Mon Sep 17 00:00:00 2001 From: Leeren Chang Date: Fri, 19 Jan 2024 22:15:03 -0800 Subject: [PATCH 3/5] Fixes linting --- .../resolvers/IIPMetadataResolver.sol | 9 --- contracts/lib/Errors.sol | 4 +- contracts/lib/IP.sol | 64 ++++++++----------- contracts/registries/IPAccountRegistry.sol | 1 - contracts/registries/IPRecordRegistry.sol | 52 ++++----------- contracts/resolvers/IPMetadataResolver.sol | 44 ++++--------- contracts/resolvers/ResolverBase.sol | 4 +- test/foundry/IPRecordRegistry.t.sol | 27 ++++---- test/foundry/ModuleRegistry.t.sol | 2 +- test/foundry/mocks/MockModuleRegistry.sol | 3 +- .../resolvers/IPMetadataResolver.t.sol | 37 +++-------- 11 files changed, 77 insertions(+), 170 deletions(-) diff --git a/contracts/interfaces/resolvers/IIPMetadataResolver.sol b/contracts/interfaces/resolvers/IIPMetadataResolver.sol index 3a2ef32a4..6f0b3afe6 100644 --- a/contracts/interfaces/resolvers/IIPMetadataResolver.sol +++ b/contracts/interfaces/resolvers/IIPMetadataResolver.sol @@ -15,10 +15,6 @@ interface IIPMetadataResolver is IResolver { /// @param ipId The canonical ID of the specified IP. function name(address ipId) external view returns (string memory); - /// @notice Fetches the category associated with the specified IP. - /// @param ipId The canonical ID of the specified IP. - function category(address ipId) external view returns (IP.Category); - /// @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. @@ -55,11 +51,6 @@ interface IIPMetadataResolver is IResolver { /// @param name The string name to associate with the IP. function setName(address ipId, string calldata name) external; - /// @notice Sets the category associated with an IP. - /// @param ipId The canonical ID of the specified IP. - /// @param category The IP category to associate with the IP. - function setCategory(address ipId, IP.Category category) 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. diff --git a/contracts/lib/Errors.sol b/contracts/lib/Errors.sol index 419f908ef..9a8e1182a 100644 --- a/contracts/lib/Errors.sol +++ b/contracts/lib/Errors.sol @@ -5,7 +5,6 @@ pragma solidity ^0.8.19; /// @title Errors Library /// @notice Library for all Story Protocol contract errors. library Errors { - //////////////////////////////////////////////////////////////////////////// // IPRecordRegistry // //////////////////////////////////////////////////////////////////////////// @@ -38,7 +37,7 @@ library Errors { //////////////////////////////////////////////////////////////////////////// // LicenseRegistry // //////////////////////////////////////////////////////////////////////////// - + /// @notice Error thrown when a policy is already set for an IP ID. error LicenseRegistry__PolicyAlreadySetForIpId(); error LicenseRegistry__FrameworkNotFound(); @@ -71,5 +70,4 @@ library Errors { error AccessController__SignerIsZeroAddress(); error AccessController__CallerIsNotIPAccount(); error AccessController__PermissionIsNotValid(); - } diff --git a/contracts/lib/IP.sol b/contracts/lib/IP.sol index 30d526cb6..78cfc2594 100644 --- a/contracts/lib/IP.sol +++ b/contracts/lib/IP.sol @@ -5,51 +5,39 @@ pragma solidity ^0.8.21; /// @title IP Library /// @notice Library for constants, structs, and helper functions used for IP. library IP { - - /// @notice List of IP categories currently declared in Story Protocol.. - /// @custom:note Later on this may be canonicalized in the licensing module. - enum Category { - PATENT, - TRADEMARK, - COPYRIGHT - } - /// @notice Core metadata associated with an IP. struct Metadata { - address owner; // The current owner of the IP. - string name; // The name associated with the IP. - Category category; // The overarching category the IP belongs to. - string description; // A description associated with the IP. - bytes32 hash; // A keccak-256 hash of the IP content. - uint64 registrationDate; // The date which the IP was registered. - address registrant; // The address of the initial IP registrant. - string uri; // The token URI associated with the IP. + // 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 Resolved attributes not referenced here are processed through /// their corresponding data modules (e.g. licensing for license data). struct MetadataRecord { - string name; // The name associated with the IP. - Category category; // The overarching category the IP belongs to. - string description; // A description associated with the IP. - bytes32 hash; // A keccak-256 hash of the IP content. - uint64 registrationDate; // The date which the IP was registered. - address registrant; // The address of the initial IP registrant. - string uri; // The token URI associated with the IP. + // 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 Converts a custom IP category type to its string representation. - /// @param category The category of the IP. - function toString(Category category) internal pure returns (string memory) { - if (category == Category.PATENT) { - return "Patent"; - } else if (category == Category.TRADEMARK) { - return "Trademark"; - } else { - return "Copyright"; - } - } - } diff --git a/contracts/registries/IPAccountRegistry.sol b/contracts/registries/IPAccountRegistry.sol index bebbac5de..03f2e9e7c 100644 --- a/contracts/registries/IPAccountRegistry.sol +++ b/contracts/registries/IPAccountRegistry.sol @@ -9,7 +9,6 @@ import { IERC6551Registry } from "lib/reference/src/interfaces/IERC6551Registry. /// @notice This contract is responsible for managing the registration and tracking of IP Accounts. /// It leverages a public ERC6551 registry to deploy IPAccount contracts. contract IPAccountRegistry is IIPAccountRegistry { - address public immutable IP_ACCOUNT_IMPL; bytes32 public immutable IP_ACCOUNT_SALT; address public immutable ERC6551_PUBLIC_REGISTRY; diff --git a/contracts/registries/IPRecordRegistry.sol b/contracts/registries/IPRecordRegistry.sol index a30318285..2e8b5fc1d 100644 --- a/contracts/registries/IPRecordRegistry.sol +++ b/contracts/registries/IPRecordRegistry.sol @@ -9,17 +9,16 @@ import { IModuleRegistry } from "contracts/interfaces/registries/IModuleRegistry import { Errors } from "contracts/lib/Errors.sol"; /// @title IP Record Registry -/// @notice This contract acts as the source of truth for all IP registered in +/// @notice This contract acts as the source of truth for all IP registered in /// Story Protocol. An IP is identified by its contract address, token /// id, and coin type, meaning any NFT may be conceptualized as an IP. /// Once an IP is registered into the protocol, a corresponding IP /// record is generated, which references an IP resolver for metadata -/// attribution and an IP account for protocol authorization. Only +/// attribution and an IP account for protocol authorization. Only /// approved registration modules may register IP into this registry. /// 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 Gets the factory contract used for IP account creation. IIPAccountRegistry public immutable IP_ACCOUNT_REGISTRY; @@ -34,7 +33,7 @@ contract IPRecordRegistry is IIPRecordRegistry { /// @notice Restricts calls to only originate from the registration module. modifier onlyRegistrationModule() { - if (address(MODULE_REGISTRY.protocolModule(REGISTRATION_MODULE_KEY)) != msg.sender) { + if (address(MODULE_REGISTRY.getModule(REGISTRATION_MODULE_KEY)) != msg.sender) { revert Errors.IPRecordRegistry_Unauthorized(); } _; @@ -54,20 +53,14 @@ contract IPRecordRegistry is IIPRecordRegistry { /// @param tokenContract The address of the IP. /// @param tokenId The token identifier of the IP. /// @return The IP's canonical address identifier. - function ipId( - uint256 chainId, - address tokenContract, - uint256 tokenId - ) public view returns (address) { + function ipId(uint256 chainId, address tokenContract, uint256 tokenId) public view returns (address) { return IP_ACCOUNT_REGISTRY.ipAccount(chainId, tokenContract, tokenId); } /// @notice Checks whether an IP was registered based on its ID. /// @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) { + function isRegistered(address id) external view returns (bool) { return _resolvers[id] != address(0); } @@ -76,11 +69,7 @@ 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. - function isRegistered( - uint256 chainId, - address tokenContract, - uint256 tokenId - ) external view returns (bool) { + function isRegistered(uint256 chainId, address tokenContract, uint256 tokenId) external view returns (bool) { address id = ipId(chainId, tokenContract, tokenId); return _resolvers[id] != address(0); } @@ -88,9 +77,7 @@ contract IPRecordRegistry is IIPRecordRegistry { /// @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) { + function resolver(address id) external view returns (address) { return _resolvers[id]; } @@ -99,11 +86,7 @@ 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. - function resolver( - uint256 chainId, - address tokenContract, - uint256 tokenId - ) external view returns (address) { + function resolver(uint256 chainId, address tokenContract, uint256 tokenId) external view returns (address) { address id = ipId(chainId, tokenContract, tokenId); return _resolvers[id]; } @@ -125,7 +108,7 @@ contract IPRecordRegistry is IIPRecordRegistry { revert Errors.IPRecordRegistry_AlreadyRegistered(); } - // This is to emphasize the semantic differences between utilizing the + // This is to emphasize the semantic differences between utilizing the // IP account as an identifier versus as an account used for auth. address account = id; @@ -145,11 +128,7 @@ 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. - function createIPAccount( - uint256 chainId, - address tokenContract, - uint256 tokenId - ) external returns (address) { + function createIPAccount(uint256 chainId, address tokenContract, uint256 tokenId) external returns (address) { address account = IP_ACCOUNT_REGISTRY.ipAccount(chainId, tokenContract, tokenId); if (account.code.length != 0) { revert Errors.IPRecordRegistry_IPAccountAlreadyCreated(); @@ -175,10 +154,7 @@ contract IPRecordRegistry is IIPRecordRegistry { /// @notice Sets the resolver for an IP based on its canonical ID. /// @param id The canonical ID of the IP. /// @param resolverAddr The address of the resolver being set. - function setResolver( - address id, - address resolverAddr - ) public onlyRegistrationModule { + function setResolver(address id, address resolverAddr) public onlyRegistrationModule { if (resolverAddr == address(0)) { revert Errors.IPRecordRegistry_ResolverInvalid(); } @@ -204,12 +180,8 @@ contract IPRecordRegistry is IIPRecordRegistry { /// @dev Sets the resolver for the specified IP. /// @param id The canonical identifier for the specified IP. /// @param resolverAddr The address of the IP resolver. - function _setResolver( - address id, - address resolverAddr - ) internal { + function _setResolver(address id, address resolverAddr) internal { _resolvers[id] = resolverAddr; emit IPResolverSet(id, resolverAddr); } - } diff --git a/contracts/resolvers/IPMetadataResolver.sol b/contracts/resolvers/IPMetadataResolver.sol index a6e4846bf..2d110cf84 100644 --- a/contracts/resolvers/IPMetadataResolver.sol +++ b/contracts/resolvers/IPMetadataResolver.sol @@ -16,7 +16,6 @@ import { IP } from "contracts/lib/IP.sol"; /// 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; @@ -34,16 +33,16 @@ contract IPMetadataResolver is IIPMetadataResolver, ResolverBase { /// @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, - category: record.category, - description: record.description, - hash: record.hash, - registrationDate: record.registrationDate, - registrant: record.registrant, - uri: tokenURI(ipId) - }); + return + IP.Metadata({ + owner: owner(ipId), + name: record.name, + description: record.description, + hash: record.hash, + registrationDate: record.registrationDate, + registrant: record.registrant, + uri: tokenURI(ipId) + }); } /// @notice Fetches the canonical name associated with the specified IP. @@ -52,12 +51,6 @@ contract IPMetadataResolver is IIPMetadataResolver, ResolverBase { return _records[ipId].name; } - /// @notice Fetches the category associated with an IP. - /// @param ipId The canonical ID of the specified IP. - function category(address ipId) external view returns (IP.Category) { - return _records[ipId].category; - } - /// @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. @@ -103,7 +96,7 @@ contract IPMetadataResolver is IIPMetadataResolver, ResolverBase { IP.MetadataRecord memory record = _records[ipId]; string memory uri = record.uri; - if (bytes(uri).length > 0) { + if (bytes(uri).length > 0) { return uri; } @@ -124,13 +117,6 @@ contract IPMetadataResolver is IIPMetadataResolver, ResolverBase { _records[ipId].name = newName; } - /// @notice Sets the category associated with an IP. - /// @param ipId The canonical ID of the specified IP. - /// @param newCategory The IP category to associate with the IP. - function setCategory(address ipId, IP.Category newCategory) external onlyAuthorized(ipId) { - _records[ipId].category = newCategory; - } - /// @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. @@ -156,8 +142,7 @@ contract IPMetadataResolver is IIPMetadataResolver, ResolverBase { /// @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); + return id == type(IIPMetadataResolver).interfaceId || super.supportsInterface(id); } /// @dev Internal function for generating a default IP URI if not provided. @@ -185,9 +170,6 @@ contract IPMetadataResolver is IIPMetadataResolver, ResolverBase { '{"trait_type": "Owner", "value": "', Strings.toHexString(uint160(owner(ipId)), 20), '"},' - '{"trait_type": "Category", "value": "', - IP.toString(record.category), - '"},', '{"trait_type": "Registrant", "value": "', Strings.toHexString(uint160(record.registrant), 20), '"},', @@ -208,7 +190,5 @@ contract IPMetadataResolver is IIPMetadataResolver, ResolverBase { Base64.encode(bytes(string(abi.encodePacked(baseJson, ipAttributes, "]}")))) ) ); - } - } diff --git a/contracts/resolvers/ResolverBase.sol b/contracts/resolvers/ResolverBase.sol index 438da1bdd..7fb8d9198 100644 --- a/contracts/resolvers/ResolverBase.sol +++ b/contracts/resolvers/ResolverBase.sol @@ -10,7 +10,6 @@ import { Errors } from "contracts/lib/Errors.sol"; /// @notice IP Resolver Base Contract abstract contract ResolverBase is IResolver { - /// @notice Gets the protocol-wide module access controller. IAccessController public immutable ACCESS_CONTROLLER; @@ -23,7 +22,7 @@ abstract contract ResolverBase is IResolver { /// @notice Checks if IP identified by ipId is authorized to perform a call. /// @param ipId The identifier for the IP being authorized. modifier onlyAuthorized(address ipId) { - if (!ACCESS_CONTROLLER.checkPolicy(ipId, msg.sender, address(this), msg.sig)) { + if (!ACCESS_CONTROLLER.checkPermission(ipId, msg.sender, address(this), msg.sig)) { revert Errors.IPResolver_Unauthorized(); } _; @@ -51,5 +50,4 @@ abstract contract ResolverBase is IResolver { function supportsInterface(bytes4 id) public view virtual override returns (bool) { return id == type(IResolver).interfaceId; } - } diff --git a/test/foundry/IPRecordRegistry.t.sol b/test/foundry/IPRecordRegistry.t.sol index 64d608689..12f96a34e 100644 --- a/test/foundry/IPRecordRegistry.t.sol +++ b/test/foundry/IPRecordRegistry.t.sol @@ -4,13 +4,14 @@ pragma solidity ^0.8.23; 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 { IPAccountRegistry } from "contracts/registries/IPAccountRegistry.sol"; +import { ERC6551Registry } from "lib/reference/src/ERC6551Registry.sol"; import { IPRecordRegistry } from "contracts/registries/IPRecordRegistry.sol"; import { IPAccountImpl} from "contracts/IPAccountImpl.sol"; import { MockAccessController } from "test/foundry/mocks/MockAccessController.sol"; import { MockModuleRegistry } from "test/foundry/mocks/MockModuleRegistry.sol"; import { MockERC721 } from "test/foundry/mocks/MockERC721.sol"; -import { MockERC6551Registry } from "test/foundry/mocks/MockERC6551Registry.sol"; import { Errors } from "contracts/lib/Errors.sol"; /// @title IP Record Registry Testing Contract @@ -49,7 +50,7 @@ contract IPRecordRegistryTest is BaseTest { function setUp() public virtual override { BaseTest.setUp(); address accessController = address(new MockAccessController()); - erc6551Registry = address(new MockERC6551Registry()); + erc6551Registry = address(new ERC6551Registry()); moduleRegistry = IModuleRegistry( address(new MockModuleRegistry(registrationModule)) ); @@ -103,7 +104,7 @@ contract IPRecordRegistryTest is BaseTest { assertEq(registry.resolver(ipId), address(0)); assertTrue(!registry.isRegistered(ipId)); assertTrue(!registry.isRegistered(block.chainid, tokenAddress, tokenId)); - assertTrue(!ipAccountRegistry.isRegistered(block.chainid, tokenAddress, tokenId)); + assertTrue(!IPAccountChecker.isRegistered(ipAccountRegistry, block.chainid, tokenAddress, tokenId)); // Ensure all expected events are emitted. vm.expectEmit(true, true, true, true); @@ -137,7 +138,7 @@ contract IPRecordRegistryTest is BaseTest { assertEq(totalSupply + 1, registry.totalSupply()); assertTrue(registry.isRegistered(ipId)); assertTrue(registry.isRegistered(block.chainid, tokenAddress, tokenId)); - assertTrue(ipAccountRegistry.isRegistered(block.chainid, tokenAddress, tokenId)); + assertTrue(IPAccountChecker.isRegistered(ipAccountRegistry, block.chainid, tokenAddress, tokenId)); } /// @notice Tests registration of IP records with lazy account creation. @@ -154,7 +155,7 @@ contract IPRecordRegistryTest is BaseTest { // Ensure unregistered IP preconditions are satisfied. assertEq(registry.resolver(ipId), address(0)); assertTrue(!registry.isRegistered(ipId)); - assertTrue(!ipAccountRegistry.isRegistered(block.chainid, tokenAddress, tokenId)); + assertTrue(!IPAccountChecker.isRegistered(ipAccountRegistry, block.chainid, tokenAddress, tokenId)); // Ensure all expected events are emitted. vm.expectEmit(true, true, true, true); @@ -180,13 +181,13 @@ contract IPRecordRegistryTest is BaseTest { assertEq(registry.resolver(ipId), resolver); assertEq(totalSupply + 1, registry.totalSupply()); assertTrue(registry.isRegistered(ipId)); - assertTrue(!ipAccountRegistry.isRegistered(block.chainid, tokenAddress, tokenId)); + assertTrue(!IPAccountChecker.isRegistered(ipAccountRegistry, block.chainid, tokenAddress, tokenId)); } /// @notice Tests registration of IP records works with existing IP accounts. function test_IPRecordRegistry_RegisterExistingAccount() public { address ipId = registry.createIPAccount(block.chainid, tokenAddress, tokenId); - assertTrue(ipAccountRegistry.isRegistered(block.chainid, tokenAddress, tokenId)); + assertTrue(IPAccountChecker.isRegistered(ipAccountRegistry, block.chainid, tokenAddress, tokenId)); vm.prank(registrationModule); registry.register( block.chainid, @@ -215,9 +216,9 @@ contract IPRecordRegistryTest is BaseTest { /// @notice Tests generic IP account creation works. function test_IPRecordRegistry_CreateIPAccount() public { - assertTrue(!ipAccountRegistry.isRegistered(block.chainid, tokenAddress, tokenId)); + assertTrue(!IPAccountChecker.isRegistered(ipAccountRegistry, block.chainid, tokenAddress, tokenId)); address ipId = registry.createIPAccount(block.chainid, tokenAddress, tokenId); - assertTrue(ipAccountRegistry.isRegistered(block.chainid, tokenAddress, tokenId)); + assertTrue(IPAccountChecker.isRegistered(ipAccountRegistry, block.chainid, tokenAddress, tokenId)); } /// @notice Tests IP account creation reverts if one already exists. @@ -277,14 +278,14 @@ contract IPRecordRegistryTest is BaseTest { uint256 chainId, address contractAddress, uint256 contractId, - uint256 salt + bytes32 salt ) internal view returns (address) { - return MockERC6551Registry(erc6551Registry).account( + return ERC6551Registry(erc6551Registry).account( impl, + salt, chainId, contractAddress, - contractId, - ipAccountRegistry.IP_ACCOUNT_SALT() + contractId ); } diff --git a/test/foundry/ModuleRegistry.t.sol b/test/foundry/ModuleRegistry.t.sol index 8a6bf4041..aa856ce98 100644 --- a/test/foundry/ModuleRegistry.t.sol +++ b/test/foundry/ModuleRegistry.t.sol @@ -8,7 +8,7 @@ import "contracts/IPAccountImpl.sol"; import "contracts/interfaces/IIPAccount.sol"; import "lib/reference/src/interfaces/IERC6551Account.sol"; import "test/foundry/mocks/MockERC721.sol"; -import "test/foundry/mocks/MockERC6551Registry.sol"; +import { MockERC6551Registry} from "test/foundry/mocks/MockERC6551Registry.sol"; import "test/foundry/mocks/MockAccessController.sol"; import "test/foundry/mocks/MockModule.sol"; import "contracts/registries/ModuleRegistry.sol"; diff --git a/test/foundry/mocks/MockModuleRegistry.sol b/test/foundry/mocks/MockModuleRegistry.sol index 4d17bbc53..c34ba289f 100644 --- a/test/foundry/mocks/MockModuleRegistry.sol +++ b/test/foundry/mocks/MockModuleRegistry.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.23; import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; +import { IModuleRegistry } from "contracts/interfaces/registries/IModuleRegistry.sol"; import { REGISTRATION_MODULE_KEY } from "contracts/lib/modules/Module.sol"; /// @title Mock Module Registry Contract @@ -13,7 +14,7 @@ contract MockModuleRegistry { REGISTRATION_MODULE = registrationModule; } - function protocolModule(string memory moduleKey) external view returns (address module) { + function getModule(string memory moduleKey) external view returns (address module) { if (Strings.equal(moduleKey, REGISTRATION_MODULE_KEY)) { module = REGISTRATION_MODULE; } diff --git a/test/foundry/resolvers/IPMetadataResolver.t.sol b/test/foundry/resolvers/IPMetadataResolver.t.sol index 320f0afab..db1b6fa90 100644 --- a/test/foundry/resolvers/IPMetadataResolver.t.sol +++ b/test/foundry/resolvers/IPMetadataResolver.t.sol @@ -7,6 +7,7 @@ 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 { IPAccountRegistry } from "contracts/registries/IPAccountRegistry.sol"; @@ -14,7 +15,6 @@ 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 { MockERC6551Registry } from "test/foundry/mocks/MockERC6551Registry.sol"; import { MockERC721 } from "test/foundry/mocks/MockERC721.sol"; import { IP } from "contracts/lib/IP.sol"; import { Errors } from "contracts/lib/Errors.sol"; @@ -25,7 +25,6 @@ contract IPMetadataResolverTest is ResolverBaseTest { // Default IP record attributes. string public constant RECORD_NAME = "IPRecord"; - IP.Category public constant RECORD_CATEGORY = IP.Category.COPYRIGHT; string public constant RECORD_DESCRIPTION = "IPs all the way down."; bytes32 public constant RECORD_HASH = ""; uint64 public constant RECORD_REGISTRATION_DATE = 999999; @@ -50,7 +49,7 @@ contract IPMetadataResolverTest is ResolverBaseTest { function setUp() public virtual override(ResolverBaseTest) { ResolverBaseTest.setUp(); ipAccountRegistry = new IPAccountRegistry( - address(new MockERC6551Registry()), + address(new ERC6551Registry()), address(accessController), address(new IPAccountImpl()) ); @@ -80,13 +79,12 @@ contract IPMetadataResolverTest is ResolverBaseTest { /// @notice Tests that metadata may be properly set for the resolver. function test_IPMetadataResolver_SetMetadata() public { - accessController.setPolicy(ipId, alice, address(ipResolver), IIPMetadataResolver.setMetadata.selector, 1); + accessController.setPermission(ipId, alice, address(ipResolver), IIPMetadataResolver.setMetadata.selector, 1); vm.prank(alice); ipResolver.setMetadata( ipId, IP.MetadataRecord({ name: RECORD_NAME, - category: RECORD_CATEGORY, description: RECORD_DESCRIPTION, hash: RECORD_HASH, registrationDate: RECORD_REGISTRATION_DATE, @@ -95,7 +93,6 @@ contract IPMetadataResolverTest is ResolverBaseTest { }) ); assertEq(ipResolver.name(ipId), RECORD_NAME); - assertTrue(ipResolver.category(ipId) == RECORD_CATEGORY); assertEq(ipResolver.description(ipId), RECORD_DESCRIPTION); assertEq(ipResolver.hash(ipId), RECORD_HASH); assertEq(ipResolver.registrationDate(ipId), RECORD_REGISTRATION_DATE); @@ -106,7 +103,6 @@ contract IPMetadataResolverTest is ResolverBaseTest { // Also check the metadata getter returns as expected. IP.Metadata memory metadata = ipResolver.metadata(ipId); assertEq(metadata.name, RECORD_NAME); - assertTrue(metadata.category == RECORD_CATEGORY); assertEq(metadata.description, RECORD_DESCRIPTION); assertEq(metadata.hash, RECORD_HASH); assertEq(metadata.registrationDate, RECORD_REGISTRATION_DATE); @@ -122,7 +118,6 @@ contract IPMetadataResolverTest is ResolverBaseTest { ipId, IP.MetadataRecord({ name: RECORD_NAME, - category: RECORD_CATEGORY, description: RECORD_DESCRIPTION, hash: RECORD_HASH, registrationDate: RECORD_REGISTRATION_DATE, @@ -134,7 +129,7 @@ contract IPMetadataResolverTest is ResolverBaseTest { /// @notice Tests that the name may be properly set for the resolver. function test_IPMetadataResolver_SetName() public { - accessController.setPolicy(ipId, alice, address(ipResolver), IIPMetadataResolver.setName.selector, 1); + accessController.setPermission(ipId, alice, address(ipResolver), IIPMetadataResolver.setName.selector, 1); vm.prank(alice); ipResolver.setName(ipId, RECORD_NAME); assertEq(RECORD_NAME, ipResolver.name(ipId)); @@ -146,23 +141,9 @@ contract IPMetadataResolverTest is ResolverBaseTest { ipResolver.setName(ipId, RECORD_NAME); } - /// @notice Tests that the category may be properly set for the resolver. - function test_IPMetadataResolver_SetCategory() public { - accessController.setPolicy(ipId, alice, address(ipResolver), IIPMetadataResolver.setCategory.selector, 1); - vm.prank(alice); - ipResolver.setCategory(ipId, RECORD_CATEGORY); - assertTrue(RECORD_CATEGORY == ipResolver.category(ipId)); - } - - /// @notice Checks that an unauthorized call to setCategory reverts. - function test_IPMetadataResolver_SetCategory_Reverts_Unauthorized() public { - vm.expectRevert(Errors.IPResolver_Unauthorized.selector); - ipResolver.setCategory(ipId, RECORD_CATEGORY); - } - /// @notice Tests that the description may be properly set for the resolver. function test_IPMetadataResolver_SetDescription() public { - accessController.setPolicy(ipId, alice, address(ipResolver), IIPMetadataResolver.setDescription.selector, 1); + accessController.setPermission(ipId, alice, address(ipResolver), IIPMetadataResolver.setDescription.selector, 1); vm.prank(alice); ipResolver.setDescription(ipId, RECORD_DESCRIPTION); assertEq(RECORD_DESCRIPTION, ipResolver.description(ipId)); @@ -176,7 +157,7 @@ contract IPMetadataResolverTest is ResolverBaseTest { /// @notice Tests that the hash may be properly set for the resolver. function test_IPMetadataResolver_SetHash() public { - accessController.setPolicy(ipId, alice, address(ipResolver), IIPMetadataResolver.setHash.selector, 1); + accessController.setPermission(ipId, alice, address(ipResolver), IIPMetadataResolver.setHash.selector, 1); vm.prank(alice); ipResolver.setHash(ipId, RECORD_HASH); assertEq(RECORD_HASH, ipResolver.hash(ipId)); @@ -197,7 +178,7 @@ contract IPMetadataResolverTest is ResolverBaseTest { /// @notice Checks setting token URI works as expected. function test_IPMetadataResolver_SetTokenURI() public { - accessController.setPolicy(ipId, alice, address(ipResolver), IIPMetadataResolver.setTokenURI.selector, 1); + accessController.setPermission(ipId, alice, address(ipResolver), IIPMetadataResolver.setTokenURI.selector, 1); vm.prank(alice); ipResolver.setTokenURI(ipId, RECORD_URI); assertEq(ipResolver.tokenURI(ipId), RECORD_URI); @@ -209,13 +190,12 @@ contract IPMetadataResolverTest is ResolverBaseTest { assertEq(ipResolver.tokenURI(address(0)), ""); // Check default string value for registered IP. - accessController.setPolicy(ipId, alice, address(ipResolver), IIPMetadataResolver.setMetadata.selector, 1); + assertTrue(accessController.checkPermission(ipId, alice, address(ipResolver), IIPMetadataResolver.setMetadata.selector)); vm.prank(alice); ipResolver.setMetadata( ipId, IP.MetadataRecord({ name: RECORD_NAME, - category: RECORD_CATEGORY, description: RECORD_DESCRIPTION, hash: RECORD_HASH, registrationDate: RECORD_REGISTRATION_DATE, @@ -229,7 +209,6 @@ contract IPMetadataResolverTest is ResolverBaseTest { '{"name": "IP Asset #', ipIdStr, '", "description": "IPs all the way down.", "attributes": [', '{"trait_type": "Name", "value": "IPRecord"},', '{"trait_type": "Owner", "value": "', ownerStr, '"},' - '{"trait_type": "Category", "value": "Copyright"},', '{"trait_type": "Registrant", "value": "', ownerStr, '"},', '{"trait_type": "Hash", "value": "0x0000000000000000000000000000000000000000000000000000000000000000"},', '{"trait_type": "Registration Date", "value": "', Strings.toString(RECORD_REGISTRATION_DATE), '"}', From dba4d74081a6cb05f21d0039e746c93336ef6fda Mon Sep 17 00:00:00 2001 From: Leeren Chang Date: Fri, 19 Jan 2024 22:32:50 -0800 Subject: [PATCH 4/5] Fixes PR reviews --- .../interfaces/erc6551/IERC6551Registry.sol | 34 ------ contracts/lib/IP.sol | 8 +- contracts/registries/IPRecordRegistry.sol | 2 + test/foundry/ModuleRegistry.t.sol | 4 +- test/foundry/mocks/MockERC6551Registry.sol | 107 ------------------ 5 files changed, 10 insertions(+), 145 deletions(-) delete mode 100644 contracts/interfaces/erc6551/IERC6551Registry.sol delete mode 100644 test/foundry/mocks/MockERC6551Registry.sol diff --git a/contracts/interfaces/erc6551/IERC6551Registry.sol b/contracts/interfaces/erc6551/IERC6551Registry.sol deleted file mode 100644 index 4ef04972e..000000000 --- a/contracts/interfaces/erc6551/IERC6551Registry.sol +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -interface IERC6551Registry { - - /// @notice Emits when a new ERC6551 account is created. - event AccountCreated( - address account, - address indexed implementation, - uint256 chainId, - address indexed tokenContract, - uint256 indexed tokenId, - uint256 salt - ); - - /// @notice Creates a new token-bound account for an NFT. - function createAccount( - address implementation, - uint256 chainId, - address tokenContract, - uint256 tokenId, - uint256 seed, - bytes calldata initData - ) external returns (address); - - /// @notice Retrieves the token-bound account address for an NFT. - function account( - address implementation, - uint256 chainId, - address tokenContract, - uint256 tokenId, - uint256 salt - ) external view returns (address); -} diff --git a/contracts/lib/IP.sol b/contracts/lib/IP.sol index 78cfc2594..341ddef4a 100644 --- a/contracts/lib/IP.sol +++ b/contracts/lib/IP.sol @@ -6,6 +6,9 @@ pragma solidity ^0.8.21; /// @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. struct Metadata { // The current owner of the IP. address owner; @@ -24,8 +27,9 @@ library IP { } /// @notice Core metadata exclusively saved by the IP resolver. - /// @dev Resolved attributes not referenced here are processed through - /// their corresponding data modules (e.g. licensing for license data). + /// @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; diff --git a/contracts/registries/IPRecordRegistry.sol b/contracts/registries/IPRecordRegistry.sol index 2e8b5fc1d..4ca03ddad 100644 --- a/contracts/registries/IPRecordRegistry.sol +++ b/contracts/registries/IPRecordRegistry.sol @@ -130,6 +130,7 @@ contract IPRecordRegistry is IIPRecordRegistry { /// @param tokenId The token identifier of the NFT. 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. if (account.code.length != 0) { revert Errors.IPRecordRegistry_IPAccountAlreadyCreated(); } @@ -158,6 +159,7 @@ contract IPRecordRegistry is IIPRecordRegistry { if (resolverAddr == address(0)) { revert Errors.IPRecordRegistry_ResolverInvalid(); } + // Resolvers may not be set unless the IP was registered into the protocol. if (_resolvers[id] == address(0)) { revert Errors.IPRecordRegistry_NotYetRegistered(); } diff --git a/test/foundry/ModuleRegistry.t.sol b/test/foundry/ModuleRegistry.t.sol index aa856ce98..13cd53a9d 100644 --- a/test/foundry/ModuleRegistry.t.sol +++ b/test/foundry/ModuleRegistry.t.sol @@ -8,7 +8,7 @@ import "contracts/IPAccountImpl.sol"; import "contracts/interfaces/IIPAccount.sol"; import "lib/reference/src/interfaces/IERC6551Account.sol"; import "test/foundry/mocks/MockERC721.sol"; -import { MockERC6551Registry} from "test/foundry/mocks/MockERC6551Registry.sol"; +import { ERC6551Registry } from "lib/reference/src/ERC6551Registry.sol"; import "test/foundry/mocks/MockAccessController.sol"; import "test/foundry/mocks/MockModule.sol"; import "contracts/registries/ModuleRegistry.sol"; @@ -18,7 +18,7 @@ contract ModuleRegistryTest is Test { IPAccountRegistry public registry; IPAccountImpl public implementation; ModuleRegistry public moduleRegistry = new ModuleRegistry(); - MockERC6551Registry public erc6551Registry = new MockERC6551Registry(); + ERC6551Registry public erc6551Registry = new ERC6551Registry(); MockAccessController public accessController = new MockAccessController(); MockModule public module; diff --git a/test/foundry/mocks/MockERC6551Registry.sol b/test/foundry/mocks/MockERC6551Registry.sol deleted file mode 100644 index 857a7a89f..000000000 --- a/test/foundry/mocks/MockERC6551Registry.sol +++ /dev/null @@ -1,107 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.23; - -import { Create2 } from "@openzeppelin/contracts/utils/Create2.sol"; -import { IERC6551Registry } from 'contracts/interfaces/erc6551/IERC6551Registry.sol'; - -contract MockERC6551Registry is IERC6551Registry { - - /// @notice Creates a new token-bound account for an NFT. - function createAccount( - address implementation, - uint256 chainId, - address tokenContract, - uint256 tokenId, - uint256 salt, - bytes calldata initData - ) external returns (address) { - bytes memory code = _getCreationCode( - implementation, - chainId, - tokenContract, - tokenId, - salt - ); - - address _account = Create2.computeAddress(bytes32(salt), keccak256(code)); - - if (_account.code.length != 0) return _account; - - emit AccountCreated(_account, implementation, chainId, tokenContract, tokenId, salt); - - _account = Create2.deploy(0, bytes32(salt), code); - - if (initData.length != 0) { - (bool success, bytes memory result) = _account.call(initData); - if (!success) { - assembly { - revert(add(result, 32), mload(result)) - } - } - } - - return _account; - } - - /// @notice Retrieves the token-bound account address for an NFT. - function account( - address implementation, - uint256 chainId, - address tokenContract, - uint256 tokenId, - uint256 salt - ) external view returns (address) { - bytes32 bytecodeHash = keccak256( - _getCreationCode( - implementation, - chainId, - tokenContract, - tokenId, - salt - ) - ); - - return Create2.computeAddress(bytes32(salt), bytecodeHash); - } - - function _getCreationCode( - address implementation_, - uint256 chainId_, - address tokenContract_, - uint256 tokenId_, - uint256 salt_ - ) internal pure returns (bytes memory) { - return - // Proxy that delegate call to IPAccountProxy - // | 0x00000000 36 calldatasize cds - // | 0x00000001 3d returndatasize 0 cds - // | 0x00000002 3d returndatasize 0 0 cds - // | 0x00000003 37 calldatacopy - // | 0x00000004 3d returndatasize 0 - // | 0x00000005 3d returndatasize 0 0 - // | 0x00000006 3d returndatasize 0 0 0 - // | 0x00000007 36 calldatasize cds 0 0 0 - // | 0x00000008 3d returndatasize 0 cds 0 0 0 - // | 0x00000009 73bebebebebe. push20 0xbebebebe 0xbebe 0 cds 0 0 0 - // | 0x0000001e 5a gas gas 0xbebe 0 cds 0 0 0 - // | 0x0000001f f4 delegatecall suc 0 - // | 0x00000020 3d returndatasize rds suc 0 - // | 0x00000021 82 dup3 0 rds suc 0 - // | 0x00000022 80 dup1 0 0 rds suc 0 - // | 0x00000023 3e returndatacopy suc 0 - // | 0x00000024 90 swap1 0 suc - // | 0x00000025 3d returndatasize rds 0 suc - // | 0x00000026 91 swap2 suc 0 rds - // | 0x00000027 602b push1 0x2b 0x2b suc 0 rds - // | ,=< 0x00000029 57 jumpi 0 rds - // | | 0x0000002a fd revert - // | `-> 0x0000002b 5b jumpdest 0 rds - // \ 0x0000002c f3 return - abi.encodePacked( - hex"3d60ad80600a3d3981f3363d3d373d3d3d363d73", - implementation_, - hex"5af43d82803e903d91602b57fd5bf3", - abi.encode(salt_, chainId_, tokenContract_, tokenId_) - ); - } -} From 0b3cc8aeee67c2490d4caa869cf0be329fd3ff8f Mon Sep 17 00:00:00 2001 From: Leeren Chang Date: Fri, 19 Jan 2024 22:42:23 -0800 Subject: [PATCH 5/5] Change naming of tokenURI to simply URI --- .../resolvers/IIPMetadataResolver.sol | 8 ++++---- contracts/resolvers/IPMetadataResolver.sol | 20 +++++++++---------- .../resolvers/IPMetadataResolver.t.sol | 12 +++++------ 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/contracts/interfaces/resolvers/IIPMetadataResolver.sol b/contracts/interfaces/resolvers/IIPMetadataResolver.sol index 6f0b3afe6..81a74317e 100644 --- a/contracts/interfaces/resolvers/IIPMetadataResolver.sol +++ b/contracts/interfaces/resolvers/IIPMetadataResolver.sol @@ -37,9 +37,9 @@ interface IIPMetadataResolver is IResolver { /// @param ipId The canonical ID of the specified IP. function owner(address ipId) external view returns (address); - /// @notice Fetches the token URI associated with the IP. + /// @notice Fetches an IP owner defined URI associated with the IP. /// @param ipId The canonical ID of the specified IP. - function tokenURI(address ipId) external view returns (string memory); + 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. @@ -61,8 +61,8 @@ interface IIPMetadataResolver is IResolver { /// @param hash The keccak-256 hash to associate with the IP. function setHash(address ipId, bytes32 hash) external; - /// @notice Sets a token URI to associated with the IP. + /// @notice Sets an IP owner defined URI to associate with the IP. /// @param ipId The canonical ID of the specified IP. - function setTokenURI(address ipId, string calldata tokenURI) external; + function setURI(address ipId, string calldata uri) external; } diff --git a/contracts/resolvers/IPMetadataResolver.sol b/contracts/resolvers/IPMetadataResolver.sol index 2d110cf84..c3090c036 100644 --- a/contracts/resolvers/IPMetadataResolver.sol +++ b/contracts/resolvers/IPMetadataResolver.sol @@ -41,7 +41,7 @@ contract IPMetadataResolver is IIPMetadataResolver, ResolverBase { hash: record.hash, registrationDate: record.registrationDate, registrant: record.registrant, - uri: tokenURI(ipId) + uri: uri(ipId) }); } @@ -86,18 +86,18 @@ contract IPMetadataResolver is IIPMetadataResolver, ResolverBase { return IIPAccount(payable(ipId)).owner(); } - /// @notice Fetches the token URI associated with the IP. + /// @notice Fetches an IP owner defined URI associated with the IP. /// @param ipId The canonical ID of the specified IP. - function tokenURI(address ipId) public view returns (string memory) { + function uri(address ipId) public view returns (string memory) { if (!IP_RECORD_REGISTRY.isRegistered(ipId)) { return ""; } IP.MetadataRecord memory record = _records[ipId]; - string memory uri = record.uri; + string memory ipUri = record.uri; - if (bytes(uri).length > 0) { - return uri; + if (bytes(ipUri).length > 0) { + return ipUri; } return _defaultTokenURI(ipId, record); @@ -131,11 +131,11 @@ contract IPMetadataResolver is IIPMetadataResolver, ResolverBase { _records[ipId].hash = newHash; } - /// @notice Sets a token URI to associated with the IP. + /// @notice Sets an IP owner defined URI to associate with the IP. /// @param ipId The canonical ID of the specified IP. - /// @param newTokenURI The new token URI to set for the IP. - function setTokenURI(address ipId, string calldata newTokenURI) external onlyAuthorized(ipId) { - _records[ipId].uri = newTokenURI; + /// @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; } /// @notice Checks whether the resolver interface is supported. diff --git a/test/foundry/resolvers/IPMetadataResolver.t.sol b/test/foundry/resolvers/IPMetadataResolver.t.sol index db1b6fa90..8595c880b 100644 --- a/test/foundry/resolvers/IPMetadataResolver.t.sol +++ b/test/foundry/resolvers/IPMetadataResolver.t.sol @@ -98,7 +98,7 @@ contract IPMetadataResolverTest is ResolverBaseTest { assertEq(ipResolver.registrationDate(ipId), RECORD_REGISTRATION_DATE); assertEq(ipResolver.registrant(ipId), alice); assertEq(ipResolver.owner(ipId), alice); - assertEq(ipResolver.tokenURI(ipId), RECORD_URI); + assertEq(ipResolver.uri(ipId), RECORD_URI); // Also check the metadata getter returns as expected. IP.Metadata memory metadata = ipResolver.metadata(ipId); @@ -178,16 +178,16 @@ contract IPMetadataResolverTest is ResolverBaseTest { /// @notice Checks setting token URI works as expected. function test_IPMetadataResolver_SetTokenURI() public { - accessController.setPermission(ipId, alice, address(ipResolver), IIPMetadataResolver.setTokenURI.selector, 1); + accessController.setPermission(ipId, alice, address(ipResolver), IIPMetadataResolver.setURI.selector, 1); vm.prank(alice); - ipResolver.setTokenURI(ipId, RECORD_URI); - assertEq(ipResolver.tokenURI(ipId), RECORD_URI); + 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.tokenURI(address(0)), ""); + assertEq(ipResolver.uri(address(0)), ""); // Check default string value for registered IP. assertTrue(accessController.checkPermission(ipId, alice, address(ipResolver), IIPMetadataResolver.setMetadata.selector)); @@ -218,7 +218,7 @@ contract IPMetadataResolverTest is ResolverBaseTest { "data:application/json;base64,", Base64.encode(bytes(string(abi.encodePacked(uriEncoding)))) )); - assertEq(expectedURI, ipResolver.tokenURI(ipId)); + assertEq(expectedURI, ipResolver.uri(ipId)); } /// @dev Deploys a new IP Metadata Resolver.