From 5f78dd7caa7e25d61650e7b669df7c7d9ad4e0cf Mon Sep 17 00:00:00 2001 From: Leeren Date: Mon, 22 Jan 2024 10:23:16 -0800 Subject: [PATCH] Merges latest ACL changes (#14) --- contracts/AccessController.sol | 7 +- contracts/interfaces/modules/base/IModule.sol | 3 + .../registries/IIPRecordRegistry.sol | 2 +- contracts/interfaces/resolvers/IResolver.sol | 9 +- contracts/lib/Errors.sol | 14 + contracts/lib/Licensing.sol | 5 +- contracts/lib/modules/Module.sol | 3 + contracts/modules/BaseModule.sol | 59 +++++ contracts/modules/RegistrationModule.sol | 121 +++++++++ contracts/registries/IPRecordRegistry.sol | 8 +- contracts/registries/LicenseRegistry.sol | 174 ++++++++----- contracts/resolvers/IPMetadataResolver.sol | 14 +- contracts/resolvers/ResolverBase.sol | 43 +--- test/foundry/AccessController.t.sol | 1 - test/foundry/LicenseRegistry.t.sol | 25 +- test/foundry/integration/Integration.t.sol | 14 +- test/foundry/modules/ModuleBase.t.sol | 76 ++++++ test/foundry/modules/RegistrationModule.t.sol | 243 ++++++++++++++++++ .../resolvers/IPMetadataResolver.t.sol | 64 +++-- test/foundry/resolvers/ResolverBase.t.sol | 24 +- 20 files changed, 725 insertions(+), 184 deletions(-) create mode 100644 contracts/modules/BaseModule.sol create mode 100644 contracts/modules/RegistrationModule.sol create mode 100644 test/foundry/modules/ModuleBase.t.sol create mode 100644 test/foundry/modules/RegistrationModule.t.sol diff --git a/contracts/AccessController.sol b/contracts/AccessController.sol index d0a017bd..6c3b8685 100644 --- a/contracts/AccessController.sol +++ b/contracts/AccessController.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 { IModuleRegistry } from "contracts/interfaces/registries/IModuleRegistry.sol"; import { IAccessController } from "contracts/interfaces/IAccessController.sol"; import { IIPAccountRegistry } from "contracts/interfaces/registries/IIPAccountRegistry.sol"; import { IModuleRegistry } from "contracts/interfaces/registries/IModuleRegistry.sol"; @@ -75,13 +76,13 @@ contract AccessController is IAccessController { if (!IIPAccountRegistry(IP_ACCOUNT_REGISTRY).isIpAccount(ipAccount_)) { revert Errors.AccessController__IPAccountIsNotValid(); } - if (ipAccount_ != msg.sender) { - revert Errors.AccessController__CallerIsNotIPAccount(); - } // permission must be one of ABSTAIN, ALLOW, DENY if (permission_ > 2) { revert Errors.AccessController__PermissionIsNotValid(); } + if (!IModuleRegistry(MODULE_REGISTRY).isRegistered(msg.sender) && ipAccount_ != msg.sender) { + revert Errors.AccessController__CallerIsNotIPAccount(); + } permissions[ipAccount_][signer_][to_][func_] = permission_; // TODO: emit event diff --git a/contracts/interfaces/modules/base/IModule.sol b/contracts/interfaces/modules/base/IModule.sol index 4028f56f..b14c9dd2 100644 --- a/contracts/interfaces/modules/base/IModule.sol +++ b/contracts/interfaces/modules/base/IModule.sol @@ -2,6 +2,9 @@ // See https://github.com/storyprotocol/protocol-contracts/blob/main/StoryProtocol-AlphaTestingAgreement-17942166.3.pdf pragma solidity ^0.8.23; +/// @notice Module Interface interface IModule { + + /// @notice Returns the string identifier associated with the module. function name() external returns (string memory); } diff --git a/contracts/interfaces/registries/IIPRecordRegistry.sol b/contracts/interfaces/registries/IIPRecordRegistry.sol index 5519dc51..b1844b3a 100644 --- a/contracts/interfaces/registries/IIPRecordRegistry.sol +++ b/contracts/interfaces/registries/IIPRecordRegistry.sol @@ -99,7 +99,7 @@ interface IIPRecordRegistry { uint256 tokenId, address resolverAddr, bool createAccount - ) external; + ) external returns (address); /// @notice Creates the IP account for the specified IP. /// @param chainId The chain identifier of where the IP resides. diff --git a/contracts/interfaces/resolvers/IResolver.sol b/contracts/interfaces/resolvers/IResolver.sol index ce031d47..fcf880be 100644 --- a/contracts/interfaces/resolvers/IResolver.sol +++ b/contracts/interfaces/resolvers/IResolver.sol @@ -1,12 +1,11 @@ // SPDX-License-Identifier: UNLICENSED // See https://github.com/storyprotocol/protocol-contracts/blob/main/StoryProtocol-AlphaTestingAgreement-17942166.3.pdf -pragma solidity ^0.8.21; +pragma solidity ^0.8.23; -/// @notice Resolver Interface -interface IResolver { +import { IModule } from "contracts/interfaces/modules/base/IModule.sol"; - /// @notice Gets the address of the access controller for the resolver. - function accessController() view external returns (address); +/// @notice Resolver Interface +interface IResolver is IModule { /// @notice Checks whether the resolver IP interface is supported. function supportsInterface(bytes4 id) view external returns (bool); diff --git a/contracts/lib/Errors.sol b/contracts/lib/Errors.sol index be68d14f..8e9e55ef 100644 --- a/contracts/lib/Errors.sol +++ b/contracts/lib/Errors.sol @@ -5,6 +5,13 @@ pragma solidity ^0.8.19; /// @title Errors Library /// @notice Library for all Story Protocol contract errors. library Errors { + //////////////////////////////////////////////////////////////////////////// + // Module // + //////////////////////////////////////////////////////////////////////////// + + /// @notice The caller is not allowed to call the provided module. + error Module_Unauthorized(); + //////////////////////////////////////////////////////////////////////////// // IPRecordRegistry // //////////////////////////////////////////////////////////////////////////// @@ -94,6 +101,13 @@ library Errors { error ModuleRegistry__NameDoesNotMatch(); error ModuleRegistry__ModuleNotRegistered(); + //////////////////////////////////////////////////////////////////////////// + // RegistrationModule // + //////////////////////////////////////////////////////////////////////////// + + /// @notice The caller is not the owner of the root IP NFT. + error RegistrationModule__InvalidOwner(); + //////////////////////////////////////////////////////////////////////////// // AccessController // //////////////////////////////////////////////////////////////////////////// diff --git a/contracts/lib/Licensing.sol b/contracts/lib/Licensing.sol index f978e6d7..eb9de5ff 100644 --- a/contracts/lib/Licensing.sol +++ b/contracts/lib/Licensing.sol @@ -4,7 +4,6 @@ pragma solidity ^0.8.20; import { IParamVerifier } from "../interfaces/licensing/IParamVerifier.sol"; library Licensing { - /// Identifies a license parameter (term) from a license framework struct Parameter { /// Contract that must check if the condition of the paremeter is set @@ -51,7 +50,7 @@ library Licensing { bytes[] linkParentParamDefaultValues; string licenseUrl; } - + /// A particular configuration of a Licensing Framework, setting (or not) values for the licensing /// terms (parameters) of the framework. /// The lengths of the param value arrays must correspond to the Parameter[] of the framework. @@ -66,7 +65,7 @@ library Licensing { bytes[] activationParamValues; /// If false, minted licenses will start activated and verification of activationParams will be skipped bool needsActivation; - /// Array with values for parameters verifying conditions to link a license to a parent. Empty bytes for index if + /// Array with values for params verifying conditions to link a license to a parent. Empty bytes for index if /// this policy wants to use the default value for the paremeter. bytes[] linkParentParamValues; } diff --git a/contracts/lib/modules/Module.sol b/contracts/lib/modules/Module.sol index 25bbf6c7..a36b2102 100644 --- a/contracts/lib/modules/Module.sol +++ b/contracts/lib/modules/Module.sol @@ -2,5 +2,8 @@ // See https://github.com/storyprotocol/protocol-contracts/blob/main/StoryProtocol-AlphaTestingAgreement-17942166.3.pdf pragma solidity ^0.8.21; +// String values for core protocol modules. +string constant METADATA_RESOLVER_MODULE_KEY = "METADATA_RESOLVER_MODULE"; + // String values for core protocol modules. string constant REGISTRATION_MODULE_KEY = "REGISTRATION_MODULE"; diff --git a/contracts/modules/BaseModule.sol b/contracts/modules/BaseModule.sol new file mode 100644 index 00000000..6dd68c28 --- /dev/null +++ b/contracts/modules/BaseModule.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: UNLICENSED +// See https://github.com/storyprotocol/protocol-contracts/blob/main/StoryProtocol-AlphaTestingAgreement-17942166.3.pdf +pragma solidity ^0.8.23; + +import { IModule } from "contracts/interfaces/modules/base/IModule.sol"; +import { IAccessController } from "contracts/interfaces/IAccessController.sol"; +import { IPRecordRegistry } from "contracts/registries/IPRecordRegistry.sol"; +import { IPAccountRegistry } from "contracts/registries/IPAccountRegistry.sol"; +import { LicenseRegistry } from "contracts/registries/LicenseRegistry.sol"; +import { IResolver } from "contracts/interfaces/resolvers/IResolver.sol"; +import { Errors } from "contracts/lib/Errors.sol"; + +/// @title BaseModule +/// @notice Base implementation for all modules in Story Protocol. This is to +/// ensure all modules share the same authorization through the access +/// controll manager. +abstract contract BaseModule is IModule { + /// @notice Gets the protocol-wide module access controller. + IAccessController public immutable ACCESS_CONTROLLER; + + /// @notice Gets the protocol-wide IP account registry. + IPAccountRegistry public immutable IP_ACCOUNT_REGISTRY; + + /// @notice Gets the protocol-wide IP record registry. + IPRecordRegistry public immutable IP_RECORD_REGISTRY; + + /// @notice Gets the protocol-wide license registry. + LicenseRegistry public immutable LICENSE_REGISTRY; + + /// @notice Modifier for authorizing the calling entity. + modifier onlyAuthorized(address ipId) { + _authenticate(ipId); + _; + } + + /// @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) { + // TODO: Add checks for interface support or at least zero address + ACCESS_CONTROLLER = IAccessController(controller); + IP_RECORD_REGISTRY = IPRecordRegistry(recordRegistry); + IP_ACCOUNT_REGISTRY = IPAccountRegistry(accountRegistry); + LICENSE_REGISTRY = LicenseRegistry(licenseRegistry); + } + + /// @notice Gets the protocol string identifier associated with the module. + /// @return The string identifier of the module. + function name() public pure virtual override returns (string memory); + + /// @notice Authenticates the caller entity through the access controller. + function _authenticate(address ipId) internal view { + if (!ACCESS_CONTROLLER.checkPermission(ipId, msg.sender, address(this), msg.sig)) { + revert Errors.Module_Unauthorized(); + } + } +} diff --git a/contracts/modules/RegistrationModule.sol b/contracts/modules/RegistrationModule.sol new file mode 100644 index 00000000..39984a96 --- /dev/null +++ b/contracts/modules/RegistrationModule.sol @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: UNLICENSED +// See https://github.com/storyprotocol/protocol-contracts/blob/main/StoryProtocol-AlphaTestingAgreement-17942166.3.pdf +pragma solidity ^0.8.23; + +import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; + +import { BaseModule } from "contracts/modules/BaseModule.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"; + +/// @title Registration Module +/// @notice The registration module is responsible for registration of IP into +/// the protocol. During registration, this module will register an IP +/// into the protocol, create a resolver, and bind to it any licenses +/// and terms specified by the IP registrant (IP account owner). +contract RegistrationModule is BaseModule { + /// @notice The metadata resolver used by the registration module. + IIPMetadataResolver public resolver; + + /// @notice Initializes the registration 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. + /// @param resolverAddr The address of the IP metadata resolver. + constructor( + address controller, + address recordRegistry, + address accountRegistry, + address licenseRegistry, + address resolverAddr + ) BaseModule(controller, recordRegistry, accountRegistry, licenseRegistry) { + resolver = IIPMetadataResolver(resolverAddr); + } + + /// @notice Registers a root-level IP into the protocol. Root-level IPs can + /// be thought of as organizational hubs for encapsulating policies + /// that actual IPs can use to register through. As such, a + /// root-level IP is not an actual IP, but a container for IP policy + /// management for their child IP assets. + /// TODO: Rethink the semantics behind "root-level IPs" vs. "normal IPs". + /// TODO: Update function parameters to utilize a struct instead. + /// TODO: Revisit requiring binding an existing NFT to a "root-level IP". + /// If root-level IPs are an organizational primitive, why require NFTs? + /// TODO: Change to a different resolver optimized for root IP metadata. + /// @param policyId The policy that identifies the licensing terms of the IP. + /// @param tokenContract The address of the NFT bound to the root-level IP. + /// @param tokenId The token id of the NFT bound to the root-level IP. + function registerRootIp(uint256 policyId, address tokenContract, uint256 tokenId) external returns (address) { + // Perform registrant authorization. + // Check that the caller is authorized to perform the registration. + // TODO: Perform additional registration authorization logic, allowing + // registrants or root-IP creators to specify their own auth logic. + if (IERC721(tokenContract).ownerOf(tokenId) != msg.sender) { + 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); + + // Perform core IP policy creation. + if (policyId != 0) { + // If we know the policy ID, we can register it directly on creation. + // TODO: return policy index + LICENSE_REGISTRY.addPolicyToIp(ipId, policyId); + } + + return ipId; + } + + /// @notice Registers an IP derivative into the protocol. + /// @param licenseId The license to incorporate for the new IP. + /// @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 hash The content hash of the IP being registered. + function registerDerivativeIp( + uint256 licenseId, + address tokenContract, + uint256 tokenId, + string memory ipName, + string memory ipDescription, + bytes32 hash + ) external { + // Check that the caller is authorized to perform the registration. + // TODO: Perform additional registration authorization logic, allowing + // registrants or IP creators to specify their own auth logic. + if (IERC721(tokenContract).ownerOf(tokenId) != msg.sender) { + 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(resolver), IIPMetadataResolver.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({ + name: ipName, + description: ipDescription, + hash: hash, + registrationDate: uint64(block.timestamp), + registrant: msg.sender, + uri: "" + }) + ); + } + + /// @notice Gets the protocol-wide module identifier for this module. + function name() public pure override returns (string memory) { + return REGISTRATION_MODULE_KEY; + } +} diff --git a/contracts/registries/IPRecordRegistry.sol b/contracts/registries/IPRecordRegistry.sol index 4ca03dda..599161aa 100644 --- a/contracts/registries/IPRecordRegistry.sol +++ b/contracts/registries/IPRecordRegistry.sol @@ -31,7 +31,7 @@ contract IPRecordRegistry is IIPRecordRegistry { /// @dev Maps an IP, identified by its IP ID, to a metadata resolver. mapping(address => address) internal _resolvers; - /// @notice Restricts calls to only originate from the registration module. + /// @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.IPRecordRegistry_Unauthorized(); @@ -43,8 +43,8 @@ contract IPRecordRegistry is IIPRecordRegistry { /// @param moduleRegistry The address of the protocol module registry. /// @param ipAccountRegistry The address of the IP account registry. constructor(address moduleRegistry, address ipAccountRegistry) { - IP_ACCOUNT_REGISTRY = IIPAccountRegistry(ipAccountRegistry); MODULE_REGISTRY = IModuleRegistry(moduleRegistry); + IP_ACCOUNT_REGISTRY = IIPAccountRegistry(ipAccountRegistry); } /// @notice Gets the canonical IP identifier associated with an IP NFT. @@ -102,7 +102,7 @@ contract IPRecordRegistry is IIPRecordRegistry { uint256 tokenId, address resolverAddr, bool createAccount - ) external onlyRegistrationModule { + ) external onlyRegistrationModule returns (address account) { address id = ipId(chainId, tokenContract, tokenId); if (_resolvers[id] != address(0)) { revert Errors.IPRecordRegistry_AlreadyRegistered(); @@ -110,7 +110,7 @@ contract IPRecordRegistry is IIPRecordRegistry { // This is to emphasize the semantic differences between utilizing the // IP account as an identifier versus as an account used for auth. - address account = id; + account = id; if (account.code.length == 0 && createAccount) { _createIPAccount(chainId, tokenContract, tokenId); diff --git a/contracts/registries/LicenseRegistry.sol b/contracts/registries/LicenseRegistry.sol index 7990fd18..7dc26c7b 100644 --- a/contracts/registries/LicenseRegistry.sol +++ b/contracts/registries/LicenseRegistry.sol @@ -9,10 +9,8 @@ import { ERC1155 } from "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; - // TODO: consider disabling operators/approvals on creation contract LicenseRegistry is ERC1155 { - using EnumerableSet for EnumerableSet.UintSet; using EnumerableSet for EnumerableSet.AddressSet; using Strings for *; @@ -27,10 +25,9 @@ contract LicenseRegistry is ERC1155 { mapping(address => EnumerableSet.UintSet) private _policiesPerIpId; mapping(address => EnumerableSet.AddressSet) private _ipIdParents; - mapping(bytes32 => uint256) private _hashedLicenses; mapping(uint256 => Licensing.License) private _licenses; - + /// This tracks the number of licenses registered in the protocol, it will not decrease when a license is burnt. uint256 private _totalLicenses; @@ -48,25 +45,43 @@ contract LicenseRegistry is ERC1155 { /// Must be called by protocol admin /// @param fwCreation parameters /// @return frameworkId identifier for framework, starting in 1 - function addLicenseFramework(Licensing.FrameworkCreationParams calldata fwCreation) external returns(uint256 frameworkId) { + function addLicenseFramework( + Licensing.FrameworkCreationParams calldata fwCreation + ) external returns (uint256 frameworkId) { // check protocol auth if (bytes(fwCreation.licenseUrl).length == 0 || fwCreation.licenseUrl.equal("")) { - revert Errors.LicenseRegistry__EmptyLicenseUrl(); + revert Errors.LicenseRegistry__EmptyLicenseUrl(); } // Todo: check duplications ++_totalFrameworks; _frameworks[_totalFrameworks].licenseUrl = fwCreation.licenseUrl; _frameworks[_totalFrameworks].defaultNeedsActivation = fwCreation.defaultNeedsActivation; - _setParamArray(_frameworks[_totalFrameworks], Licensing.ParamVerifierType.Minting, fwCreation.mintingParamVerifiers, fwCreation.mintingParamDefaultValues); - _setParamArray(_frameworks[_totalFrameworks], Licensing.ParamVerifierType.Activate, fwCreation.activationParamVerifiers, fwCreation.activationParamDefaultValues); - _setParamArray(_frameworks[_totalFrameworks], Licensing.ParamVerifierType.LinkParent, fwCreation.linkParentParamVerifiers, fwCreation.linkParentParamDefaultValues); + _setParamArray( + _frameworks[_totalFrameworks], + Licensing.ParamVerifierType.Minting, + fwCreation.mintingParamVerifiers, + fwCreation.mintingParamDefaultValues + ); + _setParamArray( + _frameworks[_totalFrameworks], + Licensing.ParamVerifierType.Activate, + fwCreation.activationParamVerifiers, + fwCreation.activationParamDefaultValues + ); + _setParamArray( + _frameworks[_totalFrameworks], + Licensing.ParamVerifierType.LinkParent, + fwCreation.linkParentParamVerifiers, + fwCreation.linkParentParamDefaultValues + ); // Should we add a label? // TODO: emit return _totalFrameworks; } - /// Convenience method to convert IParamVerifier[] + bytes[] into Parameter[], then stores it in a Framework storage ref + /// Convenience method to convert IParamVerifier[] + bytes[] into Parameter[] + /// After conversion, it stores it in a Framework storage ref /// (Parameter[] can be in storage but not in calldata) /// @param fw storage ref to framework /// @param pvType ParamVerifierType, to know which parameters the arrays correspond to @@ -92,39 +107,41 @@ contract LicenseRegistry is ERC1155 { revert Errors.LicenseRegistry__InvalidParamVerifierType(); } for (uint256 i = 0; i < paramVerifiers.length; i++) { - params.push(Licensing.Parameter({ - verifier: paramVerifiers[i], - defaultValue: paramDefaultValues[i] - })); + params.push(Licensing.Parameter({ verifier: paramVerifiers[i], defaultValue: paramDefaultValues[i] })); } } /// Gets total frameworks supported by LicenseRegistry - function totalFrameworks() external view returns(uint256) { + function totalFrameworks() external view returns (uint256) { return _totalFrameworks; } /// Returns framework for id. Reverts if not found - function framework(uint256 frameworkId) public view returns(Licensing.Framework memory fw) { + function framework(uint256 frameworkId) public view returns (Licensing.Framework memory fw) { fw = _frameworks[frameworkId]; if (bytes(fw.licenseUrl).length == 0) { - revert Errors.LicenseRegistry__FrameworkNotFound(); + revert Errors.LicenseRegistry__FrameworkNotFound(); } return fw; } - /// Convenience method to store data without repetition, assigning an id to it if new or reusing the existing one if already stored + /// Stores data without repetition, assigning an id to it if new or reusing existing one if already stored /// @param data raw bytes, abi.encode() a value to be hashed /// @param _hashToIds storage ref to the mapping of hash -> data id /// @param existingIds amount of distinct data stored. /// @return id new sequential id if new data, reused id if not new - /// @return isNew true if a new id was generated, signaling the value was stored in _hashToIds. False if id is reused and data was not stored - function _addIdOrGetExisting(bytes memory data, mapping(bytes32 => uint256) storage _hashToIds, uint256 existingIds) private returns(uint256 id, bool isNew) { + /// @return isNew True if a new id was generated, signaling the value was stored in _hashToIds. + /// False if id is reused and data was not stored + function _addIdOrGetExisting( + bytes memory data, + mapping(bytes32 => uint256) storage _hashToIds, + uint256 existingIds + ) private returns (uint256 id, bool isNew) { // We could just use the hash of the policy as id to save some gas, but the UX/DX of having huge random // numbers for ID is bad enough to justify the cost, plus we have accountability on current number of // policies. bytes32 hash = keccak256(data); - uint256 id = _hashToIds[hash]; + id = _hashToIds[hash]; if (id != 0) { return (id, false); } @@ -132,26 +149,47 @@ contract LicenseRegistry is ERC1155 { _hashToIds[hash] = id; return (id, true); } - - /// Adds a policy to an ipId, which can be used to mint licenses, which are permissions for ipIds to be derivatives (children). + + /// Adds a policy to an ipId, which can be used to mint licenses. + /// Licenses are permissions for ipIds to be derivatives (children). /// If an exact policy already existed, it will reuse the id. /// Will revert if ipId already has the same policy /// @param ipId to receive the policy /// @param pol policy data /// @return policyId if policy data was in the contract, policyId is reused, if it's new, id will be new. + /// @return isNew true if policy data was not in the contract, false if it was already stored /// @return indexOnIpId position of policy within the ipIds policy set - function addPolicy(address ipId, Licensing.Policy memory pol) public returns(uint256 policyId, uint256 indexOnIpId) { + function addPolicyToIp( + address ipId, + Licensing.Policy memory pol + ) public returns (uint256 policyId, bool isNew, uint256 indexOnIpId) { // check protocol auth - Licensing.Framework memory fw = framework(pol.frameworkId); - // TODO: check if policy is compatible with existing or is allowed to add more - (uint256 polId, bool isNew) = _addIdOrGetExisting(abi.encode(pol), _hashedPolicies, _totalPolicies); - policyId = polId; - if (isNew) { + (uint256 polId, bool newPolicy) = addPolicy(pol); + return (polId, newPolicy, _addPolictyIdToIp(ipId, polId)); + } + + /// Adds a policy to an ipId, which can be used to mint licenses. + /// Licnses are permissions for ipIds to be derivatives (children). + /// if policyId is not defined in LicenseRegistry, reverts. + /// Will revert if ipId already has the same policy + /// @param ipId to receive the policy + /// @param polId id of the policy data + /// @return indexOnIpId position of policy within the ipIds policy set + function addPolicyToIp(address ipId, uint256 polId) external returns (uint256 indexOnIpId) { + if (!isPolicyDefined(polId)) { + revert Errors.LicenseRegistry__PolicyNotFound(); + } + return _addPolictyIdToIp(ipId, polId); + } + + function addPolicy(Licensing.Policy memory pol) public returns (uint256 policyId, bool isNew) { + (uint256 polId, bool newPol) = _addIdOrGetExisting(abi.encode(pol), _hashedPolicies, _totalPolicies); + if (newPol) { _totalPolicies = polId; _policies[polId] = pol; // TODO: emit } - return (policyId, _addPolictyId(ipId, policyId)); + return (polId, newPol); } /// Adds a policy id to the ipId policy set @@ -159,7 +197,7 @@ contract LicenseRegistry is ERC1155 { /// @param ipId the IP identifier /// @param policyId id of the policy data /// @return index of the policy added to the set - function _addPolictyId(address ipId, uint256 policyId) internal returns(uint256 index) { + function _addPolictyIdToIp(address ipId, uint256 policyId) internal returns (uint256 index) { EnumerableSet.UintSet storage policySet = _policiesPerIpId[ipId]; // TODO: check if policy is compatible with the others if (!policySet.add(policyId)) { @@ -170,12 +208,12 @@ contract LicenseRegistry is ERC1155 { } /// Returns amount of distinct licensing policies in LicenseRegistry - function totalPolicies() external view returns(uint256) { + function totalPolicies() external view returns (uint256) { return _totalPolicies; } /// Gets policy data for policyId, reverts if not found - function policy(uint256 policyId) public view returns(Licensing.Policy memory pol) { + function policy(uint256 policyId) public view returns (Licensing.Policy memory pol) { pol = _policies[policyId]; if (pol.frameworkId == 0) { revert Errors.LicenseRegistry__PolicyNotFound(); @@ -183,31 +221,37 @@ contract LicenseRegistry is ERC1155 { return pol; } + /// Returns true if policyId is defined in LicenseRegistry, false otherwise. + function isPolicyDefined(uint256 policyId) public view returns (bool) { + return _policies[policyId].frameworkId != 0; + } + /// Gets the policy set for an IpId /// @dev potentially expensive operation, use with care - function policyIdsForIp(address ipId) external view returns(uint256[] memory policyIds) { + function policyIdsForIp(address ipId) external view returns (uint256[] memory policyIds) { return _policiesPerIpId[ipId].values(); } - function totalPoliciesForIp(address ipId) external view returns(uint256) { + function totalPoliciesForIp(address ipId) external view returns (uint256) { return _policiesPerIpId[ipId].length(); } - function isPolicyIdSetForIp(address ipId, uint256 policyId) external view returns(bool) { + function isPolicyIdSetForIp(address ipId, uint256 policyId) external view returns (bool) { return _policiesPerIpId[ipId].contains(policyId); } - function policyIdForIpAtIndex(address ipId, uint256 index) external view returns(uint256 policyId) { + function policyIdForIpAtIndex(address ipId, uint256 index) external view returns (uint256 policyId) { return _policiesPerIpId[ipId].at(index); } - function policyForIpAtIndex(address ipId, uint256 index) external view returns(Licensing.Policy memory) { + function policyForIpAtIndex(address ipId, uint256 index) external view returns (Licensing.Policy memory) { return _policies[_policiesPerIpId[ipId].at(index)]; } - /// Mints license NFTs representing a licensing policy granted by a set of ipIds (licensors). This NFT needs to be burned + /// Mints license NFTs representing a policy granted by a set of ipIds (licensors). This NFT needs to be burned /// in order to link a derivative IP with its parents. - /// If this is the first combination of policy and licensors, a new licenseId will be created (by incrementing prev totalLicenses). + /// If this is the first combination of policy and licensors, a new licenseId + /// will be created (by incrementing prev totalLicenses). /// If not, the license is fungible and an id will be reused. /// The licensing terms that regulate creating new licenses will be verified to allow minting. /// Reverts if caller is not authorized by licensors. @@ -215,24 +259,28 @@ contract LicenseRegistry is ERC1155 { /// @param amount of licenses to be minted. License NFT is fungible for same policy and same licensors /// @param receiver of the License NFT(s). /// @return licenseId of the NFT(s). - function mintLicense(Licensing.License calldata licenseData, uint256 amount, address receiver) external returns(uint256 licenseId) { + function mintLicense( + Licensing.License calldata licenseData, + uint256 amount, + address receiver + ) external returns (uint256 licenseId) { uint256 policyId = licenseData.policyId; - - for(uint256 i = 0; i < licenseData.licensorIpIds.length; i++) { + + for (uint256 i = 0; i < licenseData.licensorIpIds.length; i++) { address licensor = licenseData.licensorIpIds[i]; - if(!_policiesPerIpId[licensor].contains(policyId)) { + if (!_policiesPerIpId[licensor].contains(policyId)) { revert Errors.LicenseRegistry__LicensorDoesntHaveThisPolicy(); } // TODO: check duplicates // TODO: check if licensors are valid IP Ids and if they have been tagged bad // TODO: check if licensor allowed sender to mint in their behalf } - + Licensing.Policy memory pol = policy(policyId); Licensing.Parameter[] memory mintParams = _frameworks[pol.frameworkId].mintingParams; bytes[] memory mintParamValues = pol.mintingParamValues; - for (uint256 i=0; i < mintParams.length; i++) { + for (uint256 i = 0; i < mintParams.length; i++) { Licensing.Parameter memory param = mintParams[i]; // Empty bytes => use default value specified in license framework creation params. bytes memory data = mintParamValues[i].length == 0 ? param.defaultValue : mintParamValues[i]; @@ -254,37 +302,39 @@ contract LicenseRegistry is ERC1155 { } /// Returns true if holder has positive balance for licenseId - function isLicensee(uint256 licenseId, address holder) external view returns(bool) { + function isLicensee(uint256 licenseId, address holder) external view returns (bool) { return balanceOf(holder, licenseId) > 0; } /// Relates an IP ID with its parents (licensors), by burning the License NFT the holder owns /// License must be activated to succeed, reverts otherwise. /// Licensing parameters related to linking IPAs must be verified in order to succeed, reverts otherwise. - /// The child IP ID will have the policy that the license represent added to it's own, if it's compatible with + /// The child IP ID will have the policy that the license represent added to it's own, if it's compatible with /// existing child policies. /// The child IP ID will be linked to the parent (if it wasn't before). /// @param licenseId license NFT to be burned /// @param childIpId that will receive the policy defined by licenseId /// @param holder of the license NFT - function setParentPolicy(uint256 licenseId, address childIpId, address holder) - external - onlyLicensee(licenseId, holder) { + function linkIpToParent( + uint256 licenseId, + address childIpId, + address holder + ) external onlyLicensee(licenseId, holder) { // TODO: auth // TODO: check if license is activated // TODO: check if childIpId exists and is owned by holder Licensing.License memory licenseData = _licenses[licenseId]; address[] memory parents = licenseData.licensorIpIds; - for (uint256 i=0; i < parents.length; i++) { + for (uint256 i = 0; i < parents.length; i++) { // TODO: check licensor exist // TODO: check licensor part of a bad tag branch } - + Licensing.Policy memory pol = policy(licenseData.policyId); Licensing.Parameter[] memory linkParams = _frameworks[pol.frameworkId].linkParentParams; bytes[] memory linkParamValues = pol.linkParentParamValues; - for (uint256 i=0; i < linkParams.length; i++) { + for (uint256 i = 0; i < linkParams.length; i++) { Licensing.Parameter memory param = linkParams[i]; // Empty bytes => use default value specified in license framework creation params. bytes memory data = linkParamValues[i].length == 0 ? param.defaultValue : linkParamValues[i]; @@ -294,9 +344,10 @@ contract LicenseRegistry is ERC1155 { } // Add policy to kid - addPolicy(childIpId, pol); + // TODO: return this values + addPolicyToIp(childIpId, pol); // Set parent - for (uint256 i=0; i < parents.length; i++) { + for (uint256 i = 0; i < parents.length; i++) { // We don't care if it was already a parent, because there might be a case such as: // 1. IP2 is created from IP1 with L1(non commercial) // 2. IP1 releases L2 with commercial terms, and IP2 wants permission to commercially exploit @@ -308,26 +359,25 @@ contract LicenseRegistry is ERC1155 { _ipIdParents[childIpId].add(parent); // TODO: emit } - + // Burn license _burn(holder, licenseId, 1); } /// Returns true if the child is derivative from the parent, by at least 1 policy. - function isParent(address parentIpId, address childIpId) external view returns(bool) { + function isParent(address parentIpId, address childIpId) external view returns (bool) { return _ipIdParents[childIpId].contains(parentIpId); } - function parentIpIds(address ipId) external view returns(address[] memory) { + function parentIpIds(address ipId) external view returns (address[] memory) { return _ipIdParents[ipId].values(); } - function totalParentsForIpId(address ipId) external view returns(uint256) { + function totalParentsForIpId(address ipId) external view returns (uint256) { return _ipIdParents[ipId].length(); } // TODO: activation method // TODO: tokenUri from parameters, from a metadata resolver contract - -} \ No newline at end of file +} diff --git a/contracts/resolvers/IPMetadataResolver.sol b/contracts/resolvers/IPMetadataResolver.sol index c3090c03..72122751 100644 --- a/contracts/resolvers/IPMetadataResolver.sol +++ b/contracts/resolvers/IPMetadataResolver.sol @@ -6,10 +6,13 @@ 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 @@ -23,11 +26,13 @@ contract IPMetadataResolver is IIPMetadataResolver, ResolverBase { /// @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 - ) ResolverBase(accessController, ipRecordRegistry, ipAccountRegistry) {} + 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. @@ -145,6 +150,11 @@ contract IPMetadataResolver is IIPMetadataResolver, ResolverBase { 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. diff --git a/contracts/resolvers/ResolverBase.sol b/contracts/resolvers/ResolverBase.sol index 7fb8d919..99e5a605 100644 --- a/contracts/resolvers/ResolverBase.sol +++ b/contracts/resolvers/ResolverBase.sol @@ -1,48 +1,19 @@ // 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; -import { IAccessController } from "contracts/interfaces/IAccessController.sol"; -import { IPRecordRegistry } from "contracts/registries/IPRecordRegistry.sol"; -import { IPAccountRegistry } from "contracts/registries/IPAccountRegistry.sol"; +import { BaseModule } from "contracts/modules/BaseModule.sol"; import { IResolver } from "contracts/interfaces/resolvers/IResolver.sol"; -import { Errors } from "contracts/lib/Errors.sol"; /// @notice IP Resolver Base Contract -abstract contract ResolverBase is IResolver { - /// @notice Gets the protocol-wide module access controller. - IAccessController public immutable ACCESS_CONTROLLER; +abstract contract ResolverBase is IResolver, BaseModule { - /// @notice Gets the protocol-wide IP account registry. - IPAccountRegistry public immutable IP_ACCOUNT_REGISTRY; - - /// @notice Gets the protocol-wide IP record registry. - IPRecordRegistry public immutable IP_RECORD_REGISTRY; - - /// @notice Checks if IP identified by ipId is authorized to perform a call. - /// @param ipId The identifier for the IP being authorized. - modifier onlyAuthorized(address ipId) { - if (!ACCESS_CONTROLLER.checkPermission(ipId, msg.sender, address(this), msg.sig)) { - revert Errors.IPResolver_Unauthorized(); - } - _; - } - - /// @notice Initializes the base IP resolver contract. - /// @param controller The address of the module access controller. + /// @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. - constructor(address controller, address recordRegistry, address accountRegistry) { - ACCESS_CONTROLLER = IAccessController(controller); - IP_RECORD_REGISTRY = IPRecordRegistry(recordRegistry); - IP_ACCOUNT_REGISTRY = IPAccountRegistry(accountRegistry); - } - - /// @notice Gets the access controller responsible for resolver auth. - /// @return The address of the access controller. - function accessController() external view returns (address) { - return address(ACCESS_CONTROLLER); - } + /// @param licenseRegistry The address of the license registry. + 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/test/foundry/AccessController.t.sol b/test/foundry/AccessController.t.sol index 19ea3242..2b65e4cb 100644 --- a/test/foundry/AccessController.t.sol +++ b/test/foundry/AccessController.t.sol @@ -590,7 +590,6 @@ contract AccessControllerTest is Test { ); } - function test_AccessController_functionWildcardOverrideToAddressWildcard_allowOverrideDeny() public { moduleRegistry.registerModule("MockModule", address(mockModule)); address signer = vm.addr(2); diff --git a/test/foundry/LicenseRegistry.t.sol b/test/foundry/LicenseRegistry.t.sol index 87a0a11b..3fc96729 100644 --- a/test/foundry/LicenseRegistry.t.sol +++ b/test/foundry/LicenseRegistry.t.sol @@ -10,11 +10,7 @@ import { IParamVerifier } from "contracts/interfaces/licensing/IParamVerifier.so contract LicenseRegistryTest is Test { LicenseRegistry public registry; Licensing.Framework public framework; - enum VerifierType { - Minting, - Activate, - LinkParent - } + MockIParamVerifier public verifier; string public licenseUrl = "https://example.com/license"; Licensing.FrameworkCreationParams fwParams; @@ -28,7 +24,7 @@ contract LicenseRegistryTest is Test { registry.addLicenseFramework(fwParams); _; } - + // TODO: use ModuleBaseTest for this function _initFwParams() private { IParamVerifier[] memory mintingParamVerifiers = new IParamVerifier[](1); mintingParamVerifiers[0] = verifier; @@ -71,7 +67,7 @@ contract LicenseRegistryTest is Test { assertEq(registry.totalFrameworks(), 1, "total frameworks not updated"); } - function test_LicenseRegistry_addPolicyId() public withFrameworkParams { + function test_LicenseRegistry_addPolicyToIpId() public withFrameworkParams { Licensing.Policy memory policy = Licensing.Policy({ frameworkId: 1, mintingParamValues: new bytes[](1), @@ -82,7 +78,7 @@ contract LicenseRegistryTest is Test { policy.mintingParamValues[0] = abi.encode(true); policy.activationParamValues[0] = abi.encode(true); policy.linkParentParamValues[0] = abi.encode(true); - (uint256 policyId, uint256 indexOnIpId) = registry.addPolicy(ipId1, policy); + (uint256 policyId, bool isNew, uint256 indexOnIpId) = registry.addPolicyToIp(ipId1, policy); assertEq(policyId, 1, "policyId not 1"); assertEq(indexOnIpId, 0, "indexOnIpId not 0"); Licensing.Policy memory storedPolicy = registry.policy(policyId); @@ -100,8 +96,8 @@ contract LicenseRegistryTest is Test { policy.mintingParamValues[0] = abi.encode(true); policy.activationParamValues[0] = abi.encode(true); policy.linkParentParamValues[0] = abi.encode(true); - (uint256 policyId, uint256 indexOnIpId) = registry.addPolicy(ipId1, policy); - (uint256 policyId2, uint256 indexOnIpId2) = registry.addPolicy(ipId2, policy); + (uint256 policyId, bool isNew1, uint256 indexOnIpId) = registry.addPolicyToIp(ipId1, policy); + (uint256 policyId2, bool isNew2, uint256 indexOnIpId2) = registry.addPolicyToIp(ipId2, policy); assertEq(policyId, policyId2, "policyId not reused"); } @@ -122,7 +118,8 @@ contract LicenseRegistryTest is Test { policy.linkParentParamValues[0] = abi.encode(true); // First time adding a policy - (uint256 policyId, uint256 indexOnIpId) = registry.addPolicy(ipId1, policy); + (uint256 policyId, bool isNew, uint256 indexOnIpId) = registry.addPolicyToIp(ipId1, policy); + // TODO: test isNew assertEq(policyId, 1, "policyId not 1"); assertEq(indexOnIpId, 0, "indexOnIpId not 0"); assertEq(registry.totalPolicies(), 1, "totalPolicies not incremented"); @@ -131,7 +128,7 @@ contract LicenseRegistryTest is Test { // Adding different policy to same ipId policy.mintingParamValues[0] = abi.encode("test2"); - (uint256 policyId2, uint256 indexOnIpId2) = registry.addPolicy(ipId1, policy); + (uint256 policyId2, bool isNew2, uint256 indexOnIpId2) = registry.addPolicyToIp(ipId1, policy); assertEq(policyId2, 2, "policyId not 2"); assertEq(indexOnIpId2, 1, "indexOnIpId not 1"); assertEq(registry.totalPolicies(), 2, "totalPolicies not incremented"); @@ -151,7 +148,7 @@ contract LicenseRegistryTest is Test { policy.activationParamValues[0] = abi.encode(true); policy.linkParentParamValues[0] = abi.encode(true); - (uint256 policyId, uint256 indexOnIpId) = registry.addPolicy(ipId1, policy); + (uint256 policyId, bool isNew, uint256 indexOnIpId) = registry.addPolicyToIp(ipId1, policy); assertEq(policyId, 1); assertTrue(registry.isPolicyIdSetForIp(ipId1, policyId)); @@ -176,7 +173,7 @@ contract LicenseRegistryTest is Test { // TODO: something cleaner than this uint256 licenseId = test_LicenseRegistry_mintLicense(); - registry.setParentPolicy(licenseId, ipId2, licenseHolder); + registry.linkIpToParent(licenseId, ipId2, licenseHolder); assertEq(registry.balanceOf(licenseHolder, licenseId), 1, "not burnt"); assertEq(registry.isParent(ipId1, ipId2), true, "not parent"); assertEq( diff --git a/test/foundry/integration/Integration.t.sol b/test/foundry/integration/Integration.t.sol index a86d3ac2..4eb79df4 100644 --- a/test/foundry/integration/Integration.t.sol +++ b/test/foundry/integration/Integration.t.sol @@ -179,10 +179,9 @@ contract IntegrationTest is Test { function attachPolicyToIPID( address ipId, string memory policyName - ) public returns (uint256 policyId, uint256 indexOnIpId) { - (policyId, indexOnIpId) = licenseRegistry.addPolicy(ipId, policy[policyName]); + ) public returns (uint256 policyId, bool isNew, uint256 indexOnIpId) { + (policyId, isNew, indexOnIpId) = licenseRegistry.addPolicyToIp(ipId, policy[policyName]); policyIds[policyName][ipId] = policyId; - return (policyId, indexOnIpId); } function attachPolicyAndMintLicenseForIPID( @@ -191,7 +190,7 @@ contract IntegrationTest is Test { address licensee, uint256 amount ) public returns (uint256 licenseId) { - (uint256 policyId, uint256 indexOnIpId) = attachPolicyToIPID(ipId, policyName); + (uint256 policyId, bool isNew, uint256 indexOnIpId) = attachPolicyToIPID(ipId, policyName); Licensing.License memory licenseData = Licensing.License({ policyId: policyId, licensorIpIds: new address[](1) @@ -330,6 +329,9 @@ contract IntegrationTest is Test { linkParentParamValues: new bytes[](0) }); + licenseRegistry.addPolicy(policy["test_true"]); + licenseRegistry.addPolicy(policy["expensive_mint"]); + /*/////////////////////////////////////////////////////////////// ADD POLICIES TO IPACCOUNTS ////////////////////////////////////////////////////////////////*/ @@ -361,7 +363,7 @@ contract IntegrationTest is Test { // Carl activates above license on his NFT C IPAccount, linking as child to Alice's NFT A IPAccount vm.prank(u.carl); - licenseRegistry.setParentPolicy( + licenseRegistry.linkIpToParent( licenseIds[u.carl][policyIds["test_true"][getIpId(u.alice, nft.a, 1)]], getIpId(u.carl, nft.c, 1), u.carl @@ -414,7 +416,7 @@ contract IntegrationTest is Test { // Bob activates above license on his NFT A IPAccount, linking as child to Carl's NFT C IPAccount vm.prank(u.bob); - licenseRegistry.setParentPolicy( + licenseRegistry.linkIpToParent( licenseIds[u.bob][policyIds["expensive_mint"][getIpId(u.carl, nft.c, 1)]], getIpId(u.bob, nft.a, 2), u.bob diff --git a/test/foundry/modules/ModuleBase.t.sol b/test/foundry/modules/ModuleBase.t.sol new file mode 100644 index 00000000..d1f40eb6 --- /dev/null +++ b/test/foundry/modules/ModuleBase.t.sol @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import { MockAccessController } from "test/foundry/mocks/MockAccessController.sol"; +import { BaseTest } from "test/foundry/utils/BaseTest.sol"; +import { LicenseRegistry } from "contracts/registries/LicenseRegistry.sol"; +import { IModule } from "contracts/interfaces/modules/base/IModule.sol"; +import { BaseModule } from "contracts/modules/BaseModule.sol"; +import { ModuleRegistry } from "contracts/registries/ModuleRegistry.sol"; +import { AccessController } from "contracts/AccessController.sol"; +import { IAccessController } from "contracts/interfaces/IAccessController.sol"; +import { ERC6551Registry } from "lib/reference/src/ERC6551Registry.sol"; +import { IModuleRegistry } from "contracts/interfaces/registries/IModuleRegistry.sol"; +import { IPRecordRegistry } from "contracts/registries/IPRecordRegistry.sol"; +import { IPAccountRegistry } from "contracts/registries/IPAccountRegistry.sol"; +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"; + +/// @title Module Base Test Contract +/// @notice Base contract for testing standard module functionality. +abstract contract ModuleBaseTest is BaseTest { + + /// @notice Gets the protocol-wide license registry. + LicenseRegistry public licenseRegistry; + + /// @notice The access controller address. + AccessController public accessController; + + /// @notice Gets the protocol-wide IP account registry. + IPAccountRegistry public ipAccountRegistry; + + /// @notice Gets the protocol-wide IP record registry. + IPRecordRegistry public ipRecordRegistry; + + /// @notice Gets the protocol-wide module registry. + ModuleRegistry public moduleRegistry; + + /// @notice The module SUT. + IModule public baseModule; + + /// @notice Initializes the base module for testing. + function setUp() public virtual override(BaseTest) { + BaseTest.setUp(); + licenseRegistry = new LicenseRegistry(""); + accessController = new AccessController(); + moduleRegistry = new ModuleRegistry(); + ipAccountRegistry = new IPAccountRegistry( + address(new ERC6551Registry()), + address(accessController), + address(new IPAccountImpl()) + ); + accessController.initialize(address(ipAccountRegistry), address(moduleRegistry)); + ipRecordRegistry = new IPRecordRegistry( + address(moduleRegistry), + address(ipAccountRegistry) + ); + baseModule = IModule(_deployModule()); + } + + /// @notice Tests that the default resolver constructor runs successfully. + function test_Module_Name() public { + assertEq(baseModule.name(), _expectedName()); + } + + /// @dev Deploys the module SUT. + function _deployModule() internal virtual returns (address); + + /// @dev Gets the expected name for the module. + function _expectedName() internal virtual view returns (string memory); + +} diff --git a/test/foundry/modules/RegistrationModule.t.sol b/test/foundry/modules/RegistrationModule.t.sol new file mode 100644 index 00000000..610db220 --- /dev/null +++ b/test/foundry/modules/RegistrationModule.t.sol @@ -0,0 +1,243 @@ +// SPDX-License-Identifier: MIT +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 { ModuleBaseTest } from "./ModuleBase.t.sol"; +import { IPAccountChecker } from "contracts/lib/registries/IPAccountChecker.sol"; +import { IAccessController } from "contracts/interfaces/IAccessController.sol"; +import { ERC6551Registry } from "lib/reference/src/ERC6551Registry.sol"; +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 { 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 { IIPAccount } from "contracts/interfaces/IIPAccount.sol"; + +/// @title IP Registration Module Test Contract +/// @notice Tests IP registration module functionality. +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 = ""; + + /// @notice The registration module SUT. + RegistrationModule public registrationModule; + + /// @notice Gets the IP metadata resolver tied to the registration module. + IIPMetadataResolver public resolver; + + /// @notice Mock NFT address for IP registration testing. + address public tokenAddress; + + /// @notice Mock NFT tokenId for IP registration testing. + uint256 public tokenId; + + /// @notice Alternative NFT tokenId for IP registration testing. + uint256 public tokenId2; + + /// @notice Policy Id with mocked terms for IP registration testing. + uint256 public policyId; + + /// @notice Initializes the base token contract for testing. + function setUp() public virtual override(ModuleBaseTest) { + ModuleBaseTest.setUp(); + _initLicensing(); + resolver = new IPMetadataResolver( + address(accessController), + address(ipRecordRegistry), + address(ipAccountRegistry), + address(licenseRegistry) + ); + registrationModule = RegistrationModule(_deployModule()); + moduleRegistry.registerModule(REGISTRATION_MODULE_KEY, address(registrationModule)); + moduleRegistry.registerModule(METADATA_RESOLVER_MODULE_KEY, address(resolver)); + MockERC721 erc721 = new MockERC721(); + tokenAddress = address(erc721); + tokenId = erc721.mintId(alice, 99); + tokenId2 = erc721.mintId(bob, 100); + } + + /// @notice Checks that the registration initialization operates correctly. + function test_RegistrationModule_Constructor() public { + assertEq(address(registrationModule.ACCESS_CONTROLLER()), address(accessController)); + 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. + function test_RegistrationModule_RegisterRootIP() public { + uint256 totalSupply = ipRecordRegistry.totalSupply(); + address ipId = ipRecordRegistry.ipId(block.chainid, tokenAddress, tokenId); + + // Ensure unregistered IP preconditions are satisfied. + assertTrue(!ipRecordRegistry.isRegistered(ipId)); + assertTrue(!ipRecordRegistry.isRegistered(block.chainid, tokenAddress, tokenId)); + assertTrue(!IPAccountChecker.isRegistered(ipAccountRegistry, block.chainid, tokenAddress, tokenId)); + + vm.startPrank(alice); + registrationModule.registerRootIp( + policyId, + tokenAddress, + tokenId + ); + vm.stopPrank(); + + /// Ensure registered IP postconditiosn are met. + assertEq(ipRecordRegistry.resolver(ipId), address(resolver)); + assertEq(totalSupply + 1, ipRecordRegistry.totalSupply()); + assertTrue(ipRecordRegistry.isRegistered(ipId)); + assertTrue(ipRecordRegistry.isRegistered(block.chainid, tokenAddress, tokenId)); + assertTrue(IPAccountChecker.isRegistered(ipAccountRegistry, block.chainid, tokenAddress, tokenId)); + } + + /// @notice Checks that registration reverts when called by an invalid owner. + function test_RegistrationModule_RegisterRootIP_Reverts_InvalidOwner() public { + vm.expectRevert(Errors.RegistrationModule__InvalidOwner.selector); + registrationModule.registerRootIp( + 0, + tokenAddress, + tokenId + ); + } + + /// @notice Checks whether root IP registration operates as expected. + function test_RegistrationModule_RegisterDerivativeIP() public { + // Register root IP with policyId + vm.prank(alice); + address ipId = registrationModule.registerRootIp( + policyId, + tokenAddress, + tokenId + ); + + // Mint license + Licensing.License memory licenseData = Licensing.License({ + policyId: policyId, + licensorIpIds: new address[](1) + }); + licenseData.licensorIpIds[0] = ipId; + uint256 licenseId = licenseRegistry.mintLicense(licenseData, 1, bob); + uint256 totalSupply = ipRecordRegistry.totalSupply(); + + + // Ensure unregistered IP preconditions are satisfied. + address ipId2 = ipRecordRegistry.ipId(block.chainid, tokenAddress, tokenId2); + + assertFalse(ipRecordRegistry.isRegistered(ipId2), "IP is already registered"); + assertFalse(ipRecordRegistry.isRegistered(block.chainid, tokenAddress, tokenId2), "IP is already registered"); + assertFalse(IPAccountChecker.isRegistered(ipAccountRegistry, block.chainid, tokenAddress, tokenId2), "IP is already registered"); + + vm.prank(bob); + registrationModule.registerDerivativeIp( + licenseId, + tokenAddress, + tokenId2, + RECORD_NAME, + RECORD_DESCRIPTION, + RECORD_HASH + ); + + + /// Ensur registered IP post-conditions are met. + assertEq(ipRecordRegistry.resolver(ipId2), address(resolver)); + 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); + } + + /// @notice Checks that registration reverts when called by an invalid owner. + function test_RegistrationModule_ARegisterDerivativeIP_Reverts_InvalidOwner() public { + vm.expectRevert(Errors.RegistrationModule__InvalidOwner.selector); + registrationModule.registerDerivativeIp( + 0, + tokenAddress, + tokenId, + RECORD_NAME, + RECORD_DESCRIPTION, + RECORD_HASH + ); + } + + /// @dev Deploys the registration module SUT. + function _deployModule() internal virtual override returns (address) { + return address( + new RegistrationModule( + address(accessController), + address(ipRecordRegistry), + address(ipAccountRegistry), + address(licenseRegistry), + address(resolver) + ) + ); + } + + /// @dev Gets the expected name for the module. + function _expectedName() internal virtual view override returns (string memory) { + return "REGISTRATION_MODULE"; + } + + // TODO: put this in the base test + function _initLicensing() private { + IParamVerifier[] memory mintingParamVerifiers = new IParamVerifier[](1); + MockIParamVerifier verifier = new MockIParamVerifier(); + mintingParamVerifiers[0] = verifier; + bytes[] memory mintingParamDefaultValues = new bytes[](1); + mintingParamDefaultValues[0] = abi.encode(true); + IParamVerifier[] memory activationParamVerifiers = new IParamVerifier[](1); + activationParamVerifiers[0] = verifier; + bytes[] memory activationParamDefaultValues = new bytes[](1); + activationParamDefaultValues[0] = abi.encode(true); + IParamVerifier[] memory linkParentParamVerifiers = new IParamVerifier[](1); + linkParentParamVerifiers[0] = verifier; + bytes[] memory linkParentParamDefaultValues = new bytes[](1); + linkParentParamDefaultValues[0] = abi.encode(true); + + Licensing.FrameworkCreationParams memory fwParams = Licensing.FrameworkCreationParams({ + mintingParamVerifiers: mintingParamVerifiers, + mintingParamDefaultValues: mintingParamDefaultValues, + activationParamVerifiers: activationParamVerifiers, + activationParamDefaultValues: activationParamDefaultValues, + defaultNeedsActivation: true, + linkParentParamVerifiers: linkParentParamVerifiers, + linkParentParamDefaultValues: linkParentParamDefaultValues, + licenseUrl: "https://example.com" + }); + licenseRegistry.addLicenseFramework(fwParams); + Licensing.Policy memory policy = Licensing.Policy({ + frameworkId: 1, + mintingParamValues: new bytes[](1), + activationParamValues: new bytes[](1), + needsActivation: false, + linkParentParamValues: new bytes[](1) + }); + policy.mintingParamValues[0] = abi.encode(true); + policy.activationParamValues[0] = abi.encode(true); + policy.linkParentParamValues[0] = abi.encode(true); + (uint256 polId, bool isNew) = licenseRegistry.addPolicy(policy); + + policyId = polId; + } + +} diff --git a/test/foundry/resolvers/IPMetadataResolver.t.sol b/test/foundry/resolvers/IPMetadataResolver.t.sol index ed698c72..dc7168ee 100644 --- a/test/foundry/resolvers/IPMetadataResolver.t.sol +++ b/test/foundry/resolvers/IPMetadataResolver.t.sol @@ -10,14 +10,17 @@ 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. @@ -30,14 +33,8 @@ contract IPMetadataResolverTest is ResolverBaseTest { uint64 public constant RECORD_REGISTRATION_DATE = 999999; string public constant RECORD_URI = "https://storyprotocol.xyz"; - /// @notice Placeholder for registration module. - address public registrationModule = vm.addr(0x1337); - - /// @notice The IP record registry. - IPRecordRegistry public registry; - - /// @notice The IP account registry. - IPAccountRegistry public ipAccountRegistry; + /// @notice The registration module. + address public registrationModule; /// @notice The token contract SUT. IIPMetadataResolver public ipResolver; @@ -48,22 +45,22 @@ contract IPMetadataResolverTest is ResolverBaseTest { /// @notice Initializes the base token contract for testing. function setUp() public virtual override(ResolverBaseTest) { ResolverBaseTest.setUp(); - ipAccountRegistry = new IPAccountRegistry( - address(new ERC6551Registry()), - address(accessController), - address(new IPAccountImpl()) - ); - registry = new IPRecordRegistry( - address(new MockModuleRegistry(registrationModule)), - address(ipAccountRegistry) - ); MockERC721 erc721 = new MockERC721(); vm.prank(alice); - ipResolver = IIPMetadataResolver(_deployResolver()); + ipResolver = IIPMetadataResolver(_deployModule()); uint256 tokenId = erc721.mintId(alice, 99); - ipId = registry.ipId(block.chainid, address(erc721), tokenId); + // 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); - registry.register( + ipId = ipRecordRegistry.register( block.chainid, address(erc721), tokenId, @@ -79,6 +76,7 @@ contract IPMetadataResolverTest is ResolverBaseTest { /// @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( @@ -113,7 +111,7 @@ contract IPMetadataResolverTest is ResolverBaseTest { /// @notice Checks that an unauthorized call to setMetadata reverts. function test_IPMetadataResolver_SetMetadata_Reverts_Unauthorized() public { - vm.expectRevert(Errors.IPResolver_Unauthorized.selector); + vm.expectRevert(Errors.Module_Unauthorized.selector); ipResolver.setMetadata( ipId, IP.MetadataRecord({ @@ -129,6 +127,7 @@ contract IPMetadataResolverTest is ResolverBaseTest { /// @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); @@ -137,12 +136,13 @@ contract IPMetadataResolverTest is ResolverBaseTest { /// @notice Checks that an unauthorized call to setName reverts. function test_IPMetadataResolver_SetName_Reverts_Unauthorized() public { - vm.expectRevert(Errors.IPResolver_Unauthorized.selector); + 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_SetDescription() public { + 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); @@ -151,12 +151,13 @@ contract IPMetadataResolverTest is ResolverBaseTest { /// @notice Checks that an unauthorized call to setDescription reverts. function test_IPMetadataResolver_SetDescription_Reverts_Unauthorized() public { - vm.expectRevert(Errors.IPResolver_Unauthorized.selector); + 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); @@ -165,7 +166,7 @@ contract IPMetadataResolverTest is ResolverBaseTest { /// @notice Checks that an unauthorized call to setHash reverts. function test_IPMetadataResolver_SetHash_Reverts_Unauthorized() public { - vm.expectRevert(Errors.IPResolver_Unauthorized.selector); + vm.expectRevert(Errors.Module_Unauthorized.selector); ipResolver.setHash(ipId, RECORD_HASH); } @@ -178,6 +179,7 @@ contract IPMetadataResolverTest is ResolverBaseTest { /// @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); @@ -221,13 +223,19 @@ contract IPMetadataResolverTest is ResolverBaseTest { 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 _deployResolver() internal override returns (address) { + function _deployModule() internal override returns (address) { return address( new IPMetadataResolver( address(accessController), - address(registry), - address(ipAccountRegistry) + address(ipRecordRegistry), + address(ipAccountRegistry), + address(licenseRegistry) ) ); } diff --git a/test/foundry/resolvers/ResolverBase.t.sol b/test/foundry/resolvers/ResolverBase.t.sol index b3c6995b..b64868d0 100644 --- a/test/foundry/resolvers/ResolverBase.t.sol +++ b/test/foundry/resolvers/ResolverBase.t.sol @@ -1,34 +1,23 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.23; -import "forge-std/console2.sol"; - import { MockAccessController } from "test/foundry/mocks/MockAccessController.sol"; -import { BaseTest } from "test/foundry/utils/BaseTest.sol"; +import { ModuleBaseTest } from "test/foundry/modules/ModuleBase.t.sol"; import { IResolver } from "contracts/interfaces/resolvers/IResolver.sol"; import { IAccessController } from "contracts/interfaces/IAccessController.sol"; import { Errors } from "contracts/lib/Errors.sol"; /// @title Resolver Base Test Contract /// @notice Base contract for testing standard resolver functionality. -abstract contract ResolverBaseTest is BaseTest { - - /// @notice The access controller address. - IAccessController public accessController; +abstract contract ResolverBaseTest is ModuleBaseTest { /// @notice The resolver SUT. IResolver public baseResolver; /// @notice Initializes the base ERC20 contract for testing. - function setUp() public virtual override(BaseTest) { - BaseTest.setUp(); - accessController = IAccessController(address(new MockAccessController())); - baseResolver = IResolver(_deployResolver()); - } - - /// @notice Tests that the default resolver constructor runs successfully. - function test_Resolver_Constructor() public { - assertEq(baseResolver.accessController(), address(accessController)); + function setUp() public virtual override(ModuleBaseTest) { + ModuleBaseTest.setUp(); + baseResolver = IResolver(_deployModule()); } /// @notice Tests that the base resolver interface is supported. @@ -36,7 +25,4 @@ abstract contract ResolverBaseTest is BaseTest { assertTrue(baseResolver.supportsInterface(type(IResolver).interfaceId)); } - /// @dev Deploys the resolver SUT. - function _deployResolver() internal virtual returns (address); - }