diff --git a/contracts/interfaces/modules/licensing/ILicensingModule.sol b/contracts/interfaces/modules/licensing/ILicensingModule.sol index 302c1598..a6f7f91d 100644 --- a/contracts/interfaces/modules/licensing/ILicensingModule.sol +++ b/contracts/interfaces/modules/licensing/ILicensingModule.sol @@ -59,6 +59,9 @@ interface ILicensingModule is IModule { /// @param data The policy data function registerPolicy(bool isLicenseTransferable, bytes memory data) external returns (uint256 policyId); + /// @notice returns the policy id for the given data, or 0 if not found + function getPolicyId(address framework, bool isLicenseTransferable, bytes memory data) external view returns (uint256 policyId); + /// @notice Adds a policy to an IP policy list /// @param ipId The id of the IP /// @param polId The id of the policy diff --git a/contracts/interfaces/modules/licensing/IUMLPolicyFrameworkManager.sol b/contracts/interfaces/modules/licensing/IUMLPolicyFrameworkManager.sol index e7dfd9cb..a31681e6 100644 --- a/contracts/interfaces/modules/licensing/IUMLPolicyFrameworkManager.sol +++ b/contracts/interfaces/modules/licensing/IUMLPolicyFrameworkManager.sol @@ -70,6 +70,9 @@ interface IUMLPolicyFrameworkManager is IPolicyFrameworkManager { /// @return policy The UMLPolicy struct function getPolicy(uint256 policyId) external view returns (UMLPolicy memory policy); + /// @notice gets the policy ID for the given policy data, or 0 if not found + function getPolicyId(UMLPolicy calldata umlPolicy) external view returns (uint256 policyId); + /// @notice gets the aggregation data for inherited policies. function getAggregator(address ipId) external view returns (UMLAggregator memory rights); } diff --git a/contracts/lib/Errors.sol b/contracts/lib/Errors.sol index 0e879e2a..9882ddff 100644 --- a/contracts/lib/Errors.sol +++ b/contracts/lib/Errors.sol @@ -125,7 +125,6 @@ library Errors { error LicensingModule__FrameworkNotFound(); error LicensingModule__EmptyLicenseUrl(); error LicensingModule__InvalidPolicyFramework(); - error LicensingModule__PolicyAlreadyAdded(); error LicensingModule__ParamVerifierLengthMismatch(); error LicensingModule__PolicyNotFound(); error LicensingModule__NotLicensee(); diff --git a/contracts/modules/licensing/LicensingModule.sol b/contracts/modules/licensing/LicensingModule.sol index c30c3bbd..2e1519a1 100644 --- a/contracts/modules/licensing/LicensingModule.sol +++ b/contracts/modules/licensing/LicensingModule.sol @@ -86,6 +86,32 @@ contract LicensingModule is AccessControlled, ILicensingModule { emit PolicyFrameworkRegistered(manager, fwManager.name(), licenseUrl); } + /// @notice Registers a policy into the contract. MUST be called by a registered + /// framework or it will revert. The policy data and its integrity must be + /// verified by the policy framework manager. + /// @param isLicenseTransferable True if the license is transferable + /// @param data The policy data + function registerPolicy(bool isLicenseTransferable, bytes memory data) external returns (uint256 policyId) { + _verifyRegisteredFramework(address(msg.sender)); + Licensing.Policy memory pol = Licensing.Policy({ + isLicenseTransferable: isLicenseTransferable, + policyFramework: msg.sender, + data: data + }); + (uint256 polId, bool newPol) = DataUniqueness.addIdOrGetExisting( + abi.encode(pol), + _hashedPolicies, + _totalPolicies + ); + + if (newPol) { + _totalPolicies = polId; + _policies[polId] = pol; + emit PolicyRegistered(msg.sender, polId, data); + } + return 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. @@ -119,35 +145,6 @@ contract LicensingModule is AccessControlled, ILicensingModule { } } - /// @notice Registers a policy into the contract. MUST be called by a registered - /// framework or it will revert. The policy data and its integrity must be - /// verified by the policy framework manager. - /// @param isLicenseTransferable True if the license is transferable - /// @param data The policy data - function registerPolicy(bool isLicenseTransferable, bytes memory data) external returns (uint256 policyId) { - _verifyRegisteredFramework(address(msg.sender)); - Licensing.Policy memory pol = Licensing.Policy({ - isLicenseTransferable: isLicenseTransferable, - policyFramework: msg.sender, - data: data - }); - (uint256 polId, bool newPol) = DataUniqueness.addIdOrGetExisting( - abi.encode(pol), - _hashedPolicies, - _totalPolicies - ); - - if (!newPol) { - revert Errors.LicensingModule__PolicyAlreadyAdded(); - } else { - _totalPolicies = polId; - _policies[polId] = pol; - emit PolicyRegistered(msg.sender, polId, data); - } - - return polId; - } - /// 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 @@ -299,82 +296,6 @@ contract LicensingModule is AccessControlled, ILicensingModule { LICENSE_REGISTRY.burnLicenses(holder, licenseIds); } - function _linkIpToParent( - uint256 iteration, - uint256 licenseId, - uint256 policyId, - address licensor, - address childIpId, - address royaltyPolicyAddress, - uint32 royaltyDerivativeRevShare, - uint32 derivativeRevShareSum - ) - private - returns ( - address nextRoyaltyPolicyAddress, - uint32 nextRoyaltyDerivativeRevShare, - uint32 nextDerivativeRevShareSum - ) - { - // TODO: check licensor not part of a branch tagged by disputer - if (licensor == childIpId) { - revert Errors.LicensingModule__ParentIdEqualThanChild(); - } - // Verify linking params - Licensing.Policy memory pol = policy(policyId); - IPolicyFrameworkManager.VerifyLinkResponse memory response = IPolicyFrameworkManager(pol.policyFramework) - .verifyLink(licenseId, msg.sender, childIpId, licensor, pol.data); - - if (!response.isLinkingAllowed) { - revert Errors.LicensingModule__LinkParentParamFailed(); - } - - // Compatibility check: If link says no royalty is required for license (licenseIds[i]) but - // another license requires royalty, revert. - if (!response.isRoyaltyRequired && royaltyPolicyAddress != address(0)) { - revert Errors.LicensingModule__IncompatibleLicensorCommercialPolicy(); - } - - // If link says royalty is required for license (licenseIds[i]) and no royalty policy is set, set it. - // But if the index is NOT 0, this is previous licenses didn't set the royalty policy because they don't - // require royalty payment. So, revert in this case. Similarly, if the new royaltyPolicyAddress is different - // from the previous one (in iteration > 0), revert. We currently restrict all licenses (parents) to have - // the same royalty policy, so the child can inherit it. - if (response.isRoyaltyRequired) { - if (iteration > 0 && royaltyPolicyAddress != response.royaltyPolicy) { - // If iteration > 0 and - // - royaltyPolicyAddress == address(0), revert. Previous licenses didn't set RP. - // - royaltyPolicyAddress != response.royaltyPolicy, revert. Previous licenses set different RP. - // ==> this can be considered as royaltyPolicyAddress != response.royaltyPolicy - revert Errors.LicensingModule__IncompatibleRoyaltyPolicyAddress(); - } - - // TODO: Unit test. - // If the previous license's derivativeRevShare is different from that of the current license, revert. - // For iteration == 0, this check is skipped as `royaltyDerivativeRevShare` param is at 0. - if (iteration > 0 && royaltyDerivativeRevShare != response.royaltyDerivativeRevShare) { - revert Errors.LicensingModule__IncompatibleRoyaltyPolicyDerivativeRevShare(); - } - - // TODO: Read max RNFT supply instead of hardcoding the expected max supply - // TODO: Do we need safe check? - // TODO: Test this in unit test. - if (derivativeRevShareSum + response.royaltyDerivativeRevShare > 1000) { - revert Errors.LicensingModule__DerivativeRevShareSumExceedsMaxRNFTSupply(); - } - - nextRoyaltyPolicyAddress = response.royaltyPolicy; - nextRoyaltyDerivativeRevShare = response.royaltyDerivativeRevShare; - nextDerivativeRevShareSum = derivativeRevShareSum + response.royaltyDerivativeRevShare; - } - - // Add the policy of licenseIds[i] to the child. If the policy's already set from previous parents, - // then the addition will be skipped. - _addPolicyIdToIp({ ipId: childIpId, policyId: policyId, isInherited: true, skipIfDuplicate: true }); - // Set parent - _ipIdParents[childIpId].add(licensor); - } - /// @notice True if the framework address is registered in LicenseRegistry function isFrameworkRegistered(address policyFramework) external view returns (bool) { return _registeredFrameworkManagers[policyFramework]; @@ -392,6 +313,16 @@ contract LicensingModule is AccessControlled, ILicensingModule { return pol; } + /// @notice gets the policy id for the given data, or 0 if not found + function getPolicyId(address framework, bool isLicenseTransferable, bytes memory data) external view returns (uint256 policyId) { + Licensing.Policy memory pol = Licensing.Policy({ + isLicenseTransferable: isLicenseTransferable, + policyFramework: framework, + data: data + }); + return _hashedPolicies[keccak256(abi.encode(pol))]; + } + function policyAggregatorData(address framework, address ipId) external view returns (bytes memory) { return _ipRights[framework][ipId]; } @@ -498,6 +429,83 @@ contract LicensingModule is AccessControlled, ILicensingModule { return index; } + function _linkIpToParent( + uint256 iteration, + uint256 licenseId, + uint256 policyId, + address licensor, + address childIpId, + address royaltyPolicyAddress, + uint32 royaltyDerivativeRevShare, + uint32 derivativeRevShareSum + ) + private + returns ( + address nextRoyaltyPolicyAddress, + uint32 nextRoyaltyDerivativeRevShare, + uint32 nextDerivativeRevShareSum + ) + { + // TODO: check licensor not part of a branch tagged by disputer + if (licensor == childIpId) { + revert Errors.LicensingModule__ParentIdEqualThanChild(); + } + // Verify linking params + Licensing.Policy memory pol = policy(policyId); + IPolicyFrameworkManager.VerifyLinkResponse memory response = IPolicyFrameworkManager(pol.policyFramework) + .verifyLink(licenseId, msg.sender, childIpId, licensor, pol.data); + + if (!response.isLinkingAllowed) { + revert Errors.LicensingModule__LinkParentParamFailed(); + } + + // Compatibility check: If link says no royalty is required for license (licenseIds[i]) but + // another license requires royalty, revert. + if (!response.isRoyaltyRequired && royaltyPolicyAddress != address(0)) { + revert Errors.LicensingModule__IncompatibleLicensorCommercialPolicy(); + } + + // If link says royalty is required for license (licenseIds[i]) and no royalty policy is set, set it. + // But if the index is NOT 0, this is previous licenses didn't set the royalty policy because they don't + // require royalty payment. So, revert in this case. Similarly, if the new royaltyPolicyAddress is different + // from the previous one (in iteration > 0), revert. We currently restrict all licenses (parents) to have + // the same royalty policy, so the child can inherit it. + if (response.isRoyaltyRequired) { + if (iteration > 0 && royaltyPolicyAddress != response.royaltyPolicy) { + // If iteration > 0 and + // - royaltyPolicyAddress == address(0), revert. Previous licenses didn't set RP. + // - royaltyPolicyAddress != response.royaltyPolicy, revert. Previous licenses set different RP. + // ==> this can be considered as royaltyPolicyAddress != response.royaltyPolicy + revert Errors.LicensingModule__IncompatibleRoyaltyPolicyAddress(); + } + + // TODO: Unit test. + // If the previous license's derivativeRevShare is different from that of the current license, revert. + // For iteration == 0, this check is skipped as `royaltyDerivativeRevShare` param is at 0. + if (iteration > 0 && royaltyDerivativeRevShare != response.royaltyDerivativeRevShare) { + revert Errors.LicensingModule__IncompatibleRoyaltyPolicyDerivativeRevShare(); + } + + // TODO: Read max RNFT supply instead of hardcoding the expected max supply + // TODO: Do we need safe check? + // TODO: Test this in unit test. + if (derivativeRevShareSum + response.royaltyDerivativeRevShare > 1000) { + revert Errors.LicensingModule__DerivativeRevShareSumExceedsMaxRNFTSupply(); + } + + nextRoyaltyPolicyAddress = response.royaltyPolicy; + nextRoyaltyDerivativeRevShare = response.royaltyDerivativeRevShare; + nextDerivativeRevShareSum = derivativeRevShareSum + response.royaltyDerivativeRevShare; + } + + // Add the policy of licenseIds[i] to the child. If the policy's already set from previous parents, + // then the addition will be skipped. + _addPolicyIdToIp({ ipId: childIpId, policyId: policyId, isInherited: true, skipIfDuplicate: true }); + // Set parent + _ipIdParents[childIpId].add(licensor); + } + + function _verifyCanAddPolicy(uint256 policyId, address ipId, bool isInherited) private { bool ipIdIsDerivative = _policySetPerIpId(true, ipId).length() > 0; if ( diff --git a/contracts/modules/licensing/UMLPolicyFrameworkManager.sol b/contracts/modules/licensing/UMLPolicyFrameworkManager.sol index 8f9c0306..472c00c6 100644 --- a/contracts/modules/licensing/UMLPolicyFrameworkManager.sol +++ b/contracts/modules/licensing/UMLPolicyFrameworkManager.sol @@ -123,6 +123,10 @@ contract UMLPolicyFrameworkManager is IUMLPolicyFrameworkManager, BasePolicyFram rights = abi.decode(policyAggregatorData, (UMLAggregator)); } + function getPolicyId(UMLPolicy calldata umlPolicy) external view returns (uint256 policyId) { + return LICENSING_MODULE.getPolicyId(address(this), umlPolicy.transferable, abi.encode(umlPolicy)); + } + function getRoyaltyPolicy(uint256 policyId) external view returns (address) { return getPolicy(policyId).royaltyPolicy; } diff --git a/test/foundry/mocks/licensing/MockLicensingModule.sol b/test/foundry/mocks/licensing/MockLicensingModule.sol index 8f43a6f0..b88315cf 100644 --- a/test/foundry/mocks/licensing/MockLicensingModule.sol +++ b/test/foundry/mocks/licensing/MockLicensingModule.sol @@ -37,6 +37,8 @@ contract MockLicensingModule is ILicensingModule { function isPolicyIdSetForIp(bool isInherited, address ipId, uint256 policyId) external view returns (bool) {} + function getPolicyId(address framework, bool isLicenseTransferable, bytes memory data) external view returns (uint256 policyId) {} + function policyIdForIpAtIndex( bool isInherited, address ipId, diff --git a/test/foundry/modules/licensing/LicensingModule.t.sol b/test/foundry/modules/licensing/LicensingModule.t.sol index 8607d7e1..785686fb 100644 --- a/test/foundry/modules/licensing/LicensingModule.t.sol +++ b/test/foundry/modules/licensing/LicensingModule.t.sol @@ -136,15 +136,22 @@ contract LicensingModuleTest is Test { assertEq(polId, 1, "polId not 1"); } - function test_LicensingModule_registerPolicy_revert_policyAlreadyAdded() public { + function test_LicensingModule_registerPolicy_reusesIdForAlreadyAddedPolicy() public { licensingModule.registerPolicyFrameworkManager(address(module1)); vm.startPrank(address(module1)); - licensingModule.registerPolicy(true, _createPolicy()); - vm.expectRevert(Errors.LicensingModule__PolicyAlreadyAdded.selector); - licensingModule.registerPolicy(true, _createPolicy()); + uint256 polId = licensingModule.registerPolicy(true, _createPolicy()); + assertEq(polId, licensingModule.registerPolicy(true, _createPolicy())); vm.stopPrank(); } + function test_LicensingModule_getPolicyId() public { + licensingModule.registerPolicyFrameworkManager(address(module1)); + bytes memory policy = _createPolicy(); + vm.prank(address(module1)); + uint256 policyId = licensingModule.registerPolicy(true, policy); + assertEq(licensingModule.getPolicyId(address(module1), true, policy), policyId, "policyId not found"); + } + function test_LicensingModule_registerPolicy_revert_frameworkNotFound() public { bytes memory policy = _createPolicy(); vm.expectRevert(Errors.LicensingModule__FrameworkNotFound.selector); diff --git a/test/foundry/modules/licensing/UMLPolicyFramework.t.sol b/test/foundry/modules/licensing/UMLPolicyFramework.t.sol index 7d8f7d25..f716ad9c 100644 --- a/test/foundry/modules/licensing/UMLPolicyFramework.t.sol +++ b/test/foundry/modules/licensing/UMLPolicyFramework.t.sol @@ -37,6 +37,28 @@ contract UMLPolicyFrameworkTest is TestHelper { ipId2 = ipAccountRegistry.registerIpAccount(block.chainid, address(nft), 2); } + function test_UMLPolicyFrameworkManager_getPolicyId() public { + UMLPolicy memory umlPolicy = UMLPolicy({ + transferable: true, + attribution: true, + commercialUse: false, + commercialAttribution: false, + commercializers: emptyStringArray, + commercialRevShare: 0, + derivativesAllowed: false, + derivativesAttribution: false, + derivativesApproval: false, + derivativesReciprocal: false, + derivativesRevShare: 0, + territories: emptyStringArray, + distributionChannels: emptyStringArray, + contentRestrictions: emptyStringArray, + royaltyPolicy: address(0) + }); + uint256 policyId = umlFramework.registerPolicy(umlPolicy); + assertEq(umlFramework.getPolicyId(umlPolicy), policyId); + } + function test_UMLPolicyFrameworkManager__valuesSetCorrectly() public { string[] memory territories = new string[](2); territories[0] = "test1";