diff --git a/contracts/IPAccountImpl.sol b/contracts/IPAccountImpl.sol index 456592cd..84c6683a 100644 --- a/contracts/IPAccountImpl.sol +++ b/contracts/IPAccountImpl.sol @@ -8,6 +8,11 @@ import { IERC1155Receiver } from "@openzeppelin/contracts/token/ERC1155/IERC1155 import { IAccessController } from "contracts/interfaces/IAccessController.sol"; import { IERC6551Account } from "lib/reference/src/interfaces/IERC6551Account.sol"; import { IIPAccount } from "contracts/interfaces/IIPAccount.sol"; +import { SignatureChecker } from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol"; +import { AccessPermission } from "contracts/lib/AccessPermission.sol"; +import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; +import { MetaTx } from "contracts/lib/MetaTx.sol"; +import { Errors } from "contracts/lib/Errors.sol"; /// @title IPAccountImpl /// @notice The Story Protocol's implementation of the IPAccount. @@ -94,24 +99,55 @@ contract IPAccountImpl is IERC165, IIPAccount { return IAccessController(accessController).checkPermission(address(this), signer_, to_, selector); } - /// @notice Executes a transaction from the IP Account. - /// @param to_ The recipient of the transaction. - /// @param value_ The amount of Ether to send. - /// @param data_ The data to send along with the transaction. - /// @return result The return data from the transaction. - function execute(address to_, uint256 value_, bytes calldata data_) external payable returns (bytes memory result) { - require(_isValidSigner(msg.sender, to_, data_), "Invalid signer"); + /// @notice Executes a transaction from the IP Account on behalf of the signer. + /// @param to The recipient of the transaction. + /// @param value The amount of Ether to send. + /// @param data The data to send along with the transaction. + /// @param signer The signer of the transaction. + /// @param deadline The deadline of the transaction signature. + /// @param signature The signature of the transaction, EIP-712 encoded. + function executeWithSig( + address to, + uint256 value, + bytes calldata data, + address signer, + uint256 deadline, + bytes calldata signature + ) external payable returns (bytes memory result) { + if (signer == address(0)) { + revert Errors.IPAccount__InvalidSigner(); + } + + if (deadline < block.timestamp) { + revert Errors.IPAccount__ExpiredSignature(); + } ++state; - bool success; - (success, result) = to_.call{ value: value_ }(data_); + bytes32 digest = MessageHashUtils.toTypedDataHash( + MetaTx.calculateDomainSeparator(), + MetaTx.getExecuteStructHash( + MetaTx.Execute({ to: to, value: value, data: data, nonce: state, deadline: deadline }) + ) + ); - if (!success) { - assembly { - revert(add(result, 32), mload(result)) - } + if (!SignatureChecker.isValidSignatureNow(signer, digest, signature)) { + revert Errors.IPAccount__InvalidSignature(); } + + result = _execute(signer, to, value, data); + emit ExecutedWithSig(to, value, data, state, deadline, signer, signature); + } + + /// @notice Executes a transaction from the IP Account. + /// @param to The recipient of the transaction. + /// @param value The amount of Ether to send. + /// @param data The data to send along with the transaction. + /// @return result The return data from the transaction. + function execute(address to, uint256 value, bytes calldata data) external payable returns (bytes memory result) { + ++state; + result = _execute(msg.sender, to, value, data); + emit Executed(to, value, data, state); } function onERC721Received(address, address, uint256, bytes memory) public pure returns (bytes4) { @@ -131,4 +167,23 @@ contract IPAccountImpl is IERC165, IIPAccount { ) public pure returns (bytes4) { return this.onERC1155BatchReceived.selector; } + + function _execute( + address signer, + address to, + uint256 value, + bytes calldata data + ) internal returns (bytes memory result) { + require(_isValidSigner(signer, to, data), "Invalid signer"); + + bool success; + (success, result) = to.call{ value: value }(data); + + if (!success) { + assembly { + revert(add(result, 32), mload(result)) + } + } + } + } diff --git a/contracts/interfaces/IIPAccount.sol b/contracts/interfaces/IIPAccount.sol index 886bc6ac..ca9b2ac3 100644 --- a/contracts/interfaces/IIPAccount.sol +++ b/contracts/interfaces/IIPAccount.sol @@ -15,6 +15,20 @@ import { IERC6551Account } from "lib/reference/src/interfaces/IERC6551Account.so /// This allows for seamless operations on the state and data of IP. /// IPAccount is core identity for all actions. interface IIPAccount is IERC6551Account, IERC721Receiver, IERC1155Receiver { + /// @notice Emitted when a transaction is executed. + event Executed(address indexed to, uint256 value, bytes data, uint256 nonce); + + /// @notice Emitted when a transaction is executed on behalf of the signer. + event ExecutedWithSig( + address indexed to, + uint256 value, + bytes data, + uint256 nonce, + uint256 deadline, + address indexed signer, + bytes signature + ); + /// @notice Executes a transaction from the IP Account. /// @param to_ The recipient of the transaction. /// @param value_ The amount of Ether to send. @@ -22,6 +36,22 @@ interface IIPAccount is IERC6551Account, IERC721Receiver, IERC1155Receiver { /// @return The return data from the transaction. function execute(address to_, uint256 value_, bytes calldata data_) external payable returns (bytes memory); + /// @notice Executes a transaction from the IP Account on behalf of the signer. + /// @param to The recipient of the transaction. + /// @param value The amount of Ether to send. + /// @param data The data to send along with the transaction. + /// @param signer The signer of the transaction. + /// @param deadline The deadline of the transaction signature. + /// @param signature The signature of the transaction, EIP-712 encoded. + function executeWithSig( + address to, + uint256 value, + bytes calldata data, + address signer, + uint256 deadline, + bytes calldata signature + ) external payable returns (bytes memory); + /// @notice Returns the owner of the IP Account. /// @return The address of the owner. function owner() external view returns (address); diff --git a/contracts/interfaces/modules/IRegistrationModule.sol b/contracts/interfaces/modules/IRegistrationModule.sol index 6e632006..024d7f97 100644 --- a/contracts/interfaces/modules/IRegistrationModule.sol +++ b/contracts/interfaces/modules/IRegistrationModule.sol @@ -13,7 +13,7 @@ interface IRegistrationModule { address tokenContract, uint256 tokenId, string memory ipName, - string memory ipDescription, - bytes32 hash + bytes32 hash, + string calldata externalURL ) external; } diff --git a/contracts/interfaces/modules/dispute/IDisputeModule.sol b/contracts/interfaces/modules/dispute/IDisputeModule.sol index 3bdbeb6d..c0439e9f 100644 --- a/contracts/interfaces/modules/dispute/IDisputeModule.sol +++ b/contracts/interfaces/modules/dispute/IDisputeModule.sol @@ -3,6 +3,55 @@ pragma solidity ^0.8.23; /// @title Dispute Module Interface interface IDisputeModule { + /// @notice Event emitted when a dispute tag whitelist status is updated + /// @param tag The dispute tag + /// @param allowed Indicates if the dispute tag is whitelisted + event TagWhitelistUpdated(bytes32 tag, bool allowed); + + /// @notice Event emitted when an arbitration policy whitelist status is updated + /// @param arbitrationPolicy The address of the arbitration policy + /// @param allowed Indicates if the arbitration policy is whitelisted + event ArbitrationPolicyWhitelistUpdated(address arbitrationPolicy, bool allowed); + + /// @notice Event emitted when an arbitration relayer whitelist status is updated + /// @param arbitrationPolicy The address of the arbitration policy + /// @param arbitrationRelayer The address of the arbitration relayer + /// @param allowed Indicates if the arbitration relayer is whitelisted + event ArbitrationRelayerWhitelistUpdated(address arbitrationPolicy, address arbitrationRelayer, bool allowed); + + /// @notice Event emitted when a dispute is raised + /// @param disputeId The dispute id + /// @param targetIpId The ipId that is the target of the dispute + /// @param disputeInitiator The address of the dispute initiator + /// @param arbitrationPolicy The address of the arbitration policy + /// @param linkToDisputeEvidence The link of the dispute evidence + /// @param targetTag The target tag of the dispute + /// @param data Custom data adjusted to each policy + event DisputeRaised( + uint256 disputeId, + address targetIpId, + address disputeInitiator, + address arbitrationPolicy, + bytes32 linkToDisputeEvidence, + bytes32 targetTag, + bytes data + ); + + /// @notice Event emitted when a dispute judgement is set + /// @param disputeId The dispute id + /// @param decision The decision of the dispute + /// @param data Custom data adjusted to each policy + event DisputeJudgementSet(uint256 disputeId, bool decision, bytes data); + + /// @notice Event emitted when a dispute is cancelled + /// @param disputeId The dispute id + /// @param data Custom data adjusted to each policy + event DisputeCancelled(uint256 disputeId, bytes data); + + /// @notice Event emitted when a dispute is resolved + /// @param disputeId The dispute id + event DisputeResolved(uint256 disputeId); + /// @notice Whitelists a dispute tag /// @param tag The dispute tag /// @param allowed Indicates if the dispute tag is whitelisted or not @@ -18,18 +67,18 @@ interface IDisputeModule { /// @param arbPolicyRelayer The address of the arbitration relayer /// @param allowed Indicates if the arbitration relayer is whitelisted or not function whitelistArbitrationRelayer(address arbitrationPolicy, address arbPolicyRelayer, bool allowed) external; - + /// @notice Raises a dispute - /// @param ipId The ipId + /// @param targetIpId The ipId that is the target of the dispute /// @param arbitrationPolicy The address of the arbitration policy - /// @param linkToDisputeSummary The link of the dispute summary + /// @param linkToDisputeEvidence The link of the dispute evidence /// @param targetTag The target tag of the dispute /// @param data The data to initialize the policy /// @return disputeId The dispute id function raiseDispute( - address ipId, + address targetIpId, address arbitrationPolicy, - string memory linkToDisputeSummary, + string memory linkToDisputeEvidence, bytes32 targetTag, bytes calldata data ) external returns (uint256 disputeId); @@ -50,11 +99,15 @@ interface IDisputeModule { function resolveDispute(uint256 disputeId) external; /// @notice Gets the dispute struct characteristics - function disputes(uint256 disputeId) external view returns ( - address ipId, - address disputeInitiator, - address arbitrationPolicy, - bytes32 linkToDisputeSummary, - bytes32 tag - ); + function disputes(uint256 disputeId) + external + view + returns ( + address targetIpId, // The ipId that is the target of the dispute + address disputeInitiator, // The address of the dispute initiator + address arbitrationPolicy, // The address of the arbitration policy + bytes32 linkToDisputeEvidence, // The link of the dispute summary + bytes32 targetTag, // The target tag of the dispute + bytes32 currentTag // The current tag of the dispute + ); } \ No newline at end of file diff --git a/contracts/interfaces/registries/IIPRecordRegistry.sol b/contracts/interfaces/registries/IIPRecordRegistry.sol index b1844b3a..a0c9ebfc 100644 --- a/contracts/interfaces/registries/IIPRecordRegistry.sol +++ b/contracts/interfaces/registries/IIPRecordRegistry.sol @@ -11,12 +11,14 @@ interface IIPRecordRegistry { /// @param tokenContract The address of the IP. /// @param tokenId The token identifier of the IP. /// @param resolver The address of the resolver linked to the IP. + /// @param provider The address of the metadata provider linked to the IP. event IPRegistered( address ipId, uint256 indexed chainId, address indexed tokenContract, uint256 indexed tokenId, - address resolver + address resolver, + address provider ); /// @notice Emits when an IP account is created for an IP. @@ -39,6 +41,14 @@ interface IIPRecordRegistry { address resolver ); + /// @notice Emits when a metadata provider is set for an IP. + /// @param ipId The canonical identifier of the specified IP. + /// @param metadataProvider Address of the metadata provider associated with the IP. + event MetadataProviderSet( + address ipId, + address metadataProvider + ); + /// @notice Gets the canonical IP identifier associated with an IP (NFT). /// @dev This is the same as the address of the IP account bound to the IP. /// @param chainId The chain identifier of where the IP resides. @@ -87,18 +97,26 @@ interface IIPRecordRegistry { uint256 tokenId ) external view returns (address); + /// @notice Gets the metadata provider linked to an IP based on its ID. + /// @param id The canonical identifier for the IP. + /// @return The metadata that was bound to this IP at creation time. + function metadataProvider(address id) external view returns (address); + /// @notice Registers an NFT as IP, creating a corresponding IP record. /// @dev This is only callable by an authorized registration module. /// @param chainId The chain identifier of where the IP resides. /// @param tokenContract The address of the IP. /// @param tokenId The token identifier of the IP. + /// @param resolverAddr The address of the resolver to associate with the IP. /// @param createAccount Whether to create an IP account in the process. + /// @param metadataProvider The metadata provider to associate with the IP. function register( uint256 chainId, address tokenContract, uint256 tokenId, address resolverAddr, - bool createAccount + bool createAccount, + address metadataProvider ) external returns (address); /// @notice Creates the IP account for the specified IP. diff --git a/contracts/interfaces/registries/metadata/IMetadataProvider.sol b/contracts/interfaces/registries/metadata/IMetadataProvider.sol new file mode 100644 index 00000000..1ced771f --- /dev/null +++ b/contracts/interfaces/registries/metadata/IMetadataProvider.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: UNLICENSED +// See https://github.com/storyprotocol/protocol-contracts/blob/main/StoryProtocol-AlphaTestingAgreement-17942166.3.pdf +pragma solidity ^0.8.23; + +/// @title Metadata Provider Interface +interface IMetadataProvider { + + /// @notice Gets the metadata associated with an IP asset. + /// @param ipId The address identifier of the IP asset. + function getMetadata(address ipId) external view returns (bytes memory); +} diff --git a/contracts/interfaces/resolvers/IIPMetadataResolver.sol b/contracts/interfaces/resolvers/IIPMetadataResolver.sol deleted file mode 100644 index ab5b95c1..00000000 --- a/contracts/interfaces/resolvers/IIPMetadataResolver.sol +++ /dev/null @@ -1,76 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -// See https://github.com/storyprotocol/protocol-contracts/blob/main/StoryProtocol-AlphaTestingAgreement-17942166.3.pdf -pragma solidity ^0.8.21; - -import { IResolver } from "contracts/interfaces/resolvers/IResolver.sol"; -import { IP } from "contracts/lib/IP.sol"; - -/// @notice Resolver Interface -interface IIPMetadataResolver is IResolver { - event IPMetadataResolverSetRecord(address indexed ipId, IP.MetadataRecord data); - - event IPMetadataResolverSetName(address indexed ipId, string name); - - event IPMetadataResolverSetDescription(address indexed ipId, string description); - - event IPMetadataResolverSetHash(address indexed ipId, bytes32 hash); - - event IPMetadataResolverSetURI(address indexed ipId, string uri); - - /// @notice Fetches core metadata attributed to a specific IP. - function metadata(address ipId) external view returns (IP.Metadata memory); - - /// @notice Fetches the canonical name associated with the specified IP. - /// @param ipId The canonical ID of the specified IP. - function name(address ipId) external view returns (string memory); - - /// @notice Fetches the description associated with the specified IP. - /// @param ipId The canonical ID of the specified IP. - /// @return The string descriptor of the IP. - function description(address ipId) external view returns (string memory); - - /// @notice Fetches the keccak-256 hash associated with the specified IP. - /// @param ipId The canonical ID of the specified IP. - /// @return The bytes32 content hash of the IP. - function hash(address ipId) external view returns (bytes32); - - /// @notice Fetches the date of registration of the IP. - /// @param ipId The canonical ID of the specified IP. - function registrationDate(address ipId) external view returns (uint64); - - /// @notice Fetches the initial registrant of the IP. - /// @param ipId The canonical ID of the specified IP. - function registrant(address ipId) external view returns (address); - - /// @notice Fetches the current owner of the IP. - /// @param ipId The canonical ID of the specified IP. - function owner(address ipId) external view returns (address); - - /// @notice Fetches an IP owner defined URI associated with the IP. - /// @param ipId The canonical ID of the specified IP. - function uri(address ipId) external view returns (string memory); - - /// @notice Sets the core metadata associated with an IP. - /// @param ipId The canonical ID of the specified IP. - /// @param data Metadata to be stored for the IP in the metadata resolver. - function setMetadata(address ipId, IP.MetadataRecord calldata data) external; - - /// @notice Sets the name associated with an IP. - /// @param ipId The canonical ID of the specified IP. - /// @param name The string name to associate with the IP. - function setName(address ipId, string calldata name) external; - - /// @notice Sets the description associated with an IP. - /// @param ipId The canonical ID of the specified IP. - /// @param description The string description to associate with the IP. - function setDescription(address ipId, string calldata description) external; - - /// @notice Sets the keccak-256 hash associated with an IP. - /// @param ipId The canonical ID of the specified IP. - /// @param hash The keccak-256 hash to associate with the IP. - function setHash(address ipId, bytes32 hash) external; - - /// @notice Sets an IP owner defined URI to associate with the IP. - /// @param ipId The canonical ID of the specified IP. - function setURI(address ipId, string calldata uri) external; -} diff --git a/contracts/interfaces/resolvers/IKeyValueResolver.sol b/contracts/interfaces/resolvers/IKeyValueResolver.sol new file mode 100644 index 00000000..eeb80c84 --- /dev/null +++ b/contracts/interfaces/resolvers/IKeyValueResolver.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: UNLICENSED +// See https://github.com/storyprotocol/protocol-contracts/blob/main/StoryProtocol-AlphaTestingAgreement-17942166.3.pdf +pragma solidity ^0.8.23; + +/// @title Key Value Resolver Interface +interface IKeyValueResolver { + + /// @notice Emits when a new key-value pair is set for the resolver. + event KeyValueSet( + address indexed ipId, + string indexed key, + string value + ); + + /// @notice Retrieves the string value associated with a key for an IP asset. + function value( + address ipId, + string calldata key + ) external view returns (string memory); +} diff --git a/contracts/lib/Errors.sol b/contracts/lib/Errors.sol index fab53df7..1f88a4ec 100644 --- a/contracts/lib/Errors.sol +++ b/contracts/lib/Errors.sol @@ -5,6 +5,14 @@ pragma solidity ^0.8.19; /// @title Errors Library /// @notice Library for all Story Protocol contract errors. library Errors { + //////////////////////////////////////////////////////////////////////////// + // IPAccount // + //////////////////////////////////////////////////////////////////////////// + error IPAccount__InvalidSigner(); + error IPAccount__InvalidSignature(); + error IPAccount__ExpiredSignature(); + + //////////////////////////////////////////////////////////////////////////// // Module // //////////////////////////////////////////////////////////////////////////// @@ -41,6 +49,13 @@ library Errors { /// @notice Caller not authorized to perform the IP resolver function call. error IPResolver_Unauthorized(); + //////////////////////////////////////////////////////////////////////////// + // Metadata Provider /// + //////////////////////////////////////////////////////////////////////////// + + /// @notice Caller does not access to set metadata storage for the provider. + error MetadataProvider_Unauthorized(); + //////////////////////////////////////////////////////////////////////////// // LicenseRegistry // //////////////////////////////////////////////////////////////////////////// @@ -79,11 +94,13 @@ library Errors { error DisputeModule__ZeroArbitrationPolicy(); error DisputeModule__ZeroArbitrationRelayer(); error DisputeModule__ZeroDisputeTag(); - error DisputeModule__ZeroLinkToDisputeSummary(); + error DisputeModule__ZeroLinkToDisputeEvidence(); error DisputeModule__NotWhitelistedArbitrationPolicy(); error DisputeModule__NotWhitelistedDisputeTag(); error DisputeModule__NotWhitelistedArbitrationRelayer(); error DisputeModule__NotDisputeInitiator(); + error DisputeModule__NotInDisputeState(); + error DisputeModule__NotAbleToResolve(); error ArbitrationPolicySP__ZeroDisputeModule(); error ArbitrationPolicySP__ZeroPaymentToken(); @@ -141,5 +158,4 @@ library Errors { error TaggingModule__SrcIpIdDoesNotHaveSrcTag(); error TaggingModule__DstIpIdDoesNotHaveDstTag(); error TaggingModule__RelationTypeDoesNotExist(); - } diff --git a/contracts/lib/IP.sol b/contracts/lib/IP.sol index 341ddef4..f566439d 100644 --- a/contracts/lib/IP.sol +++ b/contracts/lib/IP.sol @@ -1,47 +1,21 @@ // SPDX-License-Identifier: UNLICENSED // See https://github.com/storyprotocol/protocol-contracts/blob/main/StoryProtocol-AlphaTestingAgreement-17942166.3.pdf -pragma solidity ^0.8.21; +pragma solidity ^0.8.23; /// @title IP Library /// @notice Library for constants, structs, and helper functions used for IP. library IP { - /// @notice Core metadata associated with an IP. - /// @dev This is what is fetched when `metadata()` is called from an IP - /// resolver, and includes aggregated attributes fetched from various - /// modules in addition to that which is stored on the resolver itself. + /// @notice Core metadata to associate with each IP. struct Metadata { - // The current owner of the IP. - address owner; // The name associated with the IP. string name; - // A description associated with the IP. - string description; // A keccak-256 hash of the IP content. bytes32 hash; // The date which the IP was registered. uint64 registrationDate; // The address of the initial IP registrant. address registrant; - // The token URI associated with the IP. - string uri; - } - - /// @notice Core metadata exclusively saved by the IP resolver. - /// @dev This only encompasses metadata which is stored on the IP metadata - /// resolver itself, and does not include those attributes which may - /// be fetched from different modules (e.g. the licensing modules). - struct MetadataRecord { - // The name associated with the IP. - string name; - // A description associated with the IP. - string description; - // A keccak-256 hash of the IP content. - bytes32 hash; - // The date which the IP was registered. - uint64 registrationDate; - // The address of the initial IP registrant. - address registrant; - // The token URI associated with the IP. + // An external URI associated with the IP. string uri; } } diff --git a/contracts/lib/MetaTx.sol b/contracts/lib/MetaTx.sol new file mode 100644 index 00000000..8c8d9504 --- /dev/null +++ b/contracts/lib/MetaTx.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: UNLICENSED +// See https://github.com/storyprotocol/protocol-contracts/blob/main/StoryProtocol-AlphaTestingAgreement-17942166.3.pdf +pragma solidity ^0.8.23; + +/// @title MetaTx +/// @dev This library provides functions for handling meta transactions in the Story Protocol. +library MetaTx { + /// @dev Version of the EIP712 domain. + string constant EIP712_DOMAIN_VERSION = "1"; + /// @dev Hash of the EIP712 domain version. + bytes32 constant EIP712_DOMAIN_VERSION_HASH = keccak256(bytes(EIP712_DOMAIN_VERSION)); + /// @dev EIP712 domain type hash. + bytes32 constant EIP712_DOMAIN = + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); + /// @dev Execute type hash. + bytes32 constant EXECUTE = keccak256("Execute(address to,uint256 value,bytes data,uint256 nonce,uint256 deadline)"); + + /// @dev Structure for the Execute type. + struct Execute { + address to; + uint256 value; + bytes data; + uint256 nonce; + uint256 deadline; + } + + /// @dev Calculates the EIP712 domain separator for the current contract. + /// @return The EIP712 domain separator. + function calculateDomainSeparator() internal view returns (bytes32) { + return calculateDomainSeparator(address(this)); + } + + /// @dev Calculates the EIP712 domain separator for a given IP account. + /// @param ipAccount The IP account for which to calculate the domain separator. + /// @return The EIP712 domain separator. + function calculateDomainSeparator(address ipAccount) internal view returns (bytes32) { + return + keccak256( + abi.encode( + EIP712_DOMAIN, + keccak256("Story Protocol IP Account"), + EIP712_DOMAIN_VERSION_HASH, + block.chainid, + ipAccount + ) + ); + } + + /// @dev Calculates the EIP712 struct hash of an Execute. + /// @param execute The Execute to hash. + /// @return The EIP712 struct hash of the Execute. + function getExecuteStructHash(Execute memory execute) internal pure returns (bytes32) { + return + keccak256( + abi.encode( + MetaTx.EXECUTE, + execute.to, + execute.value, + keccak256(execute.data), + execute.nonce, + execute.deadline + ) + ); + } +} diff --git a/contracts/lib/modules/Module.sol b/contracts/lib/modules/Module.sol index a36b2102..684772d0 100644 --- a/contracts/lib/modules/Module.sol +++ b/contracts/lib/modules/Module.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: UNLICENSED // See https://github.com/storyprotocol/protocol-contracts/blob/main/StoryProtocol-AlphaTestingAgreement-17942166.3.pdf -pragma solidity ^0.8.21; +pragma solidity ^0.8.23; // String values for core protocol modules. -string constant METADATA_RESOLVER_MODULE_KEY = "METADATA_RESOLVER_MODULE"; +string constant IP_RESOLVER_MODULE_KEY = "IP_RESOLVER_MODULE"; // String values for core protocol modules. string constant REGISTRATION_MODULE_KEY = "REGISTRATION_MODULE"; diff --git a/contracts/mocks/MockERC721.sol b/contracts/mocks/MockERC721.sol index efd6ef33..d73a85fb 100644 --- a/contracts/mocks/MockERC721.sol +++ b/contracts/mocks/MockERC721.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.23; -import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import { ERC721 } from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; contract MockERC721 is ERC721 { uint256 private _counter; diff --git a/contracts/modules/RegistrationModule.sol b/contracts/modules/RegistrationModule.sol index fefe4778..39ada40e 100644 --- a/contracts/modules/RegistrationModule.sol +++ b/contracts/modules/RegistrationModule.sol @@ -2,11 +2,11 @@ // See https://github.com/storyprotocol/protocol-contracts/blob/main/StoryProtocol-AlphaTestingAgreement-17942166.3.pdf pragma solidity ^0.8.23; -// external import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; -// contracts + +import { IPResolver } from "contracts/resolvers/IPResolver.sol"; +import { IPMetadataProvider } from "contracts/registries/metadata/IPMetadataProvider.sol"; import { IRegistrationModule } from "contracts/interfaces/modules/IRegistrationModule.sol"; -import { IIPMetadataResolver } from "contracts/interfaces/resolvers/IIPMetadataResolver.sol"; import { REGISTRATION_MODULE_KEY } from "contracts/lib/modules/Module.sol"; import { Errors } from "contracts/lib/Errors.sol"; import { IP } from "contracts/lib/IP.sol"; @@ -19,7 +19,10 @@ import { BaseModule } from "contracts/modules/BaseModule.sol"; /// and terms specified by the IP registrant (IP account owner). contract RegistrationModule is BaseModule, IRegistrationModule { /// @notice The metadata resolver used by the registration module. - IIPMetadataResolver public resolver; + IPResolver public resolver; + + /// @notice Metadata storage provider contract. + IPMetadataProvider public metadataProvider; /// @notice Initializes the registration module contract. /// @param controller The access controller used for IP authorization. @@ -32,9 +35,11 @@ contract RegistrationModule is BaseModule, IRegistrationModule { address recordRegistry, address accountRegistry, address licenseRegistry, - address resolverAddr + address resolverAddr, + address metadataProviderAddr ) BaseModule(controller, recordRegistry, accountRegistry, licenseRegistry) { - resolver = IIPMetadataResolver(resolverAddr); + metadataProvider = IPMetadataProvider(metadataProviderAddr); + resolver = IPResolver(resolverAddr); } /// @notice Registers a root-level IP into the protocol. Root-level IPs can @@ -60,7 +65,14 @@ contract RegistrationModule is BaseModule, IRegistrationModule { } // Perform core IP registration and IP account creation. - address ipId = IP_RECORD_REGISTRY.register(block.chainid, tokenContract, tokenId, address(resolver), true); + address ipId = IP_RECORD_REGISTRY.register( + block.chainid, + tokenContract, + tokenId, + address(resolver), + true, + address(metadataProvider) + ); // Perform core IP policy creation. if (policyId != 0) { @@ -79,15 +91,17 @@ contract RegistrationModule is BaseModule, IRegistrationModule { /// @param tokenContract The address of the NFT bound to the derivative IP. /// @param tokenId The token id of the NFT bound to the derivative IP. /// @param ipName The name assigned to the new IP. - /// @param ipDescription A string description to assign to the IP. /// @param contentHash The content hash of the IP being registered. + /// @param externalURL An external URI to link to the IP. + /// TODO: Replace all metadata with a generic bytes parameter type, and do + /// encoding on the periphery contract level instead. function registerDerivativeIp( uint256 licenseId, address tokenContract, uint256 tokenId, string memory ipName, - string memory ipDescription, - bytes32 contentHash + bytes32 contentHash, + string calldata externalURL ) external { // Check that the caller is authorized to perform the registration. // TODO: Perform additional registration authorization logic, allowing @@ -96,32 +110,37 @@ contract RegistrationModule is BaseModule, IRegistrationModule { revert Errors.RegistrationModule__InvalidOwner(); } - // Perform core IP registration and IP account creation. - address ipId = IP_RECORD_REGISTRY.register(block.chainid, tokenContract, tokenId, address(resolver), true); - ACCESS_CONTROLLER.setPermission( - ipId, - address(this), + address ipId = IP_RECORD_REGISTRY.register( + block.chainid, + tokenContract, + tokenId, address(resolver), - IIPMetadataResolver.setMetadata.selector, - 1 + true, + address(metadataProvider) ); + // ACCESS_CONTROLLER.setPermission( + // ipId, + // address(this), + // address(resolver), + // IPResolver.setMetadata.selector, + // 1 + // ); - // Perform core IP derivative licensing - the license must be owned by the caller. - // TODO: return resulting policy index - LICENSE_REGISTRY.linkIpToParent(licenseId, ipId, msg.sender); - - // Perform metadata attribution setting. - resolver.setMetadata( - ipId, - IP.MetadataRecord({ + // Perform core IP registration and IP account creation. + bytes memory metadata = abi.encode( + IP.Metadata({ name: ipName, - description: ipDescription, hash: contentHash, registrationDate: uint64(block.timestamp), registrant: msg.sender, - uri: "" + uri: externalURL }) ); + metadataProvider.setMetadata(ipId, metadata); + + // Perform core IP derivative licensing - the license must be owned by the caller. + // TODO: return resulting policy index + LICENSE_REGISTRY.linkIpToParent(licenseId, ipId, msg.sender); emit DerivativeIPRegistered(msg.sender, ipId, licenseId); } diff --git a/contracts/modules/dispute-module/DisputeModule.sol b/contracts/modules/dispute-module/DisputeModule.sol index 6b07724f..2513813c 100644 --- a/contracts/modules/dispute-module/DisputeModule.sol +++ b/contracts/modules/dispute-module/DisputeModule.sol @@ -15,13 +15,17 @@ import { ShortStringOps } from "contracts/utils/ShortStringOps.sol"; contract DisputeModule is IDisputeModule, ReentrancyGuard { /// @notice Dispute struct struct Dispute { - address ipId; // The ipId + address targetIpId; // The ipId that is the target of the dispute address disputeInitiator; // The address of the dispute initiator address arbitrationPolicy; // The address of the arbitration policy - bytes32 linkToDisputeSummary; // The link of the dispute summary - bytes32 tag; // The target tag of the dispute // TODO: move to tagging module? + bytes32 linkToDisputeEvidence; // The link of the dispute summary + bytes32 targetTag; // The target tag of the dispute + bytes32 currentTag; // The current tag of the dispute } + // TODO: confirm if contracts will be upgradeable or not + bytes32 public constant IN_DISPUTE = bytes32("IN_DISPUTE"); + /// @notice Dispute id uint256 public disputeId; @@ -35,8 +39,8 @@ contract DisputeModule is IDisputeModule, ReentrancyGuard { mapping(address arbitrationPolicy => bool allowed) public isWhitelistedArbitrationPolicy; /// @notice Indicates if an arbitration relayer is whitelisted for a given arbitration policy - mapping(address arbitrationPolicy => mapping(address arbitrationRelayer => bool allowed)) - public isWhitelistedArbitrationRelayer; + mapping(address arbitrationPolicy => mapping(address arbitrationRelayer => bool allowed)) public + isWhitelistedArbitrationRelayer; /// @notice Restricts the calls to the governance address modifier onlyGovernance() { @@ -52,7 +56,7 @@ contract DisputeModule is IDisputeModule, ReentrancyGuard { isWhitelistedDisputeTag[_tag] = _allowed; - // TODO: emit event + emit TagWhitelistUpdated(_tag, _allowed); } /// @notice Whitelists an arbitration policy @@ -63,66 +67,66 @@ contract DisputeModule is IDisputeModule, ReentrancyGuard { isWhitelistedArbitrationPolicy[_arbitrationPolicy] = _allowed; - // TODO: emit event + emit ArbitrationPolicyWhitelistUpdated(_arbitrationPolicy, _allowed); } /// @notice Whitelists an arbitration relayer for a given arbitration policy /// @param _arbitrationPolicy The address of the arbitration policy /// @param _arbPolicyRelayer The address of the arbitration relayer /// @param _allowed Indicates if the arbitration relayer is whitelisted or not - function whitelistArbitrationRelayer( - address _arbitrationPolicy, - address _arbPolicyRelayer, - bool _allowed - ) external onlyGovernance { + function whitelistArbitrationRelayer(address _arbitrationPolicy, address _arbPolicyRelayer, bool _allowed) + external + onlyGovernance + { if (_arbitrationPolicy == address(0)) revert Errors.DisputeModule__ZeroArbitrationPolicy(); if (_arbPolicyRelayer == address(0)) revert Errors.DisputeModule__ZeroArbitrationRelayer(); isWhitelistedArbitrationRelayer[_arbitrationPolicy][_arbPolicyRelayer] = _allowed; - // TODO: emit event + emit ArbitrationRelayerWhitelistUpdated(_arbitrationPolicy, _arbPolicyRelayer, _allowed); } /// @notice Raises a dispute - /// @param _ipId The ipId + /// @param _targetIpId The ipId that is the target of the dispute /// @param _arbitrationPolicy The address of the arbitration policy - /// @param _linkToDisputeSummary The link of the dispute summary + /// @param _linkToDisputeEvidence The link of the dispute evidence /// @param _targetTag The target tag of the dispute /// @param _data The data to initialize the policy /// @return disputeId The dispute id function raiseDispute( - address _ipId, + address _targetIpId, address _arbitrationPolicy, - string memory _linkToDisputeSummary, + string memory _linkToDisputeEvidence, bytes32 _targetTag, bytes calldata _data ) external nonReentrant returns (uint256) { - // TODO: make call to ensure ipId exists/has been registered + // TODO: ensure the _targetIpId address is an existing/valid IPAccount if (!isWhitelistedArbitrationPolicy[_arbitrationPolicy]) { revert Errors.DisputeModule__NotWhitelistedArbitrationPolicy(); } if (!isWhitelistedDisputeTag[_targetTag]) revert Errors.DisputeModule__NotWhitelistedDisputeTag(); - bytes32 linkToDisputeSummary = ShortStringOps.stringToBytes32(_linkToDisputeSummary); - if (linkToDisputeSummary == bytes32(0)) revert Errors.DisputeModule__ZeroLinkToDisputeSummary(); + bytes32 linkToDisputeEvidence = ShortStringOps.stringToBytes32(_linkToDisputeEvidence); + if (linkToDisputeEvidence == bytes32(0)) revert Errors.DisputeModule__ZeroLinkToDisputeEvidence(); - disputeId++; + uint256 disputeId_ = ++disputeId; - disputes[disputeId] = Dispute({ - ipId: _ipId, + disputes[disputeId_] = Dispute({ + targetIpId: _targetIpId, disputeInitiator: msg.sender, arbitrationPolicy: _arbitrationPolicy, - linkToDisputeSummary: linkToDisputeSummary, - tag: _targetTag + linkToDisputeEvidence: linkToDisputeEvidence, + targetTag: _targetTag, + currentTag: IN_DISPUTE }); - // TODO: set tag to "in-dispute" state - IArbitrationPolicy(_arbitrationPolicy).onRaiseDispute(msg.sender, _data); - // TODO: emit event + emit DisputeRaised( + disputeId_, _targetIpId, msg.sender, _arbitrationPolicy, linkToDisputeEvidence, _targetTag, _data + ); - return disputeId; + return disputeId_; } /// @notice Sets the dispute judgement @@ -130,46 +134,50 @@ contract DisputeModule is IDisputeModule, ReentrancyGuard { /// @param _decision The decision of the dispute /// @param _data The data to set the dispute judgement function setDisputeJudgement(uint256 _disputeId, bool _decision, bytes calldata _data) external nonReentrant { - address _arbitrationPolicy = disputes[_disputeId].arbitrationPolicy; + Dispute memory dispute = disputes[_disputeId]; - // TODO: if dispute tag is not in "in-dispute" state then the function should revert - the same disputeId cannot be set twice + cancelled cannot be set - if (!isWhitelistedArbitrationRelayer[_arbitrationPolicy][msg.sender]) { + if (dispute.currentTag != IN_DISPUTE) revert Errors.DisputeModule__NotInDisputeState(); + if (!isWhitelistedArbitrationRelayer[dispute.arbitrationPolicy][msg.sender]) { revert Errors.DisputeModule__NotWhitelistedArbitrationRelayer(); } if (_decision) { - // TODO: set tag to the target dispute tag state + disputes[_disputeId].currentTag = dispute.targetTag; } else { - // TODO: remove tag/set dispute tag to null state + disputes[_disputeId].currentTag = bytes32(0); } - IArbitrationPolicy(_arbitrationPolicy).onDisputeJudgement(_disputeId, _decision, _data); + IArbitrationPolicy(dispute.arbitrationPolicy).onDisputeJudgement(_disputeId, _decision, _data); - // TODO: emit event + emit DisputeJudgementSet(_disputeId, _decision, _data); } /// @notice Cancels an ongoing dispute /// @param _disputeId The dispute id /// @param _data The data to cancel the dispute function cancelDispute(uint256 _disputeId, bytes calldata _data) external nonReentrant { - if (msg.sender != disputes[_disputeId].disputeInitiator) revert Errors.DisputeModule__NotDisputeInitiator(); - // TODO: if tag is not "in-dispute" then revert + Dispute memory dispute = disputes[_disputeId]; - IArbitrationPolicy(disputes[_disputeId].arbitrationPolicy).onDisputeCancel(msg.sender, _disputeId, _data); + if (dispute.currentTag != IN_DISPUTE) revert Errors.DisputeModule__NotInDisputeState(); + if (msg.sender != dispute.disputeInitiator) revert Errors.DisputeModule__NotDisputeInitiator(); - // TODO: remove tag/set dispute tag to null state + IArbitrationPolicy(dispute.arbitrationPolicy).onDisputeCancel(msg.sender, _disputeId, _data); - // TODO: emit event + disputes[_disputeId].currentTag = bytes32(0); + + emit DisputeCancelled(_disputeId, _data); } /// @notice Resolves a dispute after it has been judged /// @param _disputeId The dispute id function resolveDispute(uint256 _disputeId) external { - if (msg.sender != disputes[_disputeId].disputeInitiator) revert Errors.DisputeModule__NotDisputeInitiator(); - // TODO: if tag is in "in-dispute" or already "null" then revert + Dispute memory dispute = disputes[_disputeId]; + + if (dispute.currentTag == IN_DISPUTE) revert Errors.DisputeModule__NotAbleToResolve(); + if (msg.sender != dispute.disputeInitiator) revert Errors.DisputeModule__NotDisputeInitiator(); - // TODO: remove tag/set dispute tag to null state + disputes[_disputeId].currentTag = bytes32(0); - // TODO: emit event + emit DisputeResolved(_disputeId); } } diff --git a/contracts/modules/dispute-module/policies/ArbitrationPolicySP.sol b/contracts/modules/dispute-module/policies/ArbitrationPolicySP.sol index e403c2eb..955724f5 100644 --- a/contracts/modules/dispute-module/policies/ArbitrationPolicySP.sol +++ b/contracts/modules/dispute-module/policies/ArbitrationPolicySP.sol @@ -54,7 +54,7 @@ contract ArbitrationPolicySP is IArbitrationPolicy { /// @notice Executes custom logic on raise dispute /// @param _caller Address of the caller function onRaiseDispute(address _caller, bytes calldata) external onlyDisputeModule { - // TODO: we can add permit if the token supports it + // requires that the caller has given approve() to this contract IERC20(PAYMENT_TOKEN).safeTransferFrom(_caller, address(this), ARBITRATION_PRICE); } @@ -63,7 +63,7 @@ contract ArbitrationPolicySP is IArbitrationPolicy { /// @param _decision The decision of the dispute function onDisputeJudgement(uint256 _disputeId, bool _decision, bytes calldata) external onlyDisputeModule { if (_decision) { - (, address disputeInitiator, , , ) = IDisputeModule(DISPUTE_MODULE).disputes(_disputeId); + (, address disputeInitiator,,,,) = IDisputeModule(DISPUTE_MODULE).disputes(_disputeId); IERC20(PAYMENT_TOKEN).safeTransfer(disputeInitiator, ARBITRATION_PRICE); } } @@ -76,5 +76,6 @@ contract ArbitrationPolicySP is IArbitrationPolicy { function withdraw(uint256 _amount) external onlyGovernance { // TODO: where is governance address defined? /* IERC20(PAYMENT_TOKEN).safeTransfer(governance, _amount); */ + // TODO: emit event } } diff --git a/contracts/registries/IPRecordRegistry.sol b/contracts/registries/IPRecordRegistry.sol index 599161aa..90c51a38 100644 --- a/contracts/registries/IPRecordRegistry.sol +++ b/contracts/registries/IPRecordRegistry.sol @@ -2,6 +2,7 @@ // See https://github.com/storyprotocol/protocol-contracts/blob/main/StoryProtocol-AlphaTestingAgreement-17942166.3.pdf pragma solidity ^0.8.21; +import { IMetadataProvider } from "contracts/interfaces/registries/metadata/IMetadataProvider.sol"; import { REGISTRATION_MODULE_KEY } from "contracts/lib/modules/Module.sol"; import { IIPRecordRegistry } from "contracts/interfaces/registries/IIPRecordRegistry.sol"; import { IIPAccountRegistry } from "contracts/interfaces/registries/IIPAccountRegistry.sol"; @@ -19,6 +20,14 @@ import { Errors } from "contracts/lib/Errors.sol"; /// IMPORTANT: The IP account address, besides being used for protocol /// auth, is also the canonical IP identifier for the IP NFT. contract IPRecordRegistry is IIPRecordRegistry { + /// @notice Attributes for the IP record type. + struct Record { + // Metadata provider for Story Protocol canonicalized metadata. + address metadataProvider; + // IP translator for custom IP record types. + address resolver; + } + /// @notice Gets the factory contract used for IP account creation. IIPAccountRegistry public immutable IP_ACCOUNT_REGISTRY; @@ -28,8 +37,8 @@ contract IPRecordRegistry is IIPRecordRegistry { /// @notice Tracks the total number of IP records in existence. uint256 public totalSupply = 0; - /// @dev Maps an IP, identified by its IP ID, to a metadata resolver. - mapping(address => address) internal _resolvers; + /// @dev Maps an IP, identified by its IP ID, to an IP record. + mapping(address => Record) internal _records; /// @notice Restricts calls to only originate from a protocol-authorized caller. modifier onlyRegistrationModule() { @@ -61,7 +70,7 @@ contract IPRecordRegistry is IIPRecordRegistry { /// @param id The canonical identifier for the IP. /// @return Whether the IP was registered into the protocol. function isRegistered(address id) external view returns (bool) { - return _resolvers[id] != address(0); + return _records[id].resolver != address(0); } /// @notice Checks whether an IP was registered based on its NFT attributes. @@ -69,16 +78,24 @@ contract IPRecordRegistry is IIPRecordRegistry { /// @param tokenContract The address of the NFT. /// @param tokenId The token identifier of the NFT. /// @return Whether the NFT was registered into the protocol as IP. + /// TODO: Deprecate this in favor of solely using IP IDs for registration identification. function isRegistered(uint256 chainId, address tokenContract, uint256 tokenId) external view returns (bool) { address id = ipId(chainId, tokenContract, tokenId); - return _resolvers[id] != address(0); + return _records[id].resolver != address(0); } /// @notice Gets the resolver bound to an IP based on its ID. /// @param id The canonical identifier for the IP. /// @return The IP resolver address if registered, else the zero address. function resolver(address id) external view returns (address) { - return _resolvers[id]; + return _records[id].resolver; + } + + /// @notice Gets the metadata linked to an IP based on its ID. + /// @param id The canonical identifier for the IP. + /// @return The metadata that was bound to this IP at creation time. + function metadataProvider(address id) external view returns (address) { + return _records[id].metadataProvider; } /// @notice Gets the resolver bound to an IP based on its NFT attributes. @@ -86,25 +103,29 @@ contract IPRecordRegistry is IIPRecordRegistry { /// @param tokenContract The address of the NFT. /// @param tokenId The token identifier of the NFT. /// @return The IP resolver address if registered, else the zero address. + /// TODO: Deprecate this in favor of solely using IP IDs for resolver identification. function resolver(uint256 chainId, address tokenContract, uint256 tokenId) external view returns (address) { address id = ipId(chainId, tokenContract, tokenId); - return _resolvers[id]; + return _records[id].resolver; } /// @notice Registers an NFT as an IP, creating a corresponding IP record. /// @param chainId The chain identifier of where the NFT resides. /// @param tokenContract The address of the NFT. /// @param tokenId The token identifier of the NFT. + /// @param resolverAddr The address of the resolver to associate with the IP. /// @param createAccount Whether to create an IP account when registering. + /// @param provider The metadata provider to associate with the IP. function register( uint256 chainId, address tokenContract, uint256 tokenId, address resolverAddr, - bool createAccount + bool createAccount, + address provider ) external onlyRegistrationModule returns (address account) { address id = ipId(chainId, tokenContract, tokenId); - if (_resolvers[id] != address(0)) { + if (_records[id].resolver != address(0)) { revert Errors.IPRecordRegistry_AlreadyRegistered(); } @@ -116,8 +137,9 @@ contract IPRecordRegistry is IIPRecordRegistry { _createIPAccount(chainId, tokenContract, tokenId); } _setResolver(id, resolverAddr); + _setMetadataProvider(id, provider); totalSupply++; - emit IPRegistered(id, chainId, tokenContract, tokenId, resolverAddr); + emit IPRegistered(id, chainId, tokenContract, tokenId, resolverAddr, provider); } /// @notice Creates the IP account for the specified IP. @@ -128,6 +150,8 @@ contract IPRecordRegistry is IIPRecordRegistry { /// @param chainId The chain identifier of where the NFT resides. /// @param tokenContract The address of the NFT. /// @param tokenId The token identifier of the NFT. + /// TODO: Deprecate this in favor of relying on creation via a periphery + /// contract or through the IPAccountRegistry directly. function createIPAccount(uint256 chainId, address tokenContract, uint256 tokenId) external returns (address) { address account = IP_ACCOUNT_REGISTRY.ipAccount(chainId, tokenContract, tokenId); // TODO: Finalize disambiguation between IP accounts and IP identifiers. @@ -137,11 +161,24 @@ contract IPRecordRegistry is IIPRecordRegistry { return _createIPAccount(chainId, tokenContract, tokenId); } + /// @notice Sets the underlying metadata provider for an IP asset. + /// @param id The canonical ID of the IP. + /// @param provider Address of the metadata provider associated with the IP. + function setMetadataProvider(address id, address provider) external onlyRegistrationModule { + // Metadata may not be set unless the IP was registered into the protocol. + if (_records[id].resolver == address(0)) { + revert Errors.IPRecordRegistry_NotYetRegistered(); + } + _setMetadataProvider(id, provider); + } + /// @notice Sets the resolver for an IP based on its NFT attributes. /// @param chainId The chain identifier of where the NFT resides. /// @param tokenContract The address of the NFT. /// @param tokenId The token identifier of the NFT. /// @param resolverAddr The address of the resolver being set. + /// TODO: Deprecate this in favor of relying solely on IP ids for settings + /// and instead including this in a periphery contract. function setResolver( uint256 chainId, address tokenContract, @@ -160,7 +197,7 @@ contract IPRecordRegistry is IIPRecordRegistry { revert Errors.IPRecordRegistry_ResolverInvalid(); } // Resolvers may not be set unless the IP was registered into the protocol. - if (_resolvers[id] == address(0)) { + if (_records[id].resolver == address(0)) { revert Errors.IPRecordRegistry_NotYetRegistered(); } _setResolver(id, resolverAddr); @@ -183,7 +220,15 @@ contract IPRecordRegistry is IIPRecordRegistry { /// @param id The canonical identifier for the specified IP. /// @param resolverAddr The address of the IP resolver. function _setResolver(address id, address resolverAddr) internal { - _resolvers[id] = resolverAddr; + _records[id].resolver = resolverAddr; emit IPResolverSet(id, resolverAddr); } + + /// @dev Sets the metadata for the specified IP. + /// @param id The canonical identifier for the specified IP. + /// @param provider The metadata provider to associated with the IP. + function _setMetadataProvider(address id, address provider) internal { + _records[id].metadataProvider = provider; + emit MetadataProviderSet(id, provider); + } } diff --git a/contracts/registries/LicenseRegistry.sol b/contracts/registries/LicenseRegistry.sol index 7b66d31e..adb0f222 100644 --- a/contracts/registries/LicenseRegistry.sol +++ b/contracts/registries/LicenseRegistry.sol @@ -303,6 +303,7 @@ contract LicenseRegistry is ERC1155, ILicenseRegistry { } // Verify minting param Licensing.Policy memory pol = policy(policyId); +<<<<<<< HEAD Licensing.Framework storage fw = _framework(pol.frameworkId); uint256 policyParamsLength = pol.paramNames.length; bool setByLinking = _policySetups[licensorIp][policyId].setByLinking; @@ -327,6 +328,10 @@ contract LicenseRegistry is ERC1155, ILicenseRegistry { policyId: policyId, licensorIpId: licensorIp }); +======= + _verifyParams(Licensing.ParamVerifierType.Mint, pol, receiver, amount); + Licensing.License memory licenseData = Licensing.License({ policyId: policyId, licensorIpIds: licensorIpIds }); +>>>>>>> main (uint256 lId, bool isNew) = _addIdOrGetExisting(abi.encode(licenseData), _hashedLicenses, _totalLicenses); licenseId = lId; if (isNew) { @@ -364,7 +369,6 @@ contract LicenseRegistry is ERC1155, ILicenseRegistry { address childIpId, address holder ) external onlyLicensee(licenseId, holder) { - // TODO: check if childIpId exists and is owned by holder Licensing.License memory licenseData = _licenses[licenseId]; address parentIpId = licenseData.licensorIpId; @@ -376,6 +380,7 @@ contract LicenseRegistry is ERC1155, ILicenseRegistry { // Verify linking params Licensing.Policy memory pol = policy(licenseData.policyId); +<<<<<<< HEAD Licensing.Framework storage fw = _framework(pol.frameworkId); uint256 policyParamsLength = pol.paramNames.length; bool verificationOk = true; @@ -394,6 +399,10 @@ contract LicenseRegistry is ERC1155, ILicenseRegistry { } } +======= + _verifyParams(Licensing.ParamVerifierType.LinkParent, pol, holder, 1); + +>>>>>>> main // Add policy to kid _addPolictyIdToIp(childIpId, licenseData.policyId, true); // Set parent @@ -421,6 +430,7 @@ contract LicenseRegistry is ERC1155, ILicenseRegistry { return _licenses[licenseId]; } +<<<<<<< HEAD function licensorIpId(uint256 licenseId) external view returns (address) { return _licenses[licenseId].licensorIpId; } @@ -451,12 +461,60 @@ contract LicenseRegistry is ERC1155, ILicenseRegistry { } } } +======= + function _update( + address from, + address to, + uint256[] memory ids, + uint256[] memory values + ) internal virtual override { + // We are interested in transfers, minting and burning are checked in mintLicense and linkIpToParent respectively. + + if (from != address(0) && to != address(0)) { + uint256 length = ids.length; + for (uint256 i = 0; i < length; i++) { + _verifyParams(Licensing.ParamVerifierType.Transfer, policyForLicense(ids[i]), to, values[i]); + } +>>>>>>> main } super._update(from, to, ids, values); } +<<<<<<< HEAD - +======= + function _verifyParams( + Licensing.ParamVerifierType pvt, + Licensing.Policy memory pol, + address holder, + uint256 amount + ) internal { + Licensing.Framework storage fw = _framework(pol.frameworkId); + Licensing.Parameter[] storage params = fw.parameters[pvt]; + uint256 paramsLength = params.length; + bytes[] memory values = pol.getValues(pvt); + + for (uint256 i = 0; i < paramsLength; i++) { + Licensing.Parameter memory param = params[i]; + // Empty bytes => use default value specified in license framework creation params. + bytes memory data = values[i].length == 0 ? param.defaultValue : values[i]; + bool verificationOk = false; + if (pvt == Licensing.ParamVerifierType.Mint) { + verificationOk = param.verifier.verifyMinting(holder, amount, data); + } else if (pvt == Licensing.ParamVerifierType.LinkParent) { + verificationOk = param.verifier.verifyLinkParent(holder, data); + } else if (pvt == Licensing.ParamVerifierType.Transfer) { + verificationOk = param.verifier.verifyTransfer(holder, amount, data); + } else { + // This should never happen since getValues checks for pvt validity + revert Errors.LicenseRegistry__InvalidParamVerifierType(); + } + if (!verificationOk) { + revert Errors.LicenseRegistry__ParamVerifierFailed(uint8(pvt), address(param.verifier)); + } + } + } +>>>>>>> main // TODO: tokenUri from parameters, from a metadata resolver contract } diff --git a/contracts/registries/metadata/IPAssetRenderer.sol b/contracts/registries/metadata/IPAssetRenderer.sol new file mode 100644 index 00000000..8a1df8b8 --- /dev/null +++ b/contracts/registries/metadata/IPAssetRenderer.sol @@ -0,0 +1,174 @@ +// SPDX-License-Identifier: UNLICENSED +// See https://github.com/storyprotocol/protocol-contracts/blob/main/StoryProtocol-AlphaTestingAgreement-17942166.3.pdf +pragma solidity ^0.8.23; + +import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; +import { Base64 } from "@openzeppelin/contracts/utils/Base64.sol"; +import { IP } from "contracts/lib/IP.sol"; +import { IIPAccount } from "contracts/interfaces/IIPAccount.sol"; +import { IPRecordRegistry } from "contracts/registries/IPRecordRegistry.sol"; +import { IMetadataProvider } from "contracts/interfaces/registries/metadata/IMetadataProvider.sol"; +import { LicenseRegistry } from "contracts/registries/LicenseRegistry.sol"; +import { TaggingModule } from "contracts/modules/tagging/TaggingModule.sol"; +import { RoyaltyModule } from "contracts/modules/royalty-module/RoyaltyModule.sol"; + +/// @title IP Asset Renderer +/// @notice The IP asset renderer is responsible for rendering canonical +/// metadata associated with each IP asset. This includes generation +/// of attributes, on-chain SVGs, and external URLs. Note that the +/// underlying data being rendered is strictly immutable. +contract IPAssetRenderer { + /// @notice The global IP record registry. + IPRecordRegistry public immutable IP_RECORD_REGISTRY; + + /// @notice The global licensing registry. + LicenseRegistry public immutable LICENSE_REGISTRY; + + // Modules storing attribution related to IPs. + TaggingModule public immutable TAGGING_MODULE; + RoyaltyModule public immutable ROYALTY_MODULE; + + /// @notice Initializes the IP asset renderer. + /// TODO: Add different customization options - e.g. font, colorways, etc. + /// TODO: Add an external URL for generating SP-branded links for each IP. + constructor( + address recordRegistry, + address licenseRegistry, + address taggingModule, + address royaltyModule + ) { + IP_RECORD_REGISTRY = IPRecordRegistry(recordRegistry); + LICENSE_REGISTRY = LicenseRegistry(licenseRegistry); + TAGGING_MODULE = TaggingModule(taggingModule); + ROYALTY_MODULE = RoyaltyModule(royaltyModule); + } + + // TODO: Add contract URI support for metadata about the entire IP registry. + + // TODO: Add rendering functions around licensing information. + + // TODO: Add rendering functions around royalties information. + + // TODO: Add rendering functions around tagging information. + + /// @notice Fetches the canonical name associated with the specified IP. + /// @param ipId The canonical ID of the specified IP. + function name(address ipId) external view returns (string memory) { + IP.Metadata memory metadata = _metadata(ipId); + return metadata.name; + } + + /// @notice Fetches the canonical description associated with the IP. + /// @param ipId The canonical ID of the specified IP. + /// @return The string descriptor of the IP. + /// TODO: Add more information related to licensing or royalties. + /// TODO: Update the description to an SP base URL if external URL not set. + function description(address ipId) public view returns (string memory) { + IP.Metadata memory metadata = _metadata(ipId); + return string.concat( + metadata.name, + ", IP #", + Strings.toHexString(ipId), + ", is currently owned by", + Strings.toHexString(owner(ipId)), + ". To learn more about this IP, visit ", + metadata.uri + ); + } + + /// @notice Fetches the keccak-256 content hash associated with the specified IP. + /// @param ipId The canonical ID of the specified IP. + /// @return The bytes32 content hash of the IP. + function hash(address ipId) external view returns (bytes32) { + IP.Metadata memory metadata = _metadata(ipId); + return metadata.hash; + } + + /// @notice Fetches the date of registration of the IP. + /// @param ipId The canonical ID of the specified IP. + function registrationDate(address ipId) external view returns (uint64) { + IP.Metadata memory metadata = _metadata(ipId); + return metadata.registrationDate; + } + + /// @notice Fetches the initial registrant of the IP. + /// @param ipId The canonical ID of the specified IP. + function registrant(address ipId) external view returns (address) { + IP.Metadata memory metadata = _metadata(ipId); + return metadata.registrant; + } + + /// @notice Fetches the external URL associated with the IP. + /// @param ipId The canonical ID of the specified IP. + function uri(address ipId) external view returns (string memory) { + IP.Metadata memory metadata = _metadata(ipId); + return metadata.uri; + } + + /// @notice Fetches the current owner of the IP. + /// @param ipId The canonical ID of the specified IP. + function owner(address ipId) public view returns (address) { + return IIPAccount(payable(ipId)).owner(); + } + + /// @notice Generates a JSON of all metadata attribution related to the IP. + /// TODO: Make this ERC-721 compatible, so that the IP registry may act as + /// an account-bound ERC-721 that points to this function for metadata. + /// TODO: Add SVG support. + /// TODO: Add licensing, royalties, and tagging information support. + function tokenURI(address ipId) external view returns (string memory) { + IP.Metadata memory metadata = _metadata(ipId); + string memory baseJson = string( + /* solhint-disable */ + abi.encodePacked( + '{"name": "IP Asset #', + Strings.toHexString(ipId), + '", "description": "', + description(ipId), + '", "attributes": [' + ) + /* solhint-enable */ + ); + + string memory ipAttributes = string( + /* solhint-disable */ + abi.encodePacked( + '{"trait_type": "Name", "value": "', + metadata.name, + '"},' + '{"trait_type": "Owner", "value": "', + Strings.toHexString(owner(ipId)), + '"},' + '{"trait_type": "Registrant", "value": "', + Strings.toHexString(uint160(metadata.registrant), 20), + '"},', + '{"trait_type": "Hash", "value": "', + Strings.toHexString(uint256(metadata.hash), 32), + '"},', + '{"trait_type": "Registration Date", "value": "', + Strings.toString(metadata.registrationDate), + '"}' + ) + /* solhint-enable */ + ); + + return + string( + abi.encodePacked( + "data:application/json;base64,", + Base64.encode(bytes(string(abi.encodePacked(baseJson, ipAttributes, "]}")))) + ) + ); + } + + /// TODO: Add SVG generation support for branding within token metadata. + + /// @dev Internal function for fetching the metadata tied to an IP record. + function _metadata(address ipId) internal view returns (IP.Metadata memory metadata) { + IMetadataProvider provider = IMetadataProvider(IP_RECORD_REGISTRY.metadataProvider(ipId)); + bytes memory data = provider.getMetadata(ipId); + if (data.length != 0) { + metadata = abi.decode(data, (IP.Metadata)); + } + } +} diff --git a/contracts/registries/metadata/IPMetadataProvider.sol b/contracts/registries/metadata/IPMetadataProvider.sol new file mode 100644 index 00000000..8ae14a6c --- /dev/null +++ b/contracts/registries/metadata/IPMetadataProvider.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: UNLICENSED +// See https://github.com/storyprotocol/protocol-contracts/blob/main/StoryProtocol-AlphaTestingAgreement-17942166.3.pdf +pragma solidity ^0.8.23; + +import { REGISTRATION_MODULE_KEY } from "contracts/lib/modules/Module.sol"; +import { IMetadataProvider } from "contracts/interfaces/registries/metadata/IMetadataProvider.sol"; +import { IModuleRegistry } from "contracts/interfaces/registries/IModuleRegistry.sol"; +import { Errors } from "contracts/lib/Errors.sol"; + +/// @title IP Metadata Provider Contract +/// @notice Base contract used for customization of canonical IP metadata. +contract IPMetadataProvider is IMetadataProvider { + + /// @notice Gets the protocol-wide module registry. + IModuleRegistry public immutable MODULE_REGISTRY; + + /// @notice Maps IPs to their metadata based on their IP IDs. + mapping(address ip => bytes metadata) internal _ipMetadata; + + /// @notice Restricts calls to only originate from a protocol-authorized caller. + modifier onlyRegistrationModule() { + if (address(MODULE_REGISTRY.getModule(REGISTRATION_MODULE_KEY)) != msg.sender) { + revert Errors.MetadataProvider_Unauthorized(); + } + _; + } + + /// @notice Initializes the metadata provider contract. + /// @param moduleRegistry Gets the protocol-wide module registry. + constructor(address moduleRegistry) { + MODULE_REGISTRY = IModuleRegistry(moduleRegistry); + } + + /// @notice Gets the IP metadata associated with an IP asset based on its IP ID. + /// @param ipId The IP id of the target IP asset. + function getMetadata(address ipId) external view virtual override returns (bytes memory) { + return _ipMetadata[ipId]; + } + + /// @notice Sets the IP metadata associated with an IP asset based on its IP ID. + /// @param ipId The IP id of the IP asset to set metadata for. + /// @param metadata The metadata in bytes to set for the IP asset. + function setMetadata(address ipId, bytes memory metadata) external onlyRegistrationModule { + _ipMetadata[ipId] = metadata; + } +} diff --git a/contracts/resolvers/IPMetadataResolver.sol b/contracts/resolvers/IPMetadataResolver.sol deleted file mode 100644 index fb76323e..00000000 --- a/contracts/resolvers/IPMetadataResolver.sol +++ /dev/null @@ -1,209 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -// See https://github.com/storyprotocol/protocol-contracts/blob/main/StoryProtocol-AlphaTestingAgreement-17942166.3.pdf -pragma solidity ^0.8.21; - -import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; -import { Base64 } from "@openzeppelin/contracts/utils/Base64.sol"; -import { IResolver } from "contracts/interfaces/resolvers/IResolver.sol"; -import { ResolverBase } from "./ResolverBase.sol"; -import { BaseModule } from "contracts/modules/BaseModule.sol"; -import { IModule } from "contracts/interfaces/modules/base/IModule.sol"; -import { IIPMetadataResolver } from "contracts/interfaces/resolvers/IIPMetadataResolver.sol"; -import { IIPAccount } from "contracts/interfaces/IIPAccount.sol"; -import { Errors } from "contracts/lib/Errors.sol"; -import { IP } from "contracts/lib/IP.sol"; -import { METADATA_RESOLVER_MODULE_KEY } from "contracts/lib/modules/Module.sol"; - -/// @title IP Metadata Resolver -/// @notice Canonical IP resolver contract used for Story Protocol. This will -/// likely change to a separate contract that extends IPMetadataResolver -/// in the near future. -contract IPMetadataResolver is IIPMetadataResolver, ResolverBase { - /// @dev Maps IP to their metadata records based on their canonical IDs. - mapping(address => IP.MetadataRecord) public _records; - - /// @notice Initializes the IP metadata resolver. - /// @param accessController The access controller used for IP authorization. - /// @param ipRecordRegistry The address of the IP record registry. - /// @param ipAccountRegistry The address of the IP account registry. - /// @param licenseRegistry The address of the license registry. - constructor( - address accessController, - address ipRecordRegistry, - address ipAccountRegistry, - address licenseRegistry - ) ResolverBase(accessController, ipRecordRegistry, ipAccountRegistry, licenseRegistry) {} - - /// @notice Fetches all metadata associated with the specified IP. - /// @param ipId The canonical ID of the specified IP. - function metadata(address ipId) public view returns (IP.Metadata memory) { - IP.MetadataRecord memory record = _records[ipId]; - return - IP.Metadata({ - owner: owner(ipId), - name: record.name, - description: record.description, - hash: record.hash, - registrationDate: record.registrationDate, - registrant: record.registrant, - uri: uri(ipId) - }); - } - - /// @notice Fetches the canonical name associated with the specified IP. - /// @param ipId The canonical ID of the specified IP. - function name(address ipId) external view returns (string memory) { - return _records[ipId].name; - } - - /// @notice Fetches the description associated with the specified IP. - /// @param ipId The canonical ID of the specified IP. - /// @return The string descriptor of the IP. - function description(address ipId) external view returns (string memory) { - return _records[ipId].description; - } - - /// @notice Fetches the keccak-256 hash associated with the specified IP. - /// @param ipId The canonical ID of the specified IP. - /// @return The bytes32 content hash of the IP. - function hash(address ipId) external view returns (bytes32) { - return _records[ipId].hash; - } - - /// @notice Fetches the date of registration of the IP. - /// @param ipId The canonical ID of the specified IP. - function registrationDate(address ipId) external view returns (uint64) { - return _records[ipId].registrationDate; - } - - /// @notice Fetches the initial registrant of the IP. - /// @param ipId The canonical ID of the specified IP. - function registrant(address ipId) external view returns (address) { - return _records[ipId].registrant; - } - - /// @notice Fetches the current owner of the IP. - /// @param ipId The canonical ID of the specified IP. - function owner(address ipId) public view returns (address) { - if (!IP_RECORD_REGISTRY.isRegistered(ipId)) { - return address(0); - } - return IIPAccount(payable(ipId)).owner(); - } - - /// @notice Fetches an IP owner defined URI associated with the IP. - /// @param ipId The canonical ID of the specified IP. - function uri(address ipId) public view returns (string memory) { - if (!IP_RECORD_REGISTRY.isRegistered(ipId)) { - return ""; - } - - IP.MetadataRecord memory record = _records[ipId]; - string memory ipUri = record.uri; - - if (bytes(ipUri).length > 0) { - return ipUri; - } - - return _defaultTokenURI(ipId, record); - } - - /// @notice Sets metadata associated with an IP. - /// @param ipId The canonical ID of the specified IP. - /// @param newMetadata The new metadata to set for the IP. - function setMetadata(address ipId, IP.MetadataRecord calldata newMetadata) external onlyAuthorized(ipId) { - _records[ipId] = newMetadata; - emit IPMetadataResolverSetRecord(ipId, newMetadata); - } - - /// @notice Sets the name associated with an IP. - /// @param ipId The canonical ID of the specified IP. - /// @param newName The new string name to associate with the IP. - function setName(address ipId, string calldata newName) external onlyAuthorized(ipId) { - _records[ipId].name = newName; - emit IPMetadataResolverSetName(ipId, newName); - } - - /// @notice Sets the description associated with an IP. - /// @param ipId The canonical ID of the specified IP. - /// @param newDescription The string description to associate with the IP. - function setDescription(address ipId, string calldata newDescription) external onlyAuthorized(ipId) { - _records[ipId].description = newDescription; - emit IPMetadataResolverSetDescription(ipId, newDescription); - } - - /// @notice Sets the keccak-256 hash associated with an IP. - /// @param ipId The canonical ID of the specified IP. - /// @param newHash The keccak-256 hash to associate with the IP. - function setHash(address ipId, bytes32 newHash) external onlyAuthorized(ipId) { - _records[ipId].hash = newHash; - emit IPMetadataResolverSetHash(ipId, newHash); - } - - /// @notice Sets an IP owner defined URI to associate with the IP. - /// @param ipId The canonical ID of the specified IP. - /// @param newURI The new token URI to set for the IP. - function setURI(address ipId, string calldata newURI) external onlyAuthorized(ipId) { - _records[ipId].uri = newURI; - emit IPMetadataResolverSetURI(ipId, newURI); - } - - /// @notice Checks whether the resolver interface is supported. - /// @param id The resolver interface identifier. - /// @return Whether the resolver interface is supported. - function supportsInterface(bytes4 id) public view virtual override(IResolver, ResolverBase) returns (bool) { - return id == type(IIPMetadataResolver).interfaceId || super.supportsInterface(id); - } - - /// @notice Gets the protocol-wide module identifier for this module. - function name() public pure override(BaseModule, IModule) returns (string memory) { - return METADATA_RESOLVER_MODULE_KEY; - } - - /// @dev Internal function for generating a default IP URI if not provided. - /// @param ipId The canonical ID of the specified IP. - /// @param record The IP record associated with the IP. - function _defaultTokenURI(address ipId, IP.MetadataRecord memory record) internal view returns (string memory) { - string memory baseJson = string( - /* solhint-disable */ - abi.encodePacked( - '{"name": "IP Asset #', - Strings.toHexString(ipId), - '", "description": "', - record.description, - '", "attributes": [' - ) - /* solhint-enable */ - ); - - string memory ipAttributes = string( - /* solhint-disable */ - abi.encodePacked( - '{"trait_type": "Name", "value": "', - record.name, - '"},' - '{"trait_type": "Owner", "value": "', - Strings.toHexString(uint160(owner(ipId)), 20), - '"},' - '{"trait_type": "Registrant", "value": "', - Strings.toHexString(uint160(record.registrant), 20), - '"},', - '{"trait_type": "Hash", "value": "', - Strings.toHexString(uint256(record.hash), 32), - '"},', - '{"trait_type": "Registration Date", "value": "', - Strings.toString(record.registrationDate), - '"}' - ) - /* solhint-enable */ - ); - - return - string( - abi.encodePacked( - "data:application/json;base64,", - Base64.encode(bytes(string(abi.encodePacked(baseJson, ipAttributes, "]}")))) - ) - ); - } -} diff --git a/contracts/resolvers/IPResolver.sol b/contracts/resolvers/IPResolver.sol new file mode 100644 index 00000000..bfb468d2 --- /dev/null +++ b/contracts/resolvers/IPResolver.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: UNLICENSED +// See https://github.com/storyprotocol/protocol-contracts/blob/main/StoryProtocol-AlphaTestingAgreement-17942166.3.pdf +pragma solidity ^0.8.23; + +import { IResolver } from "contracts/interfaces/resolvers/IResolver.sol"; +import { ResolverBase } from "./ResolverBase.sol"; +import { BaseModule } from "contracts/modules/BaseModule.sol"; +import { IModule } from "contracts/interfaces/modules/base/IModule.sol"; +import { IKeyValueResolver } from "contracts/interfaces/resolvers/IKeyValueResolver.sol"; +import { IIPAccount } from "contracts/interfaces/IIPAccount.sol"; +import { KeyValueResolver } from "contracts/resolvers/KeyValueResolver.sol"; +import { Errors } from "contracts/lib/Errors.sol"; +import { IP } from "contracts/lib/IP.sol"; +import { IP_RESOLVER_MODULE_KEY } from "contracts/lib/modules/Module.sol"; + +/// @title IP Resolver +/// @notice Canonical IP resolver contract used for Story Protocol. +/// TODO: Add support for interface resolvers, where one can add a contract +/// and supported interface (address, interfaceId) to tie to an IP asset. +/// TODO: Add support for multicall, so multiple records may be set at once. +contract IPResolver is KeyValueResolver { + /// @notice Initializes the IP metadata resolver. + /// @param accessController The access controller used for IP authorization. + /// @param ipRecordRegistry The address of the IP record registry. + /// @param ipAccountRegistry The address of the IP account registry. + /// @param licenseRegistry The address of the license registry. + constructor( + address accessController, + address ipRecordRegistry, + address ipAccountRegistry, + address licenseRegistry + ) ResolverBase(accessController, ipRecordRegistry, ipAccountRegistry, licenseRegistry) {} + + /// @notice Checks whether the resolver interface is supported. + /// @param id The resolver interface identifier. + /// @return Whether the resolver interface is supported. + function supportsInterface(bytes4 id) public view virtual override returns (bool) { + return super.supportsInterface(id); + } + + /// @notice Gets the protocol-wide module identifier for this module. + function name() public pure override(BaseModule, IModule) returns (string memory) { + return IP_RESOLVER_MODULE_KEY; + } +} diff --git a/contracts/resolvers/KeyValueResolver.sol b/contracts/resolvers/KeyValueResolver.sol new file mode 100644 index 00000000..892b21d7 --- /dev/null +++ b/contracts/resolvers/KeyValueResolver.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: UNLICENSED +// See https://github.com/storyprotocol/protocol-contracts/blob/main/StoryProtocol-AlphaTestingAgreement-17942166.3.pdf +pragma solidity ^0.8.23; + +import { IKeyValueResolver } from "contracts/interfaces/resolvers/IKeyValueResolver.sol"; +import { ResolverBase } from "contracts/resolvers/ResolverBase.sol"; + +/// @title Key Value Resolver +/// @notice Resolver used for returning values associated with keys. This is the +/// preferred approach for adding additional attribution to IP that the +/// IP originator thinks is beneficial to have on chain. +abstract contract KeyValueResolver is IKeyValueResolver, ResolverBase { + /// @dev Stores key-value pairs associated with each IP. + mapping(address => mapping(string => string)) internal _values; + + /// @notice Sets the string value for a specified key of an IP ID. + /// @param ipId The canonical identifier of the IP asset. + /// @param k The string parameter key to update. + /// @param v The value to set for the specified key. + function setValue(address ipId, string calldata k, string calldata v) external virtual onlyAuthorized(ipId) { + _values[ipId][k] = v; + emit KeyValueSet(ipId, k, v); + } + + /// @notice Retrieves the string value associated with a key for an IP asset. + /// @param k The string parameter key to query. + function value(address ipId, string calldata k) external view virtual returns (string memory) { + return _values[ipId][k]; + } + + /// @notice Checks whether the resolver interface is supported. + /// @param id The resolver interface identifier. + /// @return Whether the resolver interface is supported. + function supportsInterface(bytes4 id) public view virtual override returns (bool) { + return id == type(IKeyValueResolver).interfaceId || super.supportsInterface(id); + } +} diff --git a/contracts/resolvers/ResolverBase.sol b/contracts/resolvers/ResolverBase.sol index 99e5a605..21596264 100644 --- a/contracts/resolvers/ResolverBase.sol +++ b/contracts/resolvers/ResolverBase.sol @@ -7,13 +7,17 @@ import { IResolver } from "contracts/interfaces/resolvers/IResolver.sol"; /// @notice IP Resolver Base Contract abstract contract ResolverBase is IResolver, BaseModule { - /// @notice Initializes the base module contract. /// @param controller The access controller used for IP authorization. /// @param recordRegistry The address of the IP record registry. /// @param accountRegistry The address of the IP account registry. /// @param licenseRegistry The address of the license registry. - constructor(address controller, address recordRegistry, address accountRegistry, address licenseRegistry) BaseModule(controller, recordRegistry, accountRegistry, licenseRegistry) {} + constructor( + address controller, + address recordRegistry, + address accountRegistry, + address licenseRegistry + ) BaseModule(controller, recordRegistry, accountRegistry, licenseRegistry) {} /// @notice Checks whether the resolver interface is supported. /// @param id The resolver interface identifier. diff --git a/interfaces/modules/dispute-module/IDisputeModule.sol b/interfaces/modules/dispute-module/IDisputeModule.sol deleted file mode 100644 index 3bdbeb6d..00000000 --- a/interfaces/modules/dispute-module/IDisputeModule.sol +++ /dev/null @@ -1,60 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.23; - -/// @title Dispute Module Interface -interface IDisputeModule { - /// @notice Whitelists a dispute tag - /// @param tag The dispute tag - /// @param allowed Indicates if the dispute tag is whitelisted or not - function whitelistDisputeTags(bytes32 tag, bool allowed) external; - - /// @notice Whitelists an arbitration policy - /// @param arbitrationPolicy The address of the arbitration policy - /// @param allowed Indicates if the arbitration policy is whitelisted or not - function whitelistArbitrationPolicy(address arbitrationPolicy, bool allowed) external; - - /// @notice Whitelists an arbitration relayer for a given arbitration policy - /// @param arbitrationPolicy The address of the arbitration policy - /// @param arbPolicyRelayer The address of the arbitration relayer - /// @param allowed Indicates if the arbitration relayer is whitelisted or not - function whitelistArbitrationRelayer(address arbitrationPolicy, address arbPolicyRelayer, bool allowed) external; - - /// @notice Raises a dispute - /// @param ipId The ipId - /// @param arbitrationPolicy The address of the arbitration policy - /// @param linkToDisputeSummary The link of the dispute summary - /// @param targetTag The target tag of the dispute - /// @param data The data to initialize the policy - /// @return disputeId The dispute id - function raiseDispute( - address ipId, - address arbitrationPolicy, - string memory linkToDisputeSummary, - bytes32 targetTag, - bytes calldata data - ) external returns (uint256 disputeId); - - /// @notice Sets the dispute judgement - /// @param disputeId The dispute id - /// @param decision The decision of the dispute - /// @param data The data to set the dispute judgement - function setDisputeJudgement(uint256 disputeId, bool decision, bytes calldata data) external; - - /// @notice Cancels an ongoing dispute - /// @param disputeId The dispute id - /// @param data The data to cancel the dispute - function cancelDispute(uint256 disputeId, bytes calldata data) external; - - /// @notice Resolves a dispute after it has been judged - /// @param disputeId The dispute id - function resolveDispute(uint256 disputeId) external; - - /// @notice Gets the dispute struct characteristics - function disputes(uint256 disputeId) external view returns ( - address ipId, - address disputeInitiator, - address arbitrationPolicy, - bytes32 linkToDisputeSummary, - bytes32 tag - ); -} \ No newline at end of file diff --git a/interfaces/modules/dispute-module/policies/IArbitrationPolicy.sol b/interfaces/modules/dispute-module/policies/IArbitrationPolicy.sol deleted file mode 100644 index b6c6ed05..00000000 --- a/interfaces/modules/dispute-module/policies/IArbitrationPolicy.sol +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.23; - -/// @title ArbitrationPolicy interface -interface IArbitrationPolicy { - /// @notice Executes custom logic on raise dispute - /// @param caller Address of the caller - function onRaiseDispute(address caller, bytes calldata data) external; - - /// @notice Executes custom logic on dispute judgement - /// @param disputeId The dispute id - /// @param decision The decision of the dispute - function onDisputeJudgement(uint256 disputeId, bool decision, bytes calldata data) external; - - /// @notice Executes custom logic on dispute cancel - function onDisputeCancel(address caller, uint256 disputeId, bytes calldata data) external; -} diff --git a/interfaces/modules/royalty-module/IRoyaltyModule.sol b/interfaces/modules/royalty-module/IRoyaltyModule.sol deleted file mode 100644 index a75f8bb2..00000000 --- a/interfaces/modules/royalty-module/IRoyaltyModule.sol +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.23; - -/// @title RoyaltyModule interface -interface IRoyaltyModule { - /// @notice Whitelist a royalty policy - /// @param royaltyPolicy The address of the royalty policy - /// @param allowed Indicates if the royalty policy is whitelisted or not - function whitelistRoyaltyPolicy(address royaltyPolicy, bool allowed) external; - - /// @notice Sets the royalty policy for an ipId - /// @param ipId The ipId - /// @param royaltyPolicy The address of the royalty policy - /// @param data The data to initialize the policy - function setRoyaltyPolicy(address ipId, address royaltyPolicy, bytes calldata data) external; - - /// @notice Allows an IPAccount to pay royalties - /// @param ipId The ipId - /// @param token The token to pay the royalties in - /// @param amount The amount to pay - function payRoyalty(address ipId, address token, uint256 amount) external; - - /// @notice Gets the royalty policy for a given ipId - function royaltyPolicies(address ipId) external view returns (address royaltyPolicy); -} \ No newline at end of file diff --git a/interfaces/modules/royalty-module/policies/ILiquidSplitClone.sol b/interfaces/modules/royalty-module/policies/ILiquidSplitClone.sol deleted file mode 100644 index f8ece07c..00000000 --- a/interfaces/modules/royalty-module/policies/ILiquidSplitClone.sol +++ /dev/null @@ -1,11 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.23; - -/// @title LiquidSplitClone interface -interface ILiquidSplitClone { - /// @notice Distributes funds to the accounts in the LiquidSplitClone contract - /// @param token The token to distribute - /// @param accounts The accounts to distribute to - /// @param distributorAddress The distributor address - function distributeFunds(address token, address[] calldata accounts, address distributorAddress) external; -} \ No newline at end of file diff --git a/interfaces/modules/royalty-module/policies/ILiquidSplitFactory.sol b/interfaces/modules/royalty-module/policies/ILiquidSplitFactory.sol deleted file mode 100644 index da9a630b..00000000 --- a/interfaces/modules/royalty-module/policies/ILiquidSplitFactory.sol +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.23; - -/// @title LiquidSplitFactory interface -interface ILiquidSplitFactory { - /// @notice Creates a new LiquidSplitClone contract - /// @param accounts The accounts to initialize the LiquidSplitClone contract with - /// @param initAllocations The initial allocations - /// @param _distributorFee The distributor fee - /// @param owner The owner of the LiquidSplitClone contract - function createLiquidSplitClone( - address[] calldata accounts, - uint32[] calldata initAllocations, - uint32 _distributorFee, - address owner - ) external returns (address); -} \ No newline at end of file diff --git a/interfaces/modules/royalty-module/policies/ILiquidSplitMain.sol b/interfaces/modules/royalty-module/policies/ILiquidSplitMain.sol deleted file mode 100644 index ba138093..00000000 --- a/interfaces/modules/royalty-module/policies/ILiquidSplitMain.sol +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.23; - -import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; - -/// @title LiquidSplitMain interface -interface ILiquidSplitMain { - /// @notice Allows an account to withdraw their accrued and distributed pending amount - /// @param account The account to withdraw from - /// @param withdrawETH The amount of ETH to withdraw - /// @param tokens The tokens to withdraw - function withdraw( - address account, - uint256 withdrawETH, - ERC20[] calldata tokens - ) external; -} \ No newline at end of file diff --git a/interfaces/modules/royalty-module/policies/IRoyaltyPolicy.sol b/interfaces/modules/royalty-module/policies/IRoyaltyPolicy.sol deleted file mode 100644 index ad97f6b6..00000000 --- a/interfaces/modules/royalty-module/policies/IRoyaltyPolicy.sol +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.23; - -/// @title RoyaltyPolicy interface -interface IRoyaltyPolicy { - /// @notice Initializes the royalty policy - /// @param ipId The ipId - /// @param data The data to initialize the policy - function initPolicy(address ipId, bytes calldata data) external; - - /// @notice Allows to pay a royalty - /// @param caller The caller - /// @param ipId The ipId - /// @param token The token to pay - /// @param amount The amount to pay - function onRoyaltyPayment(address caller, address ipId, address token, uint256 amount) external; -} \ No newline at end of file diff --git a/script/foundry/deployment/Main.s.sol b/script/foundry/deployment/Main.s.sol index 3823e233..67d3875c 100644 --- a/script/foundry/deployment/Main.s.sol +++ b/script/foundry/deployment/Main.s.sol @@ -13,15 +13,17 @@ import { AccessController } from "contracts/AccessController.sol"; import { IPAccountImpl } from "contracts/IPAccountImpl.sol"; import { IIPAccount } from "contracts/interfaces/IIPAccount.sol"; import { Errors } from "contracts/lib/Errors.sol"; +import { IPMetadataProvider } from "contracts/registries/metadata/IPMetadataProvider.sol"; import { IPAccountRegistry } from "contracts/registries/IPAccountRegistry.sol"; import { IPRecordRegistry } from "contracts/registries/IPRecordRegistry.sol"; +import { IPAssetRenderer } from "contracts/registries/metadata/IPAssetRenderer.sol"; import { ModuleRegistry } from "contracts/registries/ModuleRegistry.sol"; import { LicenseRegistry } from "contracts/registries/LicenseRegistry.sol"; import { RegistrationModule } from "contracts/modules/RegistrationModule.sol"; import { TaggingModule } from "contracts/modules/tagging/TaggingModule.sol"; import { RoyaltyModule } from "contracts/modules/royalty-module/RoyaltyModule.sol"; import { DisputeModule } from "contracts/modules/dispute-module/DisputeModule.sol"; -import { IPMetadataResolver } from "contracts/resolvers/IPMetadataResolver.sol"; +import { IPResolver } from "contracts/resolvers/IPResolver.sol"; // script import { StringUtil } from "script/foundry/utils/StringUtil.sol"; @@ -35,6 +37,8 @@ contract Main is Script, BroadcastManager, JsonDeploymentHandler { address public constant ERC6551_REGISTRY = address(0x000000006551c19487814612e58FE06813775758); AccessController public accessController; + IPAssetRenderer public renderer; + IPMetadataProvider public metadataProvider; IPAccountRegistry public ipAccountRegistry; IPRecordRegistry public ipRecordRegistry; LicenseRegistry public licenseRegistry; @@ -50,7 +54,7 @@ contract Main is Script, BroadcastManager, JsonDeploymentHandler { TaggingModule taggingModule; RoyaltyModule royaltyModule; DisputeModule disputeModule; - IPMetadataResolver ipMetadataResolver; + IPResolver ipResolver; constructor() JsonDeploymentHandler("main") {} @@ -107,15 +111,20 @@ contract Main is Script, BroadcastManager, JsonDeploymentHandler { ipRecordRegistry = new IPRecordRegistry(address(moduleRegistry), address(ipAccountRegistry)); _postdeploy(contractKey, address(ipRecordRegistry)); - contractKey = "IPMetadataResolver"; + contractKey = "IPResolver"; _predeploy(contractKey); - ipMetadataResolver = new IPMetadataResolver( + ipResolver = new IPResolver( address(accessController), address(ipRecordRegistry), address(ipAccountRegistry), address(licenseRegistry) ); - _postdeploy(contractKey, address(ipMetadataResolver)); + _postdeploy(contractKey, address(ipResolver)); + + contractKey = "MetadataProvider"; + _predeploy(contractKey); + metadataProvider = new IPMetadataProvider(address(moduleRegistry)); + _postdeploy(contractKey, address(metadataProvider)); contractKey = "RegistrationModule"; _predeploy(contractKey); @@ -124,7 +133,8 @@ contract Main is Script, BroadcastManager, JsonDeploymentHandler { address(ipRecordRegistry), address(ipAccountRegistry), address(licenseRegistry), - address(ipMetadataResolver) + address(ipResolver), + address(metadataProvider) ); _postdeploy(contractKey, address(registrationModule)); @@ -143,6 +153,17 @@ contract Main is Script, BroadcastManager, JsonDeploymentHandler { disputeModule = new DisputeModule(); _postdeploy(contractKey, address(disputeModule)); + contractKey = "IPAssetRenderer"; + _predeploy(contractKey); + renderer = new IPAssetRenderer( + address(ipRecordRegistry), + address(licenseRegistry), + address(taggingModule), + address(royaltyModule) + ); + _postdeploy(contractKey, address(renderer)); + + // mockModule = new MockModule(address(ipAccountRegistry), address(moduleRegistry), "MockModule"); } diff --git a/test/foundry/DisputeModule.t.sol b/test/foundry/DisputeModule.t.sol deleted file mode 100644 index f1548d3a..00000000 --- a/test/foundry/DisputeModule.t.sol +++ /dev/null @@ -1,135 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.23; - -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - -import {Errors} from "contracts/lib/Errors.sol"; -import {ShortStringOps} from "contracts/utils/ShortStringOps.sol"; -import {TestHelper} from "test/utils/TestHelper.sol"; - -contract TestDisputeModule is TestHelper { - function setUp() public override { - super.setUp(); - - // fund USDC - vm.startPrank(USDC_RICH); - IERC20(USDC).transfer(ipAccount1, 1000 * 10 ** 6); - vm.stopPrank(); - - // whitelist dispute tag - disputeModule.whitelistDisputeTags("plagiarism", true); - - // whitelist arbitration policy - disputeModule.whitelistArbitrationPolicy(address(arbitrationPolicySP), true); - - // whitelist arbitration relayer - disputeModule.whitelistArbitrationRelayer(address(arbitrationPolicySP), arbitrationRelayer, true); - } - - function test_DisputeModule_whitelistDisputeTags() public { - assertEq(disputeModule.isWhitelistedDisputeTag("plagiarism"), true); - } - - function test_DisputeModule_whitelistArbitrationPolicy() public { - assertEq(disputeModule.isWhitelistedArbitrationPolicy(address(arbitrationPolicySP)), true); - } - - function test_DisputeModule_whitelistArbitrationRelayer() public { - assertEq( - disputeModule.isWhitelistedArbitrationRelayer(address(arbitrationPolicySP), address(arbitrationRelayer)), - true - ); - } - - function test_DisputeModule_raiseDispute() public { - vm.startPrank(ipAccount1); - - IERC20(USDC).approve(address(arbitrationPolicySP), ARBITRATION_PRICE); - - uint256 disputeIdBefore = disputeModule.disputeId(); - uint256 ipAccount1USDCBalanceBefore = IERC20(USDC).balanceOf(ipAccount1); - uint256 arbitrationPolicySPUSDCBalanceBefore = IERC20(USDC).balanceOf(address(arbitrationPolicySP)); - - vm.expectRevert(Errors.DisputeModule__NotWhitelistedArbitrationPolicy.selector); - disputeModule.raiseDispute(address(1), address(0xbeef), string("urlExample"), "plagiarism", ""); - - disputeModule.raiseDispute(address(1), address(arbitrationPolicySP), string("urlExample"), "plagiarism", ""); - - uint256 disputeIdAfter = disputeModule.disputeId(); - uint256 ipAccount1USDCBalanceAfter = IERC20(USDC).balanceOf(ipAccount1); - uint256 arbitrationPolicySPUSDCBalanceAfter = IERC20(USDC).balanceOf(address(arbitrationPolicySP)); - - (address ip_id, address disputeInitiator, address arbitrationPolicy, bytes32 linkToDisputeSummary, bytes32 tag) - = disputeModule.disputes(disputeIdAfter); - - assertEq(disputeIdAfter - disputeIdBefore, 1); - assertEq(ipAccount1USDCBalanceBefore - ipAccount1USDCBalanceAfter, ARBITRATION_PRICE); - assertEq(arbitrationPolicySPUSDCBalanceAfter - arbitrationPolicySPUSDCBalanceBefore, ARBITRATION_PRICE); - assertEq(ip_id, address(1)); - assertEq(disputeInitiator, ipAccount1); - assertEq(arbitrationPolicy, address(arbitrationPolicySP)); - assertEq(linkToDisputeSummary, ShortStringOps.stringToBytes32("urlExample")); - assertEq(tag, bytes32("plagiarism")); - } - - function test_DisputeModule_setDisputeJudgement() public { - // raise dispute - vm.startPrank(ipAccount1); - IERC20(USDC).approve(address(arbitrationPolicySP), ARBITRATION_PRICE); - disputeModule.raiseDispute(address(1), address(arbitrationPolicySP), string("urlExample"), "plagiarism", ""); - vm.stopPrank(); - - // set dispute judgement - vm.startPrank(arbitrationRelayer); - uint256 ipAccount1USDCBalanceBefore = IERC20(USDC).balanceOf(ipAccount1); - uint256 arbitrationPolicySPUSDCBalanceBefore = IERC20(USDC).balanceOf(address(arbitrationPolicySP)); - - vm.startPrank(address(0xdeadbeef)); - vm.expectRevert(Errors.DisputeModule__NotWhitelistedArbitrationRelayer.selector); - disputeModule.setDisputeJudgement(1, false, ""); - vm.stopPrank(); - - vm.startPrank(arbitrationRelayer); - disputeModule.setDisputeJudgement(1, true, ""); - - uint256 ipAccount1USDCBalanceAfter = IERC20(USDC).balanceOf(ipAccount1); - uint256 arbitrationPolicySPUSDCBalanceAfter = IERC20(USDC).balanceOf(address(arbitrationPolicySP)); - vm.stopPrank(); - - assertEq(ipAccount1USDCBalanceAfter - ipAccount1USDCBalanceBefore, ARBITRATION_PRICE); - assertEq(arbitrationPolicySPUSDCBalanceBefore - arbitrationPolicySPUSDCBalanceAfter, ARBITRATION_PRICE); - } - - // TODO - function test_DisputeModule_cancelDispute() public { - // raise dispute - vm.startPrank(ipAccount1); - IERC20(USDC).approve(address(arbitrationPolicySP), ARBITRATION_PRICE); - disputeModule.raiseDispute(address(1), address(arbitrationPolicySP), string("urlExample"), "plagiarism", ""); - vm.stopPrank(); - - // cancel dispute - - vm.prank(address(0xdeadbeef)); - vm.expectRevert(Errors.DisputeModule__NotDisputeInitiator.selector); - disputeModule.cancelDispute(1, ""); - - vm.startPrank(ipAccount1); - disputeModule.cancelDispute(1, ""); - vm.stopPrank(); - } - - // TODO - function test_DisputeModule_resolveDispute() public { - // raise dispute - vm.startPrank(ipAccount1); - IERC20(USDC).approve(address(arbitrationPolicySP), ARBITRATION_PRICE); - disputeModule.raiseDispute(address(1), address(arbitrationPolicySP), string("urlExample"), "plagiarism", ""); - vm.stopPrank(); - - // resolve dispute - vm.prank(address(0xdeadbeef)); - vm.expectRevert(Errors.DisputeModule__NotDisputeInitiator.selector); - disputeModule.resolveDispute(1); - } -} diff --git a/test/foundry/IPAccountMetaTx.t.sol b/test/foundry/IPAccountMetaTx.t.sol new file mode 100644 index 00000000..d05faf40 --- /dev/null +++ b/test/foundry/IPAccountMetaTx.t.sol @@ -0,0 +1,470 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +import { ERC6551Registry } from "lib/reference/src/ERC6551Registry.sol"; +import "lib/reference/src/interfaces/IERC6551Account.sol"; + +import "contracts/IPAccountImpl.sol"; +import "contracts/interfaces/IIPAccount.sol"; +import "contracts/interfaces/IAccessController.sol"; +import "contracts/registries/IPAccountRegistry.sol"; +import "contracts/registries/ModuleRegistry.sol"; + +import "contracts/AccessController.sol"; +import "test/foundry/mocks/MockERC721.sol"; +import "test/foundry/mocks/MockModule.sol"; +import "test/foundry/mocks/MockMetaTxModule.sol"; + +import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; +import { SignatureChecker } from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol"; +import { MetaTx } from "contracts/lib/MetaTx.sol"; +import "contracts/lib/AccessPermission.sol"; + +contract IPAccountMetaTxTest is Test { + IPAccountRegistry public registry; + IPAccountImpl public implementation; + MockERC721 nft = new MockERC721(); + ERC6551Registry public erc6551Registry = new ERC6551Registry(); + AccessController public accessController = new AccessController(); + ModuleRegistry public moduleRegistry = new ModuleRegistry(); + MockModule public module; + MockMetaTxModule public metaTxModule; + + uint256 public ownerPrivateKey; + uint256 public callerPrivateKey; + address public owner; + address public caller; + + function setUp() public { + ownerPrivateKey = 0xA11111; + callerPrivateKey = 0xB22222; + owner = vm.addr(ownerPrivateKey); + caller = vm.addr(callerPrivateKey); + + implementation = new IPAccountImpl(); + registry = new IPAccountRegistry(address(erc6551Registry), address(accessController), address(implementation)); + accessController.initialize(address(registry), address(moduleRegistry)); + module = new MockModule(address(registry), address(moduleRegistry), "Module1WithPermission"); + metaTxModule = new MockMetaTxModule(address(registry), address(moduleRegistry), address(accessController)); + moduleRegistry.registerModule("Module1WithPermission", address(module)); + moduleRegistry.registerModule("MockMetaTxModule", address(metaTxModule)); + } + + // test called by unauthorized module with signature + // test signature expired + // test signature invalid + // test signature does not match to parameters + // test signature is not signed by signer + // test signature signed by unauthorized signer + // test signature signed by another contract + // test signature signed by unauthorized contract + // test setPermission with signature + // reuse the signature + + function test_IPAccount_ExecutionPassWithSignature() public { + uint256 tokenId = 100; + + nft.mintId(owner, tokenId); + + address account = registry.registerIpAccount(block.chainid, address(nft), tokenId); + + IIPAccount ipAccount = IIPAccount(payable(account)); + + uint deadline = block.timestamp + 1000; + + bytes32 digest = MessageHashUtils.toTypedDataHash( + MetaTx.calculateDomainSeparator(address(ipAccount)), + MetaTx.getExecuteStructHash( + MetaTx.Execute({ + to: address(module), + value: 0, + data: abi.encodeWithSignature("executeSuccessfully(string)", "test"), + nonce: ipAccount.state() + 1, + deadline: deadline + }) + ) + ); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest); + + bytes memory signature = abi.encodePacked(r, s, v); + vm.prank(caller); + bytes memory result = metaTxModule.callAnotherModuleWithSignature( + payable(address(ipAccount)), + owner, + deadline, + signature + ); + assertEq("test", abi.decode(result, (string))); + + assertEq(ipAccount.state(), 1); + } + + function test_IPAccount_setPermissionWithSignature() public { + uint256 tokenId = 100; + + nft.mintId(owner, tokenId); + + address account = registry.registerIpAccount(block.chainid, address(nft), tokenId); + + IIPAccount ipAccount = IIPAccount(payable(account)); + + uint deadline = block.timestamp + 1000; + + bytes32 digest = MessageHashUtils.toTypedDataHash( + MetaTx.calculateDomainSeparator(address(ipAccount)), + MetaTx.getExecuteStructHash( + MetaTx.Execute({ + to: address(accessController), + value: 0, + data: abi.encodeWithSignature( + "setPermission(address,address,address,bytes4,uint8)", + address(ipAccount), + address(metaTxModule), + address(module), + bytes4(0), + AccessPermission.ALLOW + ), + nonce: ipAccount.state() + 1, + deadline: deadline + }) + ) + ); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest); + + bytes memory signature = abi.encodePacked(r, s, v); + vm.prank(caller); + bytes memory result = metaTxModule.setPermissionThenCallOtherModules( + payable(address(ipAccount)), + owner, + deadline, + signature + ); + assertEq("test", abi.decode(result, (string))); + + assertEq(ipAccount.state(), 2); + } + + function test_IPAccount_revert_SignatureExpired() public { + uint256 tokenId = 100; + + nft.mintId(owner, tokenId); + + address account = registry.registerIpAccount(block.chainid, address(nft), tokenId); + + IIPAccount ipAccount = IIPAccount(payable(account)); + + uint deadline = 0; + + bytes32 digest = MessageHashUtils.toTypedDataHash( + MetaTx.calculateDomainSeparator(address(ipAccount)), + MetaTx.getExecuteStructHash( + MetaTx.Execute({ + to: address(module), + value: 0, + data: abi.encodeWithSignature("executeSuccessfully(string)", "test"), + nonce: ipAccount.state() + 1, + deadline: deadline + }) + ) + ); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest); + + bytes memory signature = abi.encodePacked(r, s, v); + + vm.expectRevert(abi.encodeWithSelector(Errors.IPAccount__ExpiredSignature.selector)); + vm.prank(caller); + metaTxModule.callAnotherModuleWithSignature(payable(address(ipAccount)), owner, deadline, signature); + assertEq(ipAccount.state(), 0); + } + + function test_IPAccount_revert_InvalidSignature() public { + uint256 tokenId = 100; + + nft.mintId(owner, tokenId); + + address account = registry.registerIpAccount(block.chainid, address(nft), tokenId); + + IIPAccount ipAccount = IIPAccount(payable(account)); + + uint deadline = block.timestamp + 1000; + + bytes32 digest = MessageHashUtils.toTypedDataHash( + MetaTx.calculateDomainSeparator(address(ipAccount)), + MetaTx.getExecuteStructHash( + MetaTx.Execute({ + to: address(module), + value: 0, + data: abi.encodeWithSignature("executeSuccessfully(string)", "test"), + nonce: ipAccount.state() + 1, + deadline: deadline + }) + ) + ); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest); + // bad signature + bytes memory invalidSignature = abi.encodePacked(r, s, v + 1); + + vm.expectRevert(abi.encodeWithSelector(Errors.IPAccount__InvalidSignature.selector)); + vm.prank(caller); + metaTxModule.callAnotherModuleWithSignature(payable(address(ipAccount)), owner, deadline, invalidSignature); + assertEq(ipAccount.state(), 0); + } + + function test_IPAccount_revert_SignatureNotMatchExecuteTargetFunction() public { + uint256 tokenId = 100; + + nft.mintId(owner, tokenId); + + address account = registry.registerIpAccount(block.chainid, address(nft), tokenId); + + IIPAccount ipAccount = IIPAccount(payable(account)); + + uint deadline = block.timestamp + 1000; + + bytes32 digest = MessageHashUtils.toTypedDataHash( + MetaTx.calculateDomainSeparator(address(ipAccount)), + MetaTx.getExecuteStructHash( + MetaTx.Execute({ + to: address(module), + value: 0, + data: abi.encodeWithSignature("UnMatchedFunction(string)", "test"), + nonce: ipAccount.state() + 1, + deadline: deadline + }) + ) + ); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest); + + bytes memory signature = abi.encodePacked(r, s, v); + + vm.expectRevert(abi.encodeWithSelector(Errors.IPAccount__InvalidSignature.selector)); + vm.prank(caller); + metaTxModule.callAnotherModuleWithSignature(payable(address(ipAccount)), owner, deadline, signature); + assertEq(ipAccount.state(), 0); + } + + function test_IPAccount_revert_WrongSigner() public { + uint256 tokenId = 100; + + nft.mintId(owner, tokenId); + + address account = registry.registerIpAccount(block.chainid, address(nft), tokenId); + + IIPAccount ipAccount = IIPAccount(payable(account)); + + uint deadline = block.timestamp + 1000; + + bytes32 digest = MessageHashUtils.toTypedDataHash( + MetaTx.calculateDomainSeparator(address(ipAccount)), + MetaTx.getExecuteStructHash( + MetaTx.Execute({ + to: address(module), + value: 0, + data: abi.encodeWithSignature("executeSuccessfully(string)", "test"), + nonce: ipAccount.state() + 1, + deadline: deadline + }) + ) + ); + + // wrong signer + (uint8 v, bytes32 r, bytes32 s) = vm.sign(callerPrivateKey, digest); + + bytes memory signature = abi.encodePacked(r, s, v); + + vm.expectRevert(abi.encodeWithSelector(Errors.IPAccount__InvalidSignature.selector)); + vm.prank(caller); + metaTxModule.callAnotherModuleWithSignature(payable(address(ipAccount)), owner, deadline, signature); + assertEq(ipAccount.state(), 0); + } + + function test_IPAccount_revert_SignatureForAnotherIPAccount() public { + uint256 tokenId = 100; + + nft.mintId(owner, tokenId); + + address account = registry.registerIpAccount(block.chainid, address(nft), tokenId); + + IIPAccount ipAccount = IIPAccount(payable(account)); + + uint256 tokenId2 = 101; + nft.mintId(owner, tokenId2); + address account2 = registry.registerIpAccount(block.chainid, address(nft), tokenId2); + IIPAccount ipAccount2 = IIPAccount(payable(account2)); + + uint deadline = block.timestamp + 1000; + + // signature for another ipAccount + bytes32 digest = MessageHashUtils.toTypedDataHash( + MetaTx.calculateDomainSeparator(account2), + MetaTx.getExecuteStructHash( + MetaTx.Execute({ + to: address(module), + value: 0, + data: abi.encodeWithSignature("executeSuccessfully(string)", "test"), + nonce: ipAccount2.state(), + deadline: deadline + }) + ) + ); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest); + + bytes memory signature = abi.encodePacked(r, s, v); + + vm.expectRevert(abi.encodeWithSelector(Errors.IPAccount__InvalidSignature.selector)); + vm.prank(caller); + metaTxModule.callAnotherModuleWithSignature(payable(address(ipAccount)), owner, deadline, signature); + assertEq(ipAccount.state(), 0); + } + + function test_IPAccount_revert_signedByNonOwner() public { + uint256 tokenId = 100; + + nft.mintId(owner, tokenId); + + address account = registry.registerIpAccount(block.chainid, address(nft), tokenId); + + IIPAccount ipAccount = IIPAccount(payable(account)); + + uint deadline = block.timestamp + 1000; + + bytes32 digest = MessageHashUtils.toTypedDataHash( + MetaTx.calculateDomainSeparator(address(ipAccount)), + MetaTx.getExecuteStructHash( + MetaTx.Execute({ + to: address(module), + value: 0, + data: abi.encodeWithSignature("executeSuccessfully(string)", "test"), + nonce: ipAccount.state() + 1, + deadline: deadline + }) + ) + ); + + // signed by non-owner + (uint8 v, bytes32 r, bytes32 s) = vm.sign(callerPrivateKey, digest); + + bytes memory signature = abi.encodePacked(r, s, v); + + vm.expectRevert("Invalid signer"); + vm.prank(caller); + metaTxModule.callAnotherModuleWithSignature(payable(address(ipAccount)), caller, deadline, signature); + assertEq(ipAccount.state(), 0); + } + + function test_IPAccount_revert_UseSignatureTwice() public { + uint256 tokenId = 100; + + nft.mintId(owner, tokenId); + + address account = registry.registerIpAccount(block.chainid, address(nft), tokenId); + + IIPAccount ipAccount = IIPAccount(payable(account)); + + uint deadline = block.timestamp + 1000; + + bytes32 digest = MessageHashUtils.toTypedDataHash( + MetaTx.calculateDomainSeparator(address(ipAccount)), + MetaTx.getExecuteStructHash( + MetaTx.Execute({ + to: address(module), + value: 0, + data: abi.encodeWithSignature("executeSuccessfully(string)", "test"), + nonce: ipAccount.state() + 1, + deadline: deadline + }) + ) + ); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest); + bytes memory signature = abi.encodePacked(r, s, v); + // first time pass + vm.prank(caller); + metaTxModule.callAnotherModuleWithSignature(payable(address(ipAccount)), owner, deadline, signature); + assertEq(ipAccount.state(), 1); + // second time fail + vm.expectRevert(abi.encodeWithSelector(Errors.IPAccount__InvalidSignature.selector)); + vm.prank(caller); + metaTxModule.callAnotherModuleWithSignature(payable(address(ipAccount)), owner, deadline, signature); + assertEq(ipAccount.state(), 1); + } + + function test_IPAccount_revert_signerZeroAddress() public { + uint256 tokenId = 100; + + nft.mintId(owner, tokenId); + + address account = registry.registerIpAccount(block.chainid, address(nft), tokenId); + + IIPAccount ipAccount = IIPAccount(payable(account)); + + uint deadline = block.timestamp + 1000; + + bytes32 digest = MessageHashUtils.toTypedDataHash( + MetaTx.calculateDomainSeparator(address(ipAccount)), + MetaTx.getExecuteStructHash( + MetaTx.Execute({ + to: address(module), + value: 0, + data: abi.encodeWithSignature("executeSuccessfully(string)", "test"), + nonce: ipAccount.state() + 1, + deadline: deadline + }) + ) + ); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest); + // bad signature + bytes memory signature = abi.encodePacked(r, s, v); + + vm.expectRevert(abi.encodeWithSelector(Errors.IPAccount__InvalidSigner.selector)); + vm.prank(caller); + metaTxModule.callAnotherModuleWithSignature(payable(address(ipAccount)), address(0), deadline, signature); + assertEq(ipAccount.state(), 0); + } + + function test_IPAccount_revert_workflowFailureWithSig() public { + uint256 tokenId = 100; + + nft.mintId(owner, tokenId); + + address account = registry.registerIpAccount(block.chainid, address(nft), tokenId); + + IIPAccount ipAccount = IIPAccount(payable(account)); + + uint deadline = block.timestamp + 1000; + + bytes32 digest = MessageHashUtils.toTypedDataHash( + MetaTx.calculateDomainSeparator(address(ipAccount)), + MetaTx.getExecuteStructHash( + MetaTx.Execute({ + to: address(module), + value: 0, + data: abi.encodeWithSignature("executeSuccessfully(string)", "test"), + nonce: ipAccount.state() + 1, + deadline: deadline + }) + ) + ); + + // signed by non-owner + (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest); + + bytes memory signature = abi.encodePacked(r, s, v); + + vm.expectRevert("Invalid signer"); + vm.prank(caller); + metaTxModule.workflowFailureWithSignature(payable(address(ipAccount)), owner, deadline, signature); + assertEq(ipAccount.state(), 0); + } + +} diff --git a/test/foundry/IPRecordRegistry.t.sol b/test/foundry/IPRecordRegistry.t.sol index 7f23c258..b0989356 100644 --- a/test/foundry/IPRecordRegistry.t.sol +++ b/test/foundry/IPRecordRegistry.t.sol @@ -5,6 +5,7 @@ import { BaseTest } from "./utils/BaseTest.sol"; import { IModuleRegistry } from "contracts/interfaces/registries/IModuleRegistry.sol"; import { IIPRecordRegistry } from "contracts/interfaces/registries/IIPRecordRegistry.sol"; import { IPAccountChecker } from "contracts/lib/registries/IPAccountChecker.sol"; +import { IPMetadataProvider } from "contracts/registries/metadata/IPMetadataProvider.sol"; import { IPAccountRegistry } from "contracts/registries/IPAccountRegistry.sol"; import { ERC6551Registry } from "lib/reference/src/ERC6551Registry.sol"; import { IPRecordRegistry } from "contracts/registries/IPRecordRegistry.sol"; @@ -34,6 +35,9 @@ contract IPRecordRegistryTest is BaseTest { /// @notice The IP account registry used for account creation. IPAccountRegistry public ipAccountRegistry; + /// @notice The IP metadata provider associated with an IP. + IPMetadataProvider public metadataProvider; + /// @notice Mock NFT address for IP registration testing. address public tokenAddress; @@ -54,6 +58,7 @@ contract IPRecordRegistryTest is BaseTest { moduleRegistry = IModuleRegistry( address(new MockModuleRegistry(registrationModule)) ); + metadataProvider = new IPMetadataProvider(address(moduleRegistry)); ipAccountImpl = address(new IPAccountImpl()); ipAccountRegistry = new IPAccountRegistry( erc6551Registry, @@ -124,7 +129,8 @@ contract IPRecordRegistryTest is BaseTest { block.chainid, tokenAddress, tokenId, - resolver + resolver, + address(metadataProvider) ); vm.prank(registrationModule); registry.register( @@ -132,7 +138,8 @@ contract IPRecordRegistryTest is BaseTest { tokenAddress, tokenId, resolver, - true + true, + address(metadataProvider) ); /// Ensures IP record post-registration conditions are met. @@ -168,7 +175,8 @@ contract IPRecordRegistryTest is BaseTest { block.chainid, tokenAddress, tokenId, - resolver + resolver, + address(metadataProvider) ); vm.prank(registrationModule); registry.register( @@ -176,7 +184,8 @@ contract IPRecordRegistryTest is BaseTest { tokenAddress, tokenId, resolver, - false + false, + address(metadataProvider) ); /// Ensures IP record post-registration conditions are met. @@ -188,7 +197,7 @@ contract IPRecordRegistryTest is BaseTest { /// @notice Tests registration of IP records works with existing IP accounts. function test_IPRecordRegistry_RegisterExistingAccount() public { - address ipId = registry.createIPAccount(block.chainid, tokenAddress, tokenId); + registry.createIPAccount(block.chainid, tokenAddress, tokenId); assertTrue(IPAccountChecker.isRegistered(ipAccountRegistry, block.chainid, tokenAddress, tokenId)); vm.prank(registrationModule); registry.register( @@ -196,7 +205,8 @@ contract IPRecordRegistryTest is BaseTest { tokenAddress, tokenId, resolver, - true + true, + address(metadataProvider) ); } @@ -204,22 +214,22 @@ contract IPRecordRegistryTest is BaseTest { /// @notice Tests registration of IP reverts when an IP has already been registered. function test_IPRecordRegistry_Register_Reverts_ExistingRegistration() public { vm.startPrank(registrationModule); - registry.register(block.chainid, tokenAddress, tokenId, resolver, false); + registry.register(block.chainid, tokenAddress, tokenId, resolver, false, address(metadataProvider)); vm.expectRevert(Errors.IPRecordRegistry_AlreadyRegistered.selector); - registry.register(block.chainid, tokenAddress, tokenId, resolver, false); + registry.register(block.chainid, tokenAddress, tokenId, resolver, false, address(metadataProvider)); } /// @notice Tests registration of IP reverts if not called by a registration module. function test_IPRecordRegistry_Register_Reverts_InvalidRegistrationModule() public { vm.expectRevert(Errors.IPRecordRegistry_Unauthorized.selector); vm.prank(alice); - registry.register(block.chainid, tokenAddress, tokenId, resolver, false); + registry.register(block.chainid, tokenAddress, tokenId, resolver, false, address(metadataProvider)); } /// @notice Tests generic IP account creation works. function test_IPRecordRegistry_CreateIPAccount() public { assertTrue(!IPAccountChecker.isRegistered(ipAccountRegistry, block.chainid, tokenAddress, tokenId)); - address ipId = registry.createIPAccount(block.chainid, tokenAddress, tokenId); + registry.createIPAccount(block.chainid, tokenAddress, tokenId); assertTrue(IPAccountChecker.isRegistered(ipAccountRegistry, block.chainid, tokenAddress, tokenId)); } @@ -240,7 +250,7 @@ contract IPRecordRegistryTest is BaseTest { ipAccountRegistry.IP_ACCOUNT_SALT() ); vm.startPrank(registrationModule); - registry.register(block.chainid, tokenAddress, tokenId, resolver, false); + registry.register(block.chainid, tokenAddress, tokenId, resolver, false, address(metadataProvider)); vm.expectEmit(true, true, true, true); emit IIPRecordRegistry.IPResolverSet( diff --git a/test/foundry/mocks/MockMetaTxModule.sol b/test/foundry/mocks/MockMetaTxModule.sol new file mode 100644 index 00000000..68b646dd --- /dev/null +++ b/test/foundry/mocks/MockMetaTxModule.sol @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +import "contracts/interfaces/modules/base/IModule.sol"; +import "contracts/interfaces/IIPAccount.sol"; +import "contracts/interfaces/registries/IModuleRegistry.sol"; +import "contracts/interfaces/registries/IIPAccountRegistry.sol"; +import { IPAccountChecker } from "contracts/lib/registries/IPAccountChecker.sol"; +import { IIPAccount } from "contracts/interfaces/IIPAccount.sol"; +import "contracts/interfaces/IAccessController.sol"; +import "contracts/lib/AccessPermission.sol"; + +contract MockMetaTxModule is IModule { + using IPAccountChecker for IIPAccountRegistry; + + IIPAccountRegistry public ipAccountRegistry; + IModuleRegistry public moduleRegistry; + IAccessController accessController; + + constructor(address _ipAccountRegistry, address _moduleRegistry, address _accessController) { + ipAccountRegistry = IIPAccountRegistry(_ipAccountRegistry); + moduleRegistry = IModuleRegistry(_moduleRegistry); + accessController = IAccessController(_accessController); + } + + function name() external pure returns (string memory) { + return "MockMetaTxModule"; + } + + function setPermissionThenCallOtherModules( + address payable ipAccount, + address signer, + uint256 deadline, + bytes calldata signature + ) external returns (bytes memory) { + // check if ipAccount is valid + require(ipAccountRegistry.isIpAccount(ipAccount), "MockMetaTxModule: ipAccount is not valid"); + address module1 = moduleRegistry.getModule("Module1WithPermission"); + // set permission + IIPAccount(ipAccount).executeWithSig( + address(accessController), + 0, + abi.encodeWithSignature( + "setPermission(address,address,address,bytes4,uint8)", + ipAccount, + address(this), + module1, + bytes4(0), + AccessPermission.ALLOW + ), + signer, + deadline, + signature + ); + // execute module1 + bytes memory module1Output = IIPAccount(ipAccount).execute( + module1, + 0, + abi.encodeWithSignature("executeSuccessfully(string)", "test") + ); + return module1Output; + } + + function callAnotherModuleWithSignature( + address payable ipAccount, + address signer, + uint256 deadline, + bytes calldata signature + ) external returns (bytes memory) { + // check if ipAccount is valid + require(ipAccountRegistry.isIpAccount(ipAccount), "MockMetaTxModule: ipAccount is not valid"); + + // call modules + // workflow does have permission to call a module + address module1 = moduleRegistry.getModule("Module1WithPermission"); + // execute module1 + bytes memory module1Output = IIPAccount(ipAccount).executeWithSig( + module1, + 0, + abi.encodeWithSignature("executeSuccessfully(string)", "test"), + signer, + deadline, + signature + ); + return module1Output; + } + + function workflowFailureWithSignature( + address payable ipAccount, + address signer, + uint256 deadline, + bytes calldata signature + ) external { + // check if ipAccount is valid + require(ipAccountRegistry.isIpAccount(ipAccount), "MockModule: ipAccount is not valid"); + + // call modules + // workflow does have permission to call a module + address module1 = moduleRegistry.getModule("Module1WithPermission"); + address module3WithoutPermission = moduleRegistry.getModule("Module3WithoutPermission"); + // workflow call 1st module + bytes memory module1Output = IIPAccount(ipAccount).executeWithSig( + module1, + 0, + abi.encodeWithSignature("executeSuccessfully(string)", "test"), + signer, + deadline, + signature + ); + // workflow call 2nd module WITHOUT permission + // the call should fail + IIPAccount(ipAccount).execute( + module3WithoutPermission, + 0, + abi.encodeWithSignature("executeNoReturn(string)", abi.decode(module1Output, (string))) + ); + } +} diff --git a/test/foundry/modules/ModuleBase.t.sol b/test/foundry/modules/ModuleBase.t.sol index d1f40eb6..e4e7b847 100644 --- a/test/foundry/modules/ModuleBase.t.sol +++ b/test/foundry/modules/ModuleBase.t.sol @@ -16,7 +16,6 @@ import { IPAccountRegistry } from "contracts/registries/IPAccountRegistry.sol"; import { MockModuleRegistry } from "test/foundry/mocks/MockModuleRegistry.sol"; import { IIPRecordRegistry } from "contracts/interfaces/registries/IIPRecordRegistry.sol"; import { IPAccountImpl} from "contracts/IPAccountImpl.sol"; -import { IIPMetadataResolver } from "contracts/interfaces/resolvers/IIPMetadataResolver.sol"; import { MockERC721 } from "test/foundry/mocks/MockERC721.sol"; import { IP } from "contracts/lib/IP.sol"; import { Errors } from "contracts/lib/Errors.sol"; diff --git a/test/foundry/modules/RegistrationModule.t.sol b/test/foundry/modules/RegistrationModule.t.sol index 30edb9bc..364fedf9 100644 --- a/test/foundry/modules/RegistrationModule.t.sol +++ b/test/foundry/modules/RegistrationModule.t.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.23; import { MockAccessController } from "test/foundry/mocks/MockAccessController.sol"; import { BaseTest } from "test/foundry/utils/BaseTest.sol"; import { IModule } from "contracts/interfaces/modules/base/IModule.sol"; +import { IPMetadataProvider } from "contracts/registries/metadata/IPMetadataProvider.sol"; import { ModuleBaseTest } from "./ModuleBase.t.sol"; import { IPAccountChecker } from "contracts/lib/registries/IPAccountChecker.sol"; import { IAccessController } from "contracts/interfaces/IAccessController.sol"; @@ -12,19 +13,19 @@ import { ModuleRegistry } from "contracts/registries/ModuleRegistry.sol"; import { IModuleRegistry } from "contracts/interfaces/registries/IModuleRegistry.sol"; import { IPRecordRegistry } from "contracts/registries/IPRecordRegistry.sol"; import { IPAccountRegistry } from "contracts/registries/IPAccountRegistry.sol"; -import { IPMetadataResolver } from "contracts/resolvers/IPMetadataResolver.sol"; +import { IPAssetRenderer } from "contracts/registries/metadata/IPAssetRenderer.sol"; +import { IPResolver } from "contracts/resolvers/IPResolver.sol"; import { RegistrationModule } from "contracts/modules/RegistrationModule.sol"; import { MockModuleRegistry } from "test/foundry/mocks/MockModuleRegistry.sol"; import { IIPRecordRegistry } from "contracts/interfaces/registries/IIPRecordRegistry.sol"; import { IPAccountImpl} from "contracts/IPAccountImpl.sol"; -import { IIPMetadataResolver } from "contracts/interfaces/resolvers/IIPMetadataResolver.sol"; import { MockERC721 } from "test/foundry/mocks/MockERC721.sol"; import { IParamVerifier } from "contracts/interfaces/licensing/IParamVerifier.sol"; import { MockIParamVerifier } from "test/foundry/mocks/licensing/MockParamVerifier.sol"; import { Licensing } from "contracts/lib/Licensing.sol"; import { IP } from "contracts/lib/IP.sol"; import { Errors } from "contracts/lib/Errors.sol"; -import { METADATA_RESOLVER_MODULE_KEY, REGISTRATION_MODULE_KEY } from "contracts/lib/modules/Module.sol"; +import { IP_RESOLVER_MODULE_KEY, REGISTRATION_MODULE_KEY } from "contracts/lib/modules/Module.sol"; import { IIPAccount } from "contracts/interfaces/IIPAccount.sol"; /// @title IP Registration Module Test Contract @@ -33,14 +34,20 @@ contract RegistrationModuleTest is ModuleBaseTest { // Default IP record attributes. string public constant RECORD_NAME = "IPRecord"; - string public constant RECORD_DESCRIPTION = "IPs all the way down."; bytes32 public constant RECORD_HASH = ""; + string public constant RECORD_URL = "https://ipasset.xyz"; + + /// @notice IP metadata rendering contract. + IPAssetRenderer public renderer; + + /// @notice The Story Protocol default IP resolver. + IPResolver public resolver; /// @notice The registration module SUT. RegistrationModule public registrationModule; - /// @notice Gets the IP metadata resolver tied to the registration module. - IIPMetadataResolver public resolver; + /// @notice Gets the contract responsible for IP metadata provisioning. + IPMetadataProvider public metadataProvider; /// @notice Mock NFT address for IP registration testing. address public tokenAddress; @@ -58,15 +65,22 @@ contract RegistrationModuleTest is ModuleBaseTest { function setUp() public virtual override(ModuleBaseTest) { ModuleBaseTest.setUp(); _initLicensing(); - resolver = new IPMetadataResolver( + resolver = new IPResolver( address(accessController), address(ipRecordRegistry), address(ipAccountRegistry), address(licenseRegistry) ); + metadataProvider = new IPMetadataProvider(address(moduleRegistry)); registrationModule = RegistrationModule(_deployModule()); + renderer = new IPAssetRenderer( + address(ipRecordRegistry), + address(licenseRegistry), + vm.addr(0x1111), // TODO: Incorporate tagging module into renderer. + vm.addr(0x2111) // TODO: Incorporate royalty module into renderer. + ); moduleRegistry.registerModule(REGISTRATION_MODULE_KEY, address(registrationModule)); - moduleRegistry.registerModule(METADATA_RESOLVER_MODULE_KEY, address(resolver)); + moduleRegistry.registerModule(IP_RESOLVER_MODULE_KEY, address(resolver)); MockERC721 erc721 = new MockERC721(); tokenAddress = address(erc721); tokenId = erc721.mintId(alice, 99); @@ -79,7 +93,6 @@ contract RegistrationModuleTest is ModuleBaseTest { assertEq(address(registrationModule.IP_ACCOUNT_REGISTRY()), address(ipAccountRegistry)); assertEq(address(registrationModule.IP_RECORD_REGISTRY()), address(ipRecordRegistry)); assertEq(address(registrationModule.LICENSE_REGISTRY()), address(licenseRegistry)); - assertEq(address(registrationModule.resolver()), address(resolver)); } /// @notice Checks whether root IP registration operates as expected. @@ -102,6 +115,7 @@ contract RegistrationModuleTest is ModuleBaseTest { /// Ensure registered IP postconditiosn are met. assertEq(ipRecordRegistry.resolver(ipId), address(resolver)); + assertEq(ipRecordRegistry.metadataProvider(ipId), address(metadataProvider)); assertEq(totalSupply + 1, ipRecordRegistry.totalSupply()); assertTrue(ipRecordRegistry.isRegistered(ipId)); assertTrue(ipRecordRegistry.isRegistered(block.chainid, tokenAddress, tokenId)); @@ -148,8 +162,8 @@ contract RegistrationModuleTest is ModuleBaseTest { tokenAddress, tokenId2, RECORD_NAME, - RECORD_DESCRIPTION, - RECORD_HASH + RECORD_HASH, + RECORD_URL ); @@ -158,10 +172,9 @@ contract RegistrationModuleTest is ModuleBaseTest { assertEq(totalSupply + 1, ipRecordRegistry.totalSupply()); assertTrue(ipRecordRegistry.isRegistered(ipId2)); assertTrue(ipRecordRegistry.isRegistered(block.chainid, tokenAddress, tokenId2)); - assertEq(resolver.name(ipId2), RECORD_NAME); - assertEq(resolver.description(ipId2), RECORD_DESCRIPTION); - assertEq(resolver.hash(ipId2), RECORD_HASH); - assertEq(resolver.owner(ipId2), bob); + assertEq(renderer.name(ipId2), RECORD_NAME); + assertEq(renderer.hash(ipId2), RECORD_HASH); + assertEq(renderer.owner(ipId2), bob); } /// @notice Checks that registration reverts when called by an invalid owner. @@ -172,8 +185,8 @@ contract RegistrationModuleTest is ModuleBaseTest { tokenAddress, tokenId, RECORD_NAME, - RECORD_DESCRIPTION, - RECORD_HASH + RECORD_HASH, + RECORD_URL ); } @@ -185,7 +198,8 @@ contract RegistrationModuleTest is ModuleBaseTest { address(ipRecordRegistry), address(ipAccountRegistry), address(licenseRegistry), - address(resolver) + address(resolver), + address(metadataProvider) ) ); } diff --git a/test/foundry/modules/dispute/ArbitrationPolicySP.t.sol b/test/foundry/modules/dispute/ArbitrationPolicySP.t.sol new file mode 100644 index 00000000..b2f3c93d --- /dev/null +++ b/test/foundry/modules/dispute/ArbitrationPolicySP.t.sol @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.23; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import {Errors} from "contracts/lib/Errors.sol"; +import {ArbitrationPolicySP} from "contracts/modules/dispute-module/policies/ArbitrationPolicySP.sol"; +import {TestHelper} from "test/utils/TestHelper.sol"; + +contract TestArbitrationPolicySP is TestHelper { + function setUp() public override { + super.setUp(); + + // fund USDC + vm.startPrank(USDC_RICH); + IERC20(USDC).transfer(ipAccount1, 1000 * 10 ** 6); + vm.stopPrank(); + + // whitelist dispute tag + disputeModule.whitelistDisputeTags("PLAGIARISM", true); + + // whitelist arbitration policy + disputeModule.whitelistArbitrationPolicy(address(arbitrationPolicySP), true); + + // whitelist arbitration relayer + disputeModule.whitelistArbitrationRelayer(address(arbitrationPolicySP), arbitrationRelayer, true); + } + + function test_ArbitrationPolicySP_constructor_ZeroDisputeModule() public { + address disputeModule = address(0); + address paymentToken = address(1); + uint256 arbitrationPrice = 1000; + + vm.expectRevert(Errors.ArbitrationPolicySP__ZeroDisputeModule.selector); + new ArbitrationPolicySP(disputeModule, paymentToken, arbitrationPrice); + } + + function test_ArbitrationPolicySP_constructor_ZeroPaymentToken() public { + address disputeModule = address(1); + address paymentToken = address(0); + uint256 arbitrationPrice = 1000; + + vm.expectRevert(Errors.ArbitrationPolicySP__ZeroPaymentToken.selector); + new ArbitrationPolicySP(disputeModule, paymentToken, arbitrationPrice); + } + + function test_ArbitrationPolicySP_constructor() public { + address disputeModule = address(1); + address paymentToken = address(2); + uint256 arbitrationPrice = 1000; + + ArbitrationPolicySP arbitrationPolicySP = new ArbitrationPolicySP(disputeModule, paymentToken, arbitrationPrice); + + assertEq(address(arbitrationPolicySP.DISPUTE_MODULE()), disputeModule); + assertEq(address(arbitrationPolicySP.PAYMENT_TOKEN()), paymentToken); + assertEq(arbitrationPolicySP.ARBITRATION_PRICE(), arbitrationPrice); + } + + function test_ArbitrationPolicySP_onRaiseDispute_NotDisputeModule() public { + vm.expectRevert(Errors.ArbitrationPolicySP__NotDisputeModule.selector); + arbitrationPolicySP.onRaiseDispute(address(1), new bytes(0)); + } + + function test_ArbitrationPolicySP_onRaiseDispute() public { + address caller = address(1); + vm.startPrank(caller); + IERC20(USDC).approve(address(arbitrationPolicySP), ARBITRATION_PRICE); + vm.stopPrank(); + + vm.startPrank(address(disputeModule)); + + uint256 userUSDCBalBefore = IERC20(USDC).balanceOf(caller); + uint256 arbitrationContractBalBefore = IERC20(USDC).balanceOf(address(arbitrationPolicySP)); + + arbitrationPolicySP.onRaiseDispute(caller, new bytes(0)); + + uint256 userUSDCBalAfter = IERC20(USDC).balanceOf(caller); + uint256 arbitrationContractBalAfter = IERC20(USDC).balanceOf(address(arbitrationPolicySP)); + + assertEq(userUSDCBalBefore - userUSDCBalAfter, ARBITRATION_PRICE); + assertEq(arbitrationContractBalAfter - arbitrationContractBalBefore, ARBITRATION_PRICE); + } + + function test_ArbitrationPolicySP_onDisputeJudgement_NotDisputeModule() public { + vm.expectRevert(Errors.ArbitrationPolicySP__NotDisputeModule.selector); + arbitrationPolicySP.onDisputeJudgement(1, true, new bytes(0)); + } + + function test_ArbitrationPolicySP_onDisputeJudgement_True() public { + // raise dispute + vm.startPrank(ipAccount1); + IERC20(USDC).approve(address(arbitrationPolicySP), ARBITRATION_PRICE); + disputeModule.raiseDispute(ipAccount1, address(arbitrationPolicySP), string("urlExample"), "PLAGIARISM", ""); + vm.stopPrank(); + + // set dispute judgement + uint256 ipAccount1USDCBalanceBefore = IERC20(USDC).balanceOf(ipAccount1); + uint256 arbitrationPolicySPUSDCBalanceBefore = IERC20(USDC).balanceOf(address(arbitrationPolicySP)); + + vm.startPrank(arbitrationRelayer); + disputeModule.setDisputeJudgement(1, true, ""); + + uint256 ipAccount1USDCBalanceAfter = IERC20(USDC).balanceOf(ipAccount1); + uint256 arbitrationPolicySPUSDCBalanceAfter = IERC20(USDC).balanceOf(address(arbitrationPolicySP)); + + assertEq(ipAccount1USDCBalanceAfter - ipAccount1USDCBalanceBefore, ARBITRATION_PRICE); + assertEq(arbitrationPolicySPUSDCBalanceBefore - arbitrationPolicySPUSDCBalanceAfter, ARBITRATION_PRICE); + } + + function test_ArbitrationPolicySP_onDisputeJudgement_False() public { + // raise dispute + vm.startPrank(ipAccount1); + IERC20(USDC).approve(address(arbitrationPolicySP), ARBITRATION_PRICE); + disputeModule.raiseDispute(ipAccount1, address(arbitrationPolicySP), string("urlExample"), "PLAGIARISM", ""); + vm.stopPrank(); + + // set dispute judgement + uint256 ipAccount1USDCBalanceBefore = IERC20(USDC).balanceOf(ipAccount1); + uint256 arbitrationPolicySPUSDCBalanceBefore = IERC20(USDC).balanceOf(address(arbitrationPolicySP)); + + vm.startPrank(arbitrationRelayer); + disputeModule.setDisputeJudgement(1, false, ""); + + uint256 ipAccount1USDCBalanceAfter = IERC20(USDC).balanceOf(ipAccount1); + uint256 arbitrationPolicySPUSDCBalanceAfter = IERC20(USDC).balanceOf(address(arbitrationPolicySP)); + + assertEq(ipAccount1USDCBalanceAfter - ipAccount1USDCBalanceBefore, 0); + assertEq(arbitrationPolicySPUSDCBalanceBefore - arbitrationPolicySPUSDCBalanceAfter, 0); + } + + function test_ArbitrationPolicySP_onDisputeCancel_NotDisputeModule() public { + vm.expectRevert(Errors.ArbitrationPolicySP__NotDisputeModule.selector); + arbitrationPolicySP.onDisputeCancel(address(1), 1, new bytes(0)); + } + + // TODO + function test_ArbitrationPolicySP_withdraw() public {} +} diff --git a/test/foundry/modules/dispute/DisputeModule.t.sol b/test/foundry/modules/dispute/DisputeModule.t.sol new file mode 100644 index 00000000..c25e3727 --- /dev/null +++ b/test/foundry/modules/dispute/DisputeModule.t.sol @@ -0,0 +1,308 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.23; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import {Errors} from "contracts/lib/Errors.sol"; +import {ShortStringOps} from "contracts/utils/ShortStringOps.sol"; +import {DisputeModule} from "contracts/modules/dispute-module/DisputeModule.sol"; +import {TestHelper} from "test/utils/TestHelper.sol"; + +contract TestDisputeModule is TestHelper { + event TagWhitelistUpdated(bytes32 tag, bool allowed); + event ArbitrationPolicyWhitelistUpdated(address arbitrationPolicy, bool allowed); + event ArbitrationRelayerWhitelistUpdated(address arbitrationPolicy, address arbitrationRelayer, bool allowed); + event DisputeRaised( + uint256 disputeId, + address targetIpId, + address disputeInitiator, + address arbitrationPolicy, + bytes32 linkToDisputeEvidence, + bytes32 targetTag, + bytes data + ); + event DisputeJudgementSet(uint256 disputeId, bool decision, bytes data); + event DisputeCancelled(uint256 disputeId, bytes data); + event DisputeResolved(uint256 disputeId); + + function setUp() public override { + super.setUp(); + + // fund USDC + vm.startPrank(USDC_RICH); + IERC20(USDC).transfer(ipAccount1, ARBITRATION_PRICE); + vm.stopPrank(); + + // whitelist dispute tag + disputeModule.whitelistDisputeTags("PLAGIARISM", true); + + // whitelist arbitration policy + disputeModule.whitelistArbitrationPolicy(address(arbitrationPolicySP), true); + + // whitelist arbitration relayer + disputeModule.whitelistArbitrationRelayer(address(arbitrationPolicySP), arbitrationRelayer, true); + } + + function test_DisputeModule_whitelistDisputeTags_ZeroDisputeTag() public { + vm.expectRevert(Errors.DisputeModule__ZeroDisputeTag.selector); + disputeModule.whitelistDisputeTags(bytes32(0), true); + } + + function test_DisputeModule_whitelistDisputeTags() public { + vm.expectEmit(true, true, true, true, address(disputeModule)); + emit TagWhitelistUpdated(bytes32("INAPPROPRIATE_CONTENT"), true); + + disputeModule.whitelistDisputeTags("INAPPROPRIATE_CONTENT", true); + assertEq(disputeModule.isWhitelistedDisputeTag("INAPPROPRIATE_CONTENT"), true); + } + + function test_DisputeModule_whitelistArbitrationPolicy_ZeroArbitrationPolicy() public { + vm.expectRevert(Errors.DisputeModule__ZeroArbitrationPolicy.selector); + disputeModule.whitelistArbitrationPolicy(address(0), true); + } + + function test_DisputeModule_whitelistArbitrationPolicy() public { + vm.expectEmit(true, true, true, true, address(disputeModule)); + emit ArbitrationPolicyWhitelistUpdated(address(1), true); + + disputeModule.whitelistArbitrationPolicy(address(1), true); + + assertEq(disputeModule.isWhitelistedArbitrationPolicy(address(1)), true); + } + + function test_DisputeModule_whitelistArbitrationRelayer_ZeroArbitrationPolicy() public { + vm.expectRevert(Errors.DisputeModule__ZeroArbitrationPolicy.selector); + disputeModule.whitelistArbitrationRelayer(address(0), arbitrationRelayer, true); + } + + function test_DisputeModule_whitelistArbitrationRelayer_ZeroArbitrationRelayer() public { + vm.expectRevert(Errors.DisputeModule__ZeroArbitrationRelayer.selector); + disputeModule.whitelistArbitrationRelayer(address(arbitrationPolicySP), address(0), true); + } + + function test_DisputeModule_whitelistArbitrationRelayer() public { + vm.expectEmit(true, true, true, true, address(disputeModule)); + emit ArbitrationRelayerWhitelistUpdated(address(arbitrationPolicySP), address(1), true); + + disputeModule.whitelistArbitrationRelayer(address(arbitrationPolicySP), address(1), true); + + assertEq(disputeModule.isWhitelistedArbitrationRelayer(address(arbitrationPolicySP), address(1)), true); + } + + function test_DisputeModule_PolicySP_raiseDispute_NotWhitelistedArbitrationPolicy() public { + vm.expectRevert(Errors.DisputeModule__NotWhitelistedArbitrationPolicy.selector); + disputeModule.raiseDispute(address(1), address(1), string("urlExample"), "PLAGIARISM", ""); + } + + function test_DisputeModule_PolicySP_raiseDispute_NotWhitelistedDisputeTag() public { + vm.expectRevert(Errors.DisputeModule__NotWhitelistedDisputeTag.selector); + disputeModule.raiseDispute( + address(1), address(arbitrationPolicySP), string("urlExample"), "NOT_WHITELISTED", "" + ); + } + + function test_DisputeModule_PolicySP_raiseDispute_ZeroLinkToDisputeEvidence() public { + vm.expectRevert(Errors.DisputeModule__ZeroLinkToDisputeEvidence.selector); + disputeModule.raiseDispute(address(1), address(arbitrationPolicySP), string(""), "PLAGIARISM", ""); + } + + function test_DisputeModule_PolicySP_raiseDispute() public { + vm.startPrank(ipAccount1); + + IERC20(USDC).approve(address(arbitrationPolicySP), ARBITRATION_PRICE); + + uint256 disputeIdBefore = disputeModule.disputeId(); + uint256 ipAccount1USDCBalanceBefore = IERC20(USDC).balanceOf(ipAccount1); + uint256 arbitrationPolicySPUSDCBalanceBefore = IERC20(USDC).balanceOf(address(arbitrationPolicySP)); + + vm.expectEmit(true, true, true, true, address(disputeModule)); + emit DisputeRaised( + disputeIdBefore + 1, + ipAccount2, + ipAccount1, + address(arbitrationPolicySP), + ShortStringOps.stringToBytes32("urlExample"), + bytes32("PLAGIARISM"), + "" + ); + + disputeModule.raiseDispute(ipAccount2, address(arbitrationPolicySP), string("urlExample"), "PLAGIARISM", ""); + + uint256 disputeIdAfter = disputeModule.disputeId(); + uint256 ipAccount1USDCBalanceAfter = IERC20(USDC).balanceOf(ipAccount1); + uint256 arbitrationPolicySPUSDCBalanceAfter = IERC20(USDC).balanceOf(address(arbitrationPolicySP)); + + ( + address targetIpId, + address disputeInitiator, + address arbitrationPolicy, + bytes32 linkToDisputeEvidence, + bytes32 targetTag, + bytes32 currentTag + ) = disputeModule.disputes(disputeIdAfter); + + assertEq(disputeIdAfter - disputeIdBefore, 1); + assertEq(ipAccount1USDCBalanceBefore - ipAccount1USDCBalanceAfter, ARBITRATION_PRICE); + assertEq(arbitrationPolicySPUSDCBalanceAfter - arbitrationPolicySPUSDCBalanceBefore, ARBITRATION_PRICE); + assertEq(targetIpId, ipAccount2); + assertEq(disputeInitiator, ipAccount1); + assertEq(arbitrationPolicy, address(arbitrationPolicySP)); + assertEq(linkToDisputeEvidence, ShortStringOps.stringToBytes32("urlExample")); + assertEq(targetTag, bytes32("PLAGIARISM")); + assertEq(currentTag, bytes32("IN_DISPUTE")); + } + + function test_DisputeModule_PolicySP_setDisputeJudgement_NotInDisputeState() public { + vm.expectRevert(Errors.DisputeModule__NotInDisputeState.selector); + disputeModule.setDisputeJudgement(1, true, ""); + } + + function test_DisputeModule_PolicySP_setDisputeJudgement_NotWhitelistedArbitrationRelayer() public { + // raise dispute + vm.startPrank(ipAccount1); + IERC20(USDC).approve(address(arbitrationPolicySP), ARBITRATION_PRICE); + disputeModule.raiseDispute(ipAccount2, address(arbitrationPolicySP), string("urlExample"), "PLAGIARISM", ""); + vm.stopPrank(); + + vm.expectRevert(Errors.DisputeModule__NotWhitelistedArbitrationRelayer.selector); + disputeModule.setDisputeJudgement(1, true, ""); + } + + function test_DisputeModule_PolicySP_setDisputeJudgement_True() public { + // raise dispute + vm.startPrank(ipAccount1); + IERC20(USDC).approve(address(arbitrationPolicySP), ARBITRATION_PRICE); + disputeModule.raiseDispute(ipAccount1, address(arbitrationPolicySP), string("urlExample"), "PLAGIARISM", ""); + vm.stopPrank(); + + // set dispute judgement + (,,,,, bytes32 currentTagBefore) = disputeModule.disputes(1); + uint256 ipAccount1USDCBalanceBefore = IERC20(USDC).balanceOf(ipAccount1); + uint256 arbitrationPolicySPUSDCBalanceBefore = IERC20(USDC).balanceOf(address(arbitrationPolicySP)); + + vm.expectEmit(true, true, true, true, address(disputeModule)); + emit DisputeJudgementSet(1, true, ""); + + vm.startPrank(arbitrationRelayer); + disputeModule.setDisputeJudgement(1, true, ""); + + (,,,,, bytes32 currentTagAfter) = disputeModule.disputes(1); + uint256 ipAccount1USDCBalanceAfter = IERC20(USDC).balanceOf(ipAccount1); + uint256 arbitrationPolicySPUSDCBalanceAfter = IERC20(USDC).balanceOf(address(arbitrationPolicySP)); + + assertEq(ipAccount1USDCBalanceAfter - ipAccount1USDCBalanceBefore, ARBITRATION_PRICE); + assertEq(arbitrationPolicySPUSDCBalanceBefore - arbitrationPolicySPUSDCBalanceAfter, ARBITRATION_PRICE); + assertEq(currentTagBefore, bytes32("IN_DISPUTE")); + assertEq(currentTagAfter, bytes32("PLAGIARISM")); + } + + function test_DisputeModule_PolicySP_setDisputeJudgement_False() public { + // raise dispute + vm.startPrank(ipAccount1); + IERC20(USDC).approve(address(arbitrationPolicySP), ARBITRATION_PRICE); + disputeModule.raiseDispute(ipAccount1, address(arbitrationPolicySP), string("urlExample"), "PLAGIARISM", ""); + vm.stopPrank(); + + // set dispute judgement + (,,,,, bytes32 currentTagBefore) = disputeModule.disputes(1); + uint256 ipAccount1USDCBalanceBefore = IERC20(USDC).balanceOf(ipAccount1); + uint256 arbitrationPolicySPUSDCBalanceBefore = IERC20(USDC).balanceOf(address(arbitrationPolicySP)); + + vm.expectEmit(true, true, true, true, address(disputeModule)); + emit DisputeJudgementSet(1, false, ""); + + vm.startPrank(arbitrationRelayer); + disputeModule.setDisputeJudgement(1, false, ""); + + (,,,,, bytes32 currentTagAfter) = disputeModule.disputes(1); + uint256 ipAccount1USDCBalanceAfter = IERC20(USDC).balanceOf(ipAccount1); + uint256 arbitrationPolicySPUSDCBalanceAfter = IERC20(USDC).balanceOf(address(arbitrationPolicySP)); + + assertEq(ipAccount1USDCBalanceAfter - ipAccount1USDCBalanceBefore, 0); + assertEq(arbitrationPolicySPUSDCBalanceBefore - arbitrationPolicySPUSDCBalanceAfter, 0); + assertEq(currentTagBefore, bytes32("IN_DISPUTE")); + assertEq(currentTagAfter, bytes32(0)); + } + + function test_DisputeModule_PolicySP_cancelDispute_NotDisputeInitiator() public { + // raise dispute + vm.startPrank(ipAccount1); + IERC20(USDC).approve(address(arbitrationPolicySP), ARBITRATION_PRICE); + disputeModule.raiseDispute(address(1), address(arbitrationPolicySP), string("urlExample"), "PLAGIARISM", ""); + vm.stopPrank(); + + vm.expectRevert(Errors.DisputeModule__NotDisputeInitiator.selector); + disputeModule.cancelDispute(1, ""); + } + + function test_DisputeModule_PolicySP_cancelDispute_NotInDisputeState() public { + vm.expectRevert(Errors.DisputeModule__NotInDisputeState.selector); + disputeModule.cancelDispute(1, ""); + } + + function test_DisputeModule_PolicySP_cancelDispute() public { + // raise dispute + vm.startPrank(ipAccount1); + IERC20(USDC).approve(address(arbitrationPolicySP), ARBITRATION_PRICE); + disputeModule.raiseDispute(address(1), address(arbitrationPolicySP), string("urlExample"), "PLAGIARISM", ""); + vm.stopPrank(); + + (,,,,, bytes32 currentTagBeforeCancel) = disputeModule.disputes(1); + + vm.startPrank(ipAccount1); + vm.expectEmit(true, true, true, true, address(disputeModule)); + emit DisputeCancelled(1, ""); + + disputeModule.cancelDispute(1, ""); + vm.stopPrank(); + + (,,,,, bytes32 currentTagAfterCancel) = disputeModule.disputes(1); + + assertEq(currentTagBeforeCancel, bytes32("IN_DISPUTE")); + assertEq(currentTagAfterCancel, bytes32(0)); + } + + function test_DisputeModule_resolveDispute_NotDisputeInitiator() public { + vm.expectRevert(Errors.DisputeModule__NotDisputeInitiator.selector); + disputeModule.resolveDispute(1); + } + + function test_DisputeModule_resolveDispute_NotAbleToResolve() public { + // raise dispute + vm.startPrank(ipAccount1); + IERC20(USDC).approve(address(arbitrationPolicySP), ARBITRATION_PRICE); + disputeModule.raiseDispute(address(1), address(arbitrationPolicySP), string("urlExample"), "PLAGIARISM", ""); + vm.stopPrank(); + + vm.startPrank(ipAccount1); + vm.expectRevert(Errors.DisputeModule__NotAbleToResolve.selector); + disputeModule.resolveDispute(1); + } + + function test_DisputeModule_resolveDispute() public { + // raise dispute + vm.startPrank(ipAccount1); + IERC20(USDC).approve(address(arbitrationPolicySP), ARBITRATION_PRICE); + disputeModule.raiseDispute(address(1), address(arbitrationPolicySP), string("urlExample"), "PLAGIARISM", ""); + vm.stopPrank(); + + // set dispute judgement + vm.startPrank(arbitrationRelayer); + disputeModule.setDisputeJudgement(1, true, ""); + vm.stopPrank(); + + (,,,,, bytes32 currentTagBeforeResolve) = disputeModule.disputes(1); + + // resolve dispute + vm.startPrank(ipAccount1); + vm.expectEmit(true, true, true, true, address(disputeModule)); + emit DisputeResolved(1); + + disputeModule.resolveDispute(1); + + (,,,,, bytes32 currentTagAfterResolve) = disputeModule.disputes(1); + + assertEq(currentTagBeforeResolve, bytes32("PLAGIARISM")); + assertEq(currentTagAfterResolve, bytes32(0)); + } +} diff --git a/test/foundry/registries/metadata/IPAssetRenderer.t.sol b/test/foundry/registries/metadata/IPAssetRenderer.t.sol new file mode 100644 index 00000000..b9214d0e --- /dev/null +++ b/test/foundry/registries/metadata/IPAssetRenderer.t.sol @@ -0,0 +1,229 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; +import { Base64 } from "@openzeppelin/contracts/utils/Base64.sol"; + +import { ResolverBaseTest } from "test/foundry/resolvers/ResolverBase.t.sol"; +import { IPResolver } from "contracts/resolvers/IPResolver.sol"; +import { IResolver } from "contracts/interfaces/resolvers/IResolver.sol"; +import { AccessController } from "contracts/AccessController.sol"; +import { ModuleRegistry } from "contracts/registries/ModuleRegistry.sol"; +import { ERC6551Registry } from "lib/reference/src/ERC6551Registry.sol"; +import { IModuleRegistry } from "contracts/interfaces/registries/IModuleRegistry.sol"; +import { IPRecordRegistry } from "contracts/registries/IPRecordRegistry.sol"; +import { RegistrationModule } from "contracts/modules/RegistrationModule.sol"; +import { IPAccountRegistry } from "contracts/registries/IPAccountRegistry.sol"; +import { LicenseRegistry } from "contracts/registries/LicenseRegistry.sol"; +import { MockModuleRegistry } from "test/foundry/mocks/MockModuleRegistry.sol"; +import { IPMetadataProvider } from "contracts/registries/metadata/IPMetadataProvider.sol"; +import { RegistrationModule } from "contracts/modules/RegistrationModule.sol"; +import { IIPRecordRegistry } from "contracts/interfaces/registries/IIPRecordRegistry.sol"; +import { IPAssetRenderer } from "contracts/registries/metadata/IPAssetRenderer.sol"; +import { BaseTest } from "test/foundry/utils/BaseTest.sol"; +import { IPAccountImpl} from "contracts/IPAccountImpl.sol"; +import { MockERC721 } from "test/foundry/mocks/MockERC721.sol"; +import { ModuleBaseTest } from "test/foundry/modules/ModuleBase.t.sol"; +import { IP } from "contracts/lib/IP.sol"; +import { Errors } from "contracts/lib/Errors.sol"; +import { IP_RESOLVER_MODULE_KEY, REGISTRATION_MODULE_KEY } from "contracts/lib/modules/Module.sol"; + +/// @title IP Asset Renderer Test Contract +/// @notice Tests IP asset rendering functionality. +/// TODO: Make this inherit module base to avoid code duplication. +contract IPAssetRendererTest is BaseTest { + + // Module placeholders + // TODO: Mock these out. + address taggingModule = vm.addr(0x1111); + address royaltyModule = vm.addr(0x2222); + + /// @notice Gets the metadata provider used for IP registration. + IPMetadataProvider metadataProvider; + + /// @notice Used for registration in the IP record registry. + RegistrationModule public registrationModule; + + /// @notice Gets the protocol-wide license registry. + LicenseRegistry public licenseRegistry; + + /// @notice Gets the protocol-wide IP record registry. + IPRecordRegistry public ipRecordRegistry; + + /// @notice Gets the protocol-wide module registry. + ModuleRegistry public moduleRegistry; + + /// @notice Gets the protocol-wide IP account registry. + IPAccountRegistry public ipAccountRegistry; + + // Default IP record attributes. + string public constant IP_NAME = "IPRecord"; + string public constant IP_DESCRIPTION = "IPs all the way down."; + bytes32 public constant IP_HASH = ""; + string public constant IP_EXTERNAL_URL = "https://storyprotocol.xyz"; + uint64 public constant IP_REGISTRATION_DATE = uint64(99); + + /// @notice The access controller address. + AccessController public accessController; + + /// @notice The token contract SUT. + IPAssetRenderer public renderer; + + /// @notice The IP resolver. + IPResolver public resolver; + + /// @notice Mock IP identifier for resolver testing. + address public ipId; + + /// @notice Initializes the base token contract for testing. + function setUp() public virtual override(BaseTest) { + BaseTest.setUp(); + // TODO: Create an IP record registry mock instead. + licenseRegistry = new LicenseRegistry(""); + accessController = new AccessController(); + moduleRegistry = new ModuleRegistry(); + MockERC721 erc721 = new MockERC721(); + ipAccountRegistry = new IPAccountRegistry( + address(new ERC6551Registry()), + address(accessController), + address(new IPAccountImpl()) + ); + accessController.initialize(address(ipAccountRegistry), address(moduleRegistry)); + ipRecordRegistry = new IPRecordRegistry( + address(moduleRegistry), + address(ipAccountRegistry) + ); + + vm.prank(alice); + uint256 tokenId = erc721.mintId(alice, 99); + + metadataProvider = new IPMetadataProvider(address(moduleRegistry)); + resolver = new IPResolver( + address(accessController), + address(ipRecordRegistry), + address(ipAccountRegistry), + address(licenseRegistry) + ); + + // TODO: Mock out the registration module and module registry. + registrationModule = new RegistrationModule( + address(accessController), + address(ipRecordRegistry), + address(ipAccountRegistry), + address(licenseRegistry), + address(metadataProvider), + address(resolver) + ); + renderer = new IPAssetRenderer( + address(ipRecordRegistry), + address(licenseRegistry), + taggingModule, + royaltyModule + ); + moduleRegistry.registerModule(REGISTRATION_MODULE_KEY, address(registrationModule)); + vm.prank(address(registrationModule)); + ipId = ipRecordRegistry.register( + block.chainid, + address(erc721), + tokenId, + address(resolver), + true, + address(metadataProvider) + ); + + bytes memory metadata = abi.encode( + IP.Metadata({ + name: IP_NAME, + hash: IP_HASH, + registrationDate: IP_REGISTRATION_DATE, + registrant: alice, + uri: IP_EXTERNAL_URL + + }) + ); + vm.prank(address(registrationModule)); + metadataProvider.setMetadata(ipId, metadata); + } + + /// @notice Tests that the constructor works as expected. + function test_IPAssetRenderer_Constructor() public virtual { + assertEq(address(renderer.IP_RECORD_REGISTRY()), address(ipRecordRegistry)); + } + + /// @notice Tests that renderer can properly resolve names. + function test_IPAssetRenderer_Name() public virtual { + assertEq(renderer.name(ipId), IP_NAME); + } + + /// @notice Tests that the renderer can properly resolve descriptions. + function test_IPAssetRenderer_Description() public virtual { + assertEq( + renderer.description(ipId), + string.concat( + IP_NAME, + ", IP #", + Strings.toHexString(ipId), + ", is currently owned by", + Strings.toHexString(alice), + ". To learn more about this IP, visit ", + IP_EXTERNAL_URL + ) + ); + } + + /// @notice Tests that renderer can properly resolve hashes. + function test_IPAssetRenderer_Hash() public virtual { + assertEq(renderer.hash(ipId), IP_HASH); + } + + /// @notice Tests that renderer can properly resolve registration dates. + function test_IPAssetRenderer_RegistrationDate() public virtual { + assertEq(uint256(renderer.registrationDate(ipId)), uint256(IP_REGISTRATION_DATE)); + } + + /// @notice Tests that renderer can properly resolve registrants. + function test_IPAssetRenderer_Registrant() public virtual { + assertEq(renderer.registrant(ipId), alice); + } + + /// @notice Tests that renderer can properly resolve URLs. + function test_IPAssetRenderer_ExternalURL() public virtual { + assertEq(renderer.uri(ipId), IP_EXTERNAL_URL); + } + + /// @notice Tests that renderer can properly owners. + function test_IPAssetRenderer_Owner() public virtual { + assertEq(renderer.owner(ipId), alice); + } + + /// @notice Tests that the renderer can get the right token URI. + function test_IPAssetRenderer_TokenURI() public virtual { + string memory ownerStr = Strings.toHexString(uint160(address(alice))); + string memory description = string.concat( + IP_NAME, + ", IP #", + Strings.toHexString(ipId), + ", is currently owned by", + Strings.toHexString(alice), + ". To learn more about this IP, visit ", + IP_EXTERNAL_URL + ); + + string memory ipIdStr = Strings.toHexString(uint160(ipId)); + string memory uriEncoding = string(abi.encodePacked( + '{"name": "IP Asset #', ipIdStr, '", "description": "', description, '", "attributes": [', + '{"trait_type": "Name", "value": "IPRecord"},', + '{"trait_type": "Owner", "value": "', ownerStr, '"},' + '{"trait_type": "Registrant", "value": "', ownerStr, '"},', + '{"trait_type": "Hash", "value": "0x0000000000000000000000000000000000000000000000000000000000000000"},', + '{"trait_type": "Registration Date", "value": "', Strings.toString(IP_REGISTRATION_DATE), '"}', + ']}' + )); + string memory expectedURI = string(abi.encodePacked( + "data:application/json;base64,", + Base64.encode(bytes(string(abi.encodePacked(uriEncoding)))) + )); + assertEq(expectedURI, renderer.tokenURI(ipId)); + } + +} diff --git a/test/foundry/registries/metadata/IPMetadataProvider.t.sol b/test/foundry/registries/metadata/IPMetadataProvider.t.sol new file mode 100644 index 00000000..1cf9ed01 --- /dev/null +++ b/test/foundry/registries/metadata/IPMetadataProvider.t.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import { BaseTest } from "test/foundry/utils/BaseTest.sol"; +import { MockModuleRegistry } from "test/foundry/mocks/MockModuleRegistry.sol"; +import { IPMetadataProvider } from "contracts/registries/metadata/IPMetadataProvider.sol"; +import { IPRecordRegistry } from "contracts/registries/IPRecordRegistry.sol"; +import { Errors } from "contracts/lib/Errors.sol"; + +/// @title IP Metadata Provider Testing Contract +/// @notice Contract for metadata provider settings. +contract IPMetadataProviderTest is BaseTest { + + /// @notice Test bytes metadata settings. + bytes public TEST_METADATA = bytes("DEADBEEF"); + + /// @notice Placeholder for module registry. + MockModuleRegistry public registry; + + /// @notice Placeholder or registration module. + address public registrationModule = vm.addr(0x69); + + /// @notice Placeholder for an IP id. + address public ipId = vm.addr(0x1234); + + /// @notice The IP metadata provider SUT. + IPMetadataProvider public metadataProvider; + + /// @notice Initializes the IP metadata provider contract. + function setUp() public virtual override { + BaseTest.setUp(); + registry = new MockModuleRegistry(registrationModule); + metadataProvider = new IPMetadataProvider(address(registry)); + } + + /// @notice Tests IP metadata provider initialization. + function test_IPMetadataProvider_Constructor() public { + assertEq(address(metadataProvider.MODULE_REGISTRY()), address(registry)); + } + + /// @notice Tests metadata is properly stored. + function test_IPMetadataProvider_Metadata() public { + vm.prank(registrationModule); + metadataProvider.setMetadata(ipId, TEST_METADATA); + bytes memory expectedMetadata = bytes("DEADBEEF"); + assertEq( + metadataProvider.getMetadata(ipId), + expectedMetadata + ); + } + + /// @notice Checks that metadata setting reverts if not called by the registry. + function test_IPMetadataProvider_SetMetadata_Reverts_Unauthorized() public { + vm.expectRevert(Errors.MetadataProvider_Unauthorized.selector); + metadataProvider.setMetadata(ipId, TEST_METADATA); + } +} diff --git a/test/foundry/resolvers/IPMetadataResolver.t.sol b/test/foundry/resolvers/IPMetadataResolver.t.sol deleted file mode 100644 index dc7168ee..00000000 --- a/test/foundry/resolvers/IPMetadataResolver.t.sol +++ /dev/null @@ -1,243 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.23; - -import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; -import { Base64 } from "@openzeppelin/contracts/utils/Base64.sol"; - -import { ResolverBaseTest } from "test/foundry/resolvers/ResolverBase.t.sol"; -import { IPMetadataResolver } from "contracts/resolvers/IPMetadataResolver.sol"; -import { IResolver } from "contracts/interfaces/resolvers/IResolver.sol"; -import { ERC6551Registry } from "lib/reference/src/ERC6551Registry.sol"; -import { IModuleRegistry } from "contracts/interfaces/registries/IModuleRegistry.sol"; -import { IPRecordRegistry } from "contracts/registries/IPRecordRegistry.sol"; -import { RegistrationModule } from "contracts/modules/RegistrationModule.sol"; -import { IPAccountRegistry } from "contracts/registries/IPAccountRegistry.sol"; -import { MockModuleRegistry } from "test/foundry/mocks/MockModuleRegistry.sol"; -import { IIPRecordRegistry } from "contracts/interfaces/registries/IIPRecordRegistry.sol"; -import { IPAccountImpl} from "contracts/IPAccountImpl.sol"; -import { IIPMetadataResolver } from "contracts/interfaces/resolvers/IIPMetadataResolver.sol"; -import { MockERC721 } from "test/foundry/mocks/MockERC721.sol"; -import { ModuleBaseTest } from "test/foundry/modules/ModuleBase.t.sol"; -import { IP } from "contracts/lib/IP.sol"; -import { Errors } from "contracts/lib/Errors.sol"; -import { METADATA_RESOLVER_MODULE_KEY, REGISTRATION_MODULE_KEY } from "contracts/lib/modules/Module.sol"; - -/// @title IP Metadata Resolver Test Contract -/// @notice Tests IP metadata resolver functionality. -contract IPMetadataResolverTest is ResolverBaseTest { - - // Default IP record attributes. - string public constant RECORD_NAME = "IPRecord"; - string public constant RECORD_DESCRIPTION = "IPs all the way down."; - bytes32 public constant RECORD_HASH = ""; - uint64 public constant RECORD_REGISTRATION_DATE = 999999; - string public constant RECORD_URI = "https://storyprotocol.xyz"; - - /// @notice The registration module. - address public registrationModule; - - /// @notice The token contract SUT. - IIPMetadataResolver public ipResolver; - - /// @notice Mock IP identifier for resolver testing. - address public ipId; - - /// @notice Initializes the base token contract for testing. - function setUp() public virtual override(ResolverBaseTest) { - ResolverBaseTest.setUp(); - MockERC721 erc721 = new MockERC721(); - vm.prank(alice); - ipResolver = IIPMetadataResolver(_deployModule()); - uint256 tokenId = erc721.mintId(alice, 99); - // TODO: Mock this correctly - registrationModule = address(new RegistrationModule( - address(accessController), - address(ipRecordRegistry), - address(ipAccountRegistry), - address(licenseRegistry), - address(ipResolver) - )); - moduleRegistry.registerModule(REGISTRATION_MODULE_KEY, registrationModule); - moduleRegistry.registerModule(METADATA_RESOLVER_MODULE_KEY, address(ipResolver)); - vm.prank(registrationModule); - ipId = ipRecordRegistry.register( - block.chainid, - address(erc721), - tokenId, - address(ipResolver), - true - ); - } - - /// @notice Tests that the IP resolver interface is supported. - function test_IPMetadataResolver_SupportsInterface() public virtual { - assertTrue(ipResolver.supportsInterface(type(IIPMetadataResolver).interfaceId)); - } - - /// @notice Tests that metadata may be properly set for the resolver. - function test_IPMetadataResolver_SetMetadata() public { - vm.prank(ipId); - accessController.setPermission(ipId, alice, address(ipResolver), IIPMetadataResolver.setMetadata.selector, 1); - vm.prank(alice); - ipResolver.setMetadata( - ipId, - IP.MetadataRecord({ - name: RECORD_NAME, - description: RECORD_DESCRIPTION, - hash: RECORD_HASH, - registrationDate: RECORD_REGISTRATION_DATE, - registrant: alice, - uri: RECORD_URI - }) - ); - assertEq(ipResolver.name(ipId), RECORD_NAME); - assertEq(ipResolver.description(ipId), RECORD_DESCRIPTION); - assertEq(ipResolver.hash(ipId), RECORD_HASH); - assertEq(ipResolver.registrationDate(ipId), RECORD_REGISTRATION_DATE); - assertEq(ipResolver.registrant(ipId), alice); - assertEq(ipResolver.owner(ipId), alice); - assertEq(ipResolver.uri(ipId), RECORD_URI); - - // Also check the metadata getter returns as expected. - IP.Metadata memory metadata = ipResolver.metadata(ipId); - assertEq(metadata.name, RECORD_NAME); - assertEq(metadata.description, RECORD_DESCRIPTION); - assertEq(metadata.hash, RECORD_HASH); - assertEq(metadata.registrationDate, RECORD_REGISTRATION_DATE); - assertEq(metadata.registrant, alice); - assertEq(metadata.uri, RECORD_URI); - assertEq(metadata.owner, alice); - } - - /// @notice Checks that an unauthorized call to setMetadata reverts. - function test_IPMetadataResolver_SetMetadata_Reverts_Unauthorized() public { - vm.expectRevert(Errors.Module_Unauthorized.selector); - ipResolver.setMetadata( - ipId, - IP.MetadataRecord({ - name: RECORD_NAME, - description: RECORD_DESCRIPTION, - hash: RECORD_HASH, - registrationDate: RECORD_REGISTRATION_DATE, - registrant: alice, - uri: RECORD_URI - }) - ); - } - - /// @notice Tests that the name may be properly set for the resolver. - function test_IPMetadataResolver_SetName() public { - vm.prank(ipId); - accessController.setPermission(ipId, alice, address(ipResolver), IIPMetadataResolver.setName.selector, 1); - vm.prank(alice); - ipResolver.setName(ipId, RECORD_NAME); - assertEq(RECORD_NAME, ipResolver.name(ipId)); - } - - /// @notice Checks that an unauthorized call to setName reverts. - function test_IPMetadataResolver_SetName_Reverts_Unauthorized() public { - vm.expectRevert(Errors.Module_Unauthorized.selector); - ipResolver.setName(ipId, RECORD_NAME); - } - - /// @notice Tests that the description may be properly set for the resolver. - function test_IPMetadataResolver_SetDescriptionx() public { - vm.prank(ipId); - accessController.setPermission(ipId, alice, address(ipResolver), IIPMetadataResolver.setDescription.selector, 1); - vm.prank(alice); - ipResolver.setDescription(ipId, RECORD_DESCRIPTION); - assertEq(RECORD_DESCRIPTION, ipResolver.description(ipId)); - } - - /// @notice Checks that an unauthorized call to setDescription reverts. - function test_IPMetadataResolver_SetDescription_Reverts_Unauthorized() public { - vm.expectRevert(Errors.Module_Unauthorized.selector); - ipResolver.setDescription(ipId, RECORD_DESCRIPTION); - } - - /// @notice Tests that the hash may be properly set for the resolver. - function test_IPMetadataResolver_SetHash() public { - vm.prank(ipId); - accessController.setPermission(ipId, alice, address(ipResolver), IIPMetadataResolver.setHash.selector, 1); - vm.prank(alice); - ipResolver.setHash(ipId, RECORD_HASH); - assertEq(RECORD_HASH, ipResolver.hash(ipId)); - } - - /// @notice Checks that an unauthorized call to setHash reverts. - function test_IPMetadataResolver_SetHash_Reverts_Unauthorized() public { - vm.expectRevert(Errors.Module_Unauthorized.selector); - ipResolver.setHash(ipId, RECORD_HASH); - } - - /// @notice Checks that owner queries return the zero address if there is no - /// IP account attached to the IP or if it was not registered. - function test_IPMetadataResolver_Owner_NonExistent() public { - // TODO: Make more granular testing for the above two conditions. - assertEq(address(0), ipResolver.owner(address(0))); - } - - /// @notice Checks setting token URI works as expected. - function test_IPMetadataResolver_SetTokenURI() public { - vm.prank(ipId); - accessController.setPermission(ipId, alice, address(ipResolver), IIPMetadataResolver.setURI.selector, 1); - vm.prank(alice); - ipResolver.setURI(ipId, RECORD_URI); - assertEq(ipResolver.uri(ipId), RECORD_URI); - } - - /// @notice Checks the default token URI renders as expected. - function test_IPMetadataResolver_TokenURI_DefaultRender() public { - // Check default empty string value for unregistered IP. - assertEq(ipResolver.uri(address(0)), ""); - - // Check default string value for registered IP. - assertTrue(accessController.checkPermission(ipId, alice, address(ipResolver), IIPMetadataResolver.setMetadata.selector)); - vm.prank(alice); - ipResolver.setMetadata( - ipId, - IP.MetadataRecord({ - name: RECORD_NAME, - description: RECORD_DESCRIPTION, - hash: RECORD_HASH, - registrationDate: RECORD_REGISTRATION_DATE, - registrant: alice, - uri: "" // Blank indicates the default record should be used. - }) - ); - string memory ownerStr = Strings.toHexString(uint160(address(alice))); - string memory ipIdStr = Strings.toHexString(uint160(ipId)); - string memory uriEncoding = string(abi.encodePacked( - '{"name": "IP Asset #', ipIdStr, '", "description": "IPs all the way down.", "attributes": [', - '{"trait_type": "Name", "value": "IPRecord"},', - '{"trait_type": "Owner", "value": "', ownerStr, '"},' - '{"trait_type": "Registrant", "value": "', ownerStr, '"},', - '{"trait_type": "Hash", "value": "0x0000000000000000000000000000000000000000000000000000000000000000"},', - '{"trait_type": "Registration Date", "value": "', Strings.toString(RECORD_REGISTRATION_DATE), '"}', - ']}' - )); - string memory expectedURI = string(abi.encodePacked( - "data:application/json;base64,", - Base64.encode(bytes(string(abi.encodePacked(uriEncoding)))) - )); - assertEq(expectedURI, ipResolver.uri(ipId)); - } - - /// @dev Gets the expected name for the module. - function _expectedName() internal virtual view override returns (string memory) { - return "METADATA_RESOLVER_MODULE"; - } - - /// @dev Deploys a new IP Metadata Resolver. - function _deployModule() internal override returns (address) { - return address( - new IPMetadataResolver( - address(accessController), - address(ipRecordRegistry), - address(ipAccountRegistry), - address(licenseRegistry) - ) - ); - } - -} diff --git a/test/foundry/resolvers/IPResolver.t.sol b/test/foundry/resolvers/IPResolver.t.sol new file mode 100644 index 00000000..cdeab148 --- /dev/null +++ b/test/foundry/resolvers/IPResolver.t.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import { ResolverBaseTest } from "test/foundry/resolvers/ResolverBase.t.sol"; +import { IPResolver } from "contracts/resolvers/IPResolver.sol"; +import { KeyValueResolver } from "contracts/resolvers/KeyValueResolver.sol"; +import { IKeyValueResolver } from "contracts/interfaces/resolvers/IKeyValueResolver.sol"; +import { IPMetadataProvider } from "contracts/registries/metadata/IPMetadataProvider.sol"; +import { IResolver } from "contracts/interfaces/resolvers/IResolver.sol"; +import { ERC6551Registry } from "lib/reference/src/ERC6551Registry.sol"; +import { IModuleRegistry } from "contracts/interfaces/registries/IModuleRegistry.sol"; +import { IPRecordRegistry } from "contracts/registries/IPRecordRegistry.sol"; +import { RegistrationModule } from "contracts/modules/RegistrationModule.sol"; +import { IPAccountRegistry } from "contracts/registries/IPAccountRegistry.sol"; +import { MockModuleRegistry } from "test/foundry/mocks/MockModuleRegistry.sol"; +import { IIPRecordRegistry } from "contracts/interfaces/registries/IIPRecordRegistry.sol"; +import { IPAccountImpl} from "contracts/IPAccountImpl.sol"; +import { MockERC721 } from "test/foundry/mocks/MockERC721.sol"; +import { ModuleBaseTest } from "test/foundry/modules/ModuleBase.t.sol"; +import { IP } from "contracts/lib/IP.sol"; +import { Errors } from "contracts/lib/Errors.sol"; +import { IP_RESOLVER_MODULE_KEY, REGISTRATION_MODULE_KEY } from "contracts/lib/modules/Module.sol"; + +/// @title IP Resolver Test Contract +/// @notice Tests IP metadata resolver functionality. +contract IPResolverTest is ResolverBaseTest { + + // Test record attributes. + string public constant TEST_KEY = "Key"; + string public constant TEST_VALUE = "Value"; + + /// @notice The registration module. + address public registrationModule; + + /// @notice The token contract SUT. + IPResolver public ipResolver; + + /// @notice Mock IP identifier for resolver testing. + address public ipId; + + /// @notice Initializes the base token contract for testing. + function setUp() public virtual override(ResolverBaseTest) { + ResolverBaseTest.setUp(); + MockERC721 erc721 = new MockERC721(); + vm.prank(alice); + ipResolver = IPResolver(_deployModule()); + IPMetadataProvider metadataProvider = new IPMetadataProvider(address(moduleRegistry)); + uint256 tokenId = erc721.mintId(alice, 99); + // TODO: Mock this correctly + registrationModule = address(new RegistrationModule( + address(accessController), + address(ipRecordRegistry), + address(ipAccountRegistry), + address(licenseRegistry), + address(ipResolver), + address(metadataProvider) + )); + moduleRegistry.registerModule(REGISTRATION_MODULE_KEY, registrationModule); + moduleRegistry.registerModule(IP_RESOLVER_MODULE_KEY, address(ipResolver)); + vm.prank(registrationModule); + ipId = ipRecordRegistry.register( + block.chainid, + address(erc721), + tokenId, + address(ipResolver), + true, + address(metadataProvider) + ); + } + + /// @notice Tests that the IP resolver interface is supported. + function test_IPMetadataResolver_SupportsInterface() public virtual { + assertTrue(ipResolver.supportsInterface(type(IKeyValueResolver).interfaceId)); + } + + /// @notice Tests that key-value pair string attribution may be properly set. + function test_IPMetadataResolver_SetValue() public { + vm.prank(ipId); + accessController.setPermission(ipId, alice, address(ipResolver), KeyValueResolver.setValue.selector, 1); + vm.prank(alice); + ipResolver.setValue( + ipId, + TEST_KEY, + TEST_VALUE + ); + assertEq(ipResolver.value(ipId, TEST_KEY), TEST_VALUE); + } + + /// @dev Gets the expected name for the module. + function _expectedName() internal virtual view override returns (string memory) { + return "IP_RESOLVER_MODULE"; + } + + /// @dev Deploys a new IP Metadata Resolver. + function _deployModule() internal override returns (address) { + return address( + new IPResolver( + address(accessController), + address(ipRecordRegistry), + address(ipAccountRegistry), + address(licenseRegistry) + ) + ); + } + +}