diff --git a/contracts/interfaces/registries/ILicenseRegistry.sol b/contracts/interfaces/registries/ILicenseRegistry.sol index b59f42e06..6c01e2e0f 100644 --- a/contracts/interfaces/registries/ILicenseRegistry.sol +++ b/contracts/interfaces/registries/ILicenseRegistry.sol @@ -50,11 +50,13 @@ interface ILicenseRegistry { /// @param parentIpIds An array of addresses of the parent IPs. /// @param licenseTemplate The address of the license template used. /// @param licenseTermsIds An array of IDs of the license terms. + /// @param isUsingLicenseToken Whether the derivative IP is registered with license tokens. function registerDerivativeIp( address ipId, address[] calldata parentIpIds, address licenseTemplate, - uint256[] calldata licenseTermsIds + uint256[] calldata licenseTermsIds, + bool isUsingLicenseToken ) external; /// @notice Checks if an IP is a derivative IP. diff --git a/contracts/modules/licensing/LicensingModule.sol b/contracts/modules/licensing/LicensingModule.sol index ccd3b9d14..37d561a49 100644 --- a/contracts/modules/licensing/LicensingModule.sol +++ b/contracts/modules/licensing/LicensingModule.sol @@ -258,7 +258,7 @@ contract LicensingModule is // Set the derivative IP as a derivative of the parent IPs. // Set the expiration timestamp for the derivative IP by invoking the license template to calculate // the earliest expiration time among all license terms. - LICENSE_REGISTRY.registerDerivativeIp(childIpId, parentIpIds, licenseTemplate, licenseTermsIds); + LICENSE_REGISTRY.registerDerivativeIp(childIpId, parentIpIds, licenseTemplate, licenseTermsIds, false); // Process the payment for the minting fee. (address commonRoyaltyPolicy, bytes[] memory royaltyDatas) = _payMintingFeeForAllParentIps( childIpId, @@ -325,7 +325,7 @@ contract LicensingModule is // Set the derivative IP as a derivative of the parent IPs. // Set the expiration timestamp for the derivative IP to match the earliest expiration time of // all license terms. - LICENSE_REGISTRY.registerDerivativeIp(childIpId, parentIpIds, licenseTemplate, licenseTermsIds); + LICENSE_REGISTRY.registerDerivativeIp(childIpId, parentIpIds, licenseTemplate, licenseTermsIds, true); // Confirm that the royalty policies defined in all license terms of the parent IPs are identical. address commonRoyaltyPolicy = address(0); diff --git a/contracts/registries/LicenseRegistry.sol b/contracts/registries/LicenseRegistry.sol index f95db503f..493a2562a 100644 --- a/contracts/registries/LicenseRegistry.sol +++ b/contracts/registries/LicenseRegistry.sol @@ -203,11 +203,13 @@ contract LicenseRegistry is ILicenseRegistry, AccessManagedUpgradeable, UUPSUpgr /// @param parentIpIds An array of addresses of the parent IPs. /// @param licenseTemplate The address of the license template used. /// @param licenseTermsIds An array of IDs of the license terms. + /// @param isUsingLicenseToken Whether the derivative IP is registered with license tokens. function registerDerivativeIp( address childIpId, address[] calldata parentIpIds, address licenseTemplate, - uint256[] calldata licenseTermsIds + uint256[] calldata licenseTermsIds, + bool isUsingLicenseToken ) external onlyLicensingModule { if (parentIpIds.length == 0) { revert Errors.LicenseRegistry__NoParentIp(); @@ -226,7 +228,13 @@ contract LicenseRegistry is ILicenseRegistry, AccessManagedUpgradeable, UUPSUpgr uint256 earliestExp = 0; for (uint256 i = 0; i < parentIpIds.length; i++) { earliestExp = ExpiringOps.getEarliestExpirationTime(earliestExp, _getExpireTime(parentIpIds[i])); - _verifyDerivativeFromParent(parentIpIds[i], childIpId, licenseTemplate, licenseTermsIds[i]); + _verifyDerivativeFromParent( + parentIpIds[i], + childIpId, + licenseTemplate, + licenseTermsIds[i], + isUsingLicenseToken + ); $.childIps[parentIpIds[i]].add(childIpId); // determine if duplicate license terms bool isNewParent = $.parentIps[childIpId].add(parentIpIds[i]); @@ -415,11 +423,13 @@ contract LicenseRegistry is ILicenseRegistry, AccessManagedUpgradeable, UUPSUpgr /// @param childIpId The address of the child IP /// @param licenseTemplate The address of the license template where the license terms are created /// @param licenseTermsId The license terms the child IP is registered with + /// @param isUsingLicenseToken Whether the child IP is registered with license tokens function _verifyDerivativeFromParent( address parentIpId, address childIpId, address licenseTemplate, - uint256 licenseTermsId + uint256 licenseTermsId, + bool isUsingLicenseToken ) internal view { LicenseRegistryStorage storage $ = _getLicenseRegistryStorage(); if (DISPUTE_MODULE.isIpTagged(parentIpId)) { @@ -433,10 +443,14 @@ contract LicenseRegistry is ILicenseRegistry, AccessManagedUpgradeable, UUPSUpgr } // childIp can only register with default license terms or the license terms attached to the parent IP if ($.defaultLicenseTemplate != licenseTemplate || $.defaultLicenseTermsId != licenseTermsId) { - if ($.licenseTemplates[parentIpId] != licenseTemplate) { + address pLicenseTemplate = $.licenseTemplates[parentIpId]; + if ( + (isUsingLicenseToken && pLicenseTemplate != address(0) && pLicenseTemplate != licenseTemplate) || + (!isUsingLicenseToken && pLicenseTemplate != licenseTemplate) + ) { revert Errors.LicenseRegistry__ParentIpUnmatchedLicenseTemplate(parentIpId, licenseTemplate); } - if (!$.attachedLicenseTerms[parentIpId].contains(licenseTermsId)) { + if (!isUsingLicenseToken && !$.attachedLicenseTerms[parentIpId].contains(licenseTermsId)) { revert Errors.LicenseRegistry__ParentIpHasNoLicenseTerms(parentIpId, licenseTermsId); } } diff --git a/test/foundry/modules/licensing/LicensingModule.t.sol b/test/foundry/modules/licensing/LicensingModule.t.sol index 1d370aa16..9b4d21d5e 100644 --- a/test/foundry/modules/licensing/LicensingModule.t.sol +++ b/test/foundry/modules/licensing/LicensingModule.t.sol @@ -628,6 +628,57 @@ contract LicensingModuleTest is BaseTest { assertEq(licenseTermsId, termsId); } + function test_LicensingModule_registerDerivativeWithLicenseTokens_privateLicense() public { + uint256 termsId = pilTemplate.registerLicenseTerms(PILFlavors.nonCommercialSocialRemixing()); + + vm.prank(ipOwner1); + uint256 lcTokenId = licensingModule.mintLicenseTokens({ + licensorIpId: ipId1, + licenseTemplate: address(pilTemplate), + licenseTermsId: termsId, + amount: 1, + receiver: ipOwner2, + royaltyContext: "" + }); + + assertEq(licenseRegistry.hasIpAttachedLicenseTerms(ipId1, address(pilTemplate), termsId), false); + + assertEq(licenseToken.ownerOf(lcTokenId), ipOwner2); + assertEq(licenseToken.getLicenseTermsId(lcTokenId), termsId); + assertEq(licenseToken.getLicenseTemplate(lcTokenId), address(pilTemplate)); + assertEq(licenseToken.getLicensorIpId(lcTokenId), ipId1); + assertEq(licenseToken.totalMintedTokens(), 1); + assertEq(licenseToken.totalSupply(), 1); + assertEq(licenseToken.balanceOf(ipOwner2), 1); + + uint256[] memory licenseTokens = new uint256[](1); + licenseTokens[0] = lcTokenId; + vm.prank(ipOwner2); + licensingModule.registerDerivativeWithLicenseTokens(ipId2, licenseTokens, ""); + + assertEq(licenseRegistry.hasIpAttachedLicenseTerms(ipId2, address(pilTemplate), termsId), true); + assertEq(licenseRegistry.hasIpAttachedLicenseTerms(ipId1, address(pilTemplate), termsId), false); + + assertEq(licenseRegistry.getAttachedLicenseTermsCount(ipId2), 1); + assertEq(licenseRegistry.isDerivativeIp(ipId2), true); + assertEq(licenseRegistry.hasDerivativeIps(ipId2), false); + assertEq(licenseRegistry.hasDerivativeIps(ipId1), true); + assertEq(licenseRegistry.isDerivativeIp(ipId1), false); + assertEq(licenseRegistry.getDerivativeIpCount(ipId1), 1); + assertEq(licenseRegistry.getDerivativeIpCount(ipId2), 0); + assertEq(licenseRegistry.getDerivativeIp(ipId1, 0), ipId2); + assertEq(licenseRegistry.getParentIp(ipId2, 0), ipId1); + assertEq(licenseRegistry.getParentIpCount(ipId2), 1); + assertEq(licenseToken.totalSupply(), 0); + assertEq(licenseToken.totalMintedTokens(), 1); + vm.expectRevert(abi.encodeWithSelector(ERC721NonexistentToken.selector, lcTokenId)); + licenseToken.ownerOf(lcTokenId); + + (address licenseTemplate, uint256 licenseTermsId) = licenseRegistry.getAttachedLicenseTerms(ipId2, 0); + assertEq(licenseTemplate, address(pilTemplate)); + assertEq(licenseTermsId, termsId); + } + function test_LicensingModule_registerDerivativeWithLicenseTokens_revert_pause() public { uint256 termsId = pilTemplate.registerLicenseTerms(PILFlavors.nonCommercialSocialRemixing()); vm.prank(ipOwner1); diff --git a/test/foundry/registries/LicenseRegistry.t.sol b/test/foundry/registries/LicenseRegistry.t.sol index be96a37d9..12c8a0f50 100644 --- a/test/foundry/registries/LicenseRegistry.t.sol +++ b/test/foundry/registries/LicenseRegistry.t.sol @@ -190,7 +190,13 @@ contract LicenseRegistryTest is BaseTest { licenseTermsIds[0] = socialRemixTermsId; vm.expectRevert(Errors.LicenseRegistry__NoParentIp.selector); vm.prank(address(licensingModule)); - licenseRegistry.registerDerivativeIp(ipAcct[2], parentIpIds, address(pilTemplate), licenseTermsIds); + licenseRegistry.registerDerivativeIp({ + childIpId: ipAcct[2], + parentIpIds: parentIpIds, + licenseTemplate: address(pilTemplate), + licenseTermsIds: licenseTermsIds, + isUsingLicenseToken: false + }); } // test getAttachedLicenseTerms @@ -250,7 +256,107 @@ contract LicenseRegistryTest is BaseTest { licenseTermsIds[0] = socialRemixTermsId; licenseTermsIds[1] = socialRemixTermsId; vm.prank(address(licensingModule)); - licenseRegistry.registerDerivativeIp(ipAcct[3], parentIpIds, address(pilTemplate), licenseTermsIds); + licenseRegistry.registerDerivativeIp({ + childIpId: ipAcct[3], + parentIpIds: parentIpIds, + licenseTemplate: address(pilTemplate), + licenseTermsIds: licenseTermsIds, + isUsingLicenseToken: false + }); + // test registerDerivativeIp using licenseToken + vm.prank(address(licensingModule)); + licenseRegistry.registerDerivativeIp({ + childIpId: ipAcct[5], + parentIpIds: parentIpIds, + licenseTemplate: address(pilTemplate), + licenseTermsIds: licenseTermsIds, + isUsingLicenseToken: true + }); + } + + function test_LicenseRegistry_registerDerivativeIp_unattachedLicenseTerm_usingLicenseToken() public { + uint256 socialRemixTermsId = pilTemplate.registerLicenseTerms(PILFlavors.nonCommercialSocialRemixing()); + vm.prank(ipOwner[1]); + licensingModule.attachLicenseTerms(ipAcct[1], address(pilTemplate), socialRemixTermsId); + vm.prank(ipOwner[2]); + licensingModule.attachLicenseTerms(ipAcct[2], address(pilTemplate), socialRemixTermsId); + + uint256 commercialRemixTermsId = pilTemplate.registerLicenseTerms( + PILFlavors.commercialRemix(100, 10, address(royaltyPolicyLAP), address(erc20)) + ); + + address[] memory parentIpIds = new address[](2); + parentIpIds[0] = ipAcct[1]; + parentIpIds[1] = ipAcct[2]; + uint256[] memory licenseTermsIds = new uint256[](2); + licenseTermsIds[0] = commercialRemixTermsId; + licenseTermsIds[1] = commercialRemixTermsId; + vm.prank(address(licensingModule)); + licenseRegistry.registerDerivativeIp({ + childIpId: ipAcct[3], + parentIpIds: parentIpIds, + licenseTemplate: address(pilTemplate), + licenseTermsIds: licenseTermsIds, + isUsingLicenseToken: true + }); + } + + function test_LicenseRegistry_registerDerivativeIp_parentNoLicenseTemplate_usingLicenseToken() public { + uint256 commercialRemixTermsId = pilTemplate.registerLicenseTerms( + PILFlavors.commercialRemix(100, 10, address(royaltyPolicyLAP), address(erc20)) + ); + + address[] memory parentIpIds = new address[](2); + parentIpIds[0] = ipAcct[1]; + parentIpIds[1] = ipAcct[2]; + uint256[] memory licenseTermsIds = new uint256[](2); + licenseTermsIds[0] = commercialRemixTermsId; + licenseTermsIds[1] = commercialRemixTermsId; + vm.prank(address(licensingModule)); + licenseRegistry.registerDerivativeIp({ + childIpId: ipAcct[3], + parentIpIds: parentIpIds, + licenseTemplate: address(pilTemplate), + licenseTermsIds: licenseTermsIds, + isUsingLicenseToken: true + }); + } + + function test_LicenseRegistry_registerDerivativeIp_unmatchLicenseTemplate_usingLicenseToken() public { + uint256 socialRemixTermsId = pilTemplate.registerLicenseTerms(PILFlavors.nonCommercialSocialRemixing()); + vm.prank(ipOwner[1]); + licensingModule.attachLicenseTerms(ipAcct[1], address(pilTemplate), socialRemixTermsId); + vm.prank(ipOwner[2]); + licensingModule.attachLicenseTerms(ipAcct[2], address(pilTemplate), socialRemixTermsId); + + MockLicenseTemplate pilTemplate2 = new MockLicenseTemplate(); + vm.prank(admin); + licenseRegistry.registerLicenseTemplate(address(pilTemplate2)); + uint256 mockTermsId = pilTemplate2.registerLicenseTerms(); + + address[] memory parentIpIds = new address[](2); + parentIpIds[0] = ipAcct[1]; + parentIpIds[1] = ipAcct[2]; + uint256[] memory licenseTermsIds = new uint256[](2); + licenseTermsIds[0] = mockTermsId; + licenseTermsIds[1] = mockTermsId; + + vm.expectRevert( + abi.encodeWithSelector( + Errors.LicenseRegistry__ParentIpUnmatchedLicenseTemplate.selector, + ipAcct[1], + address(pilTemplate2) + ) + ); + + vm.prank(address(licensingModule)); + licenseRegistry.registerDerivativeIp({ + childIpId: ipAcct[3], + parentIpIds: parentIpIds, + licenseTemplate: address(pilTemplate2), + licenseTermsIds: licenseTermsIds, + isUsingLicenseToken: true + }); } function test_LicenseRegistry_registerDerivativeIp_revert_DuplicateLicense() public { @@ -273,7 +379,66 @@ contract LicenseRegistryTest is BaseTest { ) ); vm.prank(address(licensingModule)); - licenseRegistry.registerDerivativeIp(ipAcct[2], parentIpIds, address(pilTemplate), licenseTermsIds); + licenseRegistry.registerDerivativeIp({ + childIpId: ipAcct[2], + parentIpIds: parentIpIds, + licenseTemplate: address(pilTemplate), + licenseTermsIds: licenseTermsIds, + isUsingLicenseToken: false + }); + } + + function test_LicenseRegistry_registerDerivativeIp_revert_UnattachedLicenseTemplate() public { + uint256 socialRemixTermsId = pilTemplate.registerLicenseTerms(PILFlavors.nonCommercialSocialRemixing()); + + address[] memory parentIpIds = new address[](1); + parentIpIds[0] = ipAcct[1]; + uint256[] memory licenseTermsIds = new uint256[](1); + licenseTermsIds[0] = socialRemixTermsId; + + vm.expectRevert( + abi.encodeWithSelector( + Errors.LicenseRegistry__ParentIpUnmatchedLicenseTemplate.selector, + ipAcct[1], + address(pilTemplate) + ) + ); + vm.prank(address(licensingModule)); + licenseRegistry.registerDerivativeIp({ + childIpId: ipAcct[2], + parentIpIds: parentIpIds, + licenseTemplate: address(pilTemplate), + licenseTermsIds: licenseTermsIds, + isUsingLicenseToken: false + }); + } + + function test_LicenseRegistry_registerDerivativeIp_revert_UnattachedLicenseTermsId() public { + uint256 socialRemixTermsId = pilTemplate.registerLicenseTerms(PILFlavors.nonCommercialSocialRemixing()); + vm.prank(ipOwner[1]); + licensingModule.attachLicenseTerms(ipAcct[1], address(pilTemplate), socialRemixTermsId); + uint256 commercialRemixTermsId = pilTemplate.registerLicenseTerms( + PILFlavors.commercialRemix(100, 10, address(royaltyPolicyLAP), address(erc20)) + ); + address[] memory parentIpIds = new address[](1); + parentIpIds[0] = ipAcct[1]; + uint256[] memory licenseTermsIds = new uint256[](1); + licenseTermsIds[0] = commercialRemixTermsId; + vm.expectRevert( + abi.encodeWithSelector( + Errors.LicenseRegistry__ParentIpHasNoLicenseTerms.selector, + ipAcct[1], + commercialRemixTermsId + ) + ); + vm.prank(address(licensingModule)); + licenseRegistry.registerDerivativeIp({ + childIpId: ipAcct[2], + parentIpIds: parentIpIds, + licenseTemplate: address(pilTemplate), + licenseTermsIds: licenseTermsIds, + isUsingLicenseToken: false + }); } function test_LicenseRegistry_isExpiredNow() public { @@ -333,7 +498,13 @@ contract LicenseRegistryTest is BaseTest { licenseTermsIds[0] = termsId1; licenseTermsIds[1] = termsId2; vm.prank(address(licensingModule)); - licenseRegistry.registerDerivativeIp(ipAcct[3], parentIpIds, address(pilTemplate), licenseTermsIds); + licenseRegistry.registerDerivativeIp({ + childIpId: ipAcct[3], + parentIpIds: parentIpIds, + licenseTemplate: address(pilTemplate), + licenseTermsIds: licenseTermsIds, + isUsingLicenseToken: false + }); assertEq(licenseRegistry.getExpireTime(ipAcct[1]), block.timestamp + 100, "ipAcct[1] expire time is incorrect"); assertEq(licenseRegistry.getExpireTime(ipAcct[2]), 0, "ipAcct[2] expire time is incorrect"); @@ -371,8 +542,13 @@ contract LicenseRegistryTest is BaseTest { licenseTermsIds[0] = termsId1; licenseTermsIds[1] = termsId2; vm.prank(address(licensingModule)); - licenseRegistry.registerDerivativeIp(ipAcct[3], parentIpIds, address(pilTemplate), licenseTermsIds); - + licenseRegistry.registerDerivativeIp({ + childIpId: ipAcct[3], + parentIpIds: parentIpIds, + licenseTemplate: address(pilTemplate), + licenseTermsIds: licenseTermsIds, + isUsingLicenseToken: false + }); assertEq(licenseRegistry.getExpireTime(ipAcct[1]), block.timestamp + 500, "ipAcct[1] expire time is incorrect"); assertEq(licenseRegistry.getExpireTime(ipAcct[2]), 0, "ipAcct[2] expire time is incorrect"); assertEq(licenseRegistry.getExpireTime(ipAcct[3]), block.timestamp + 400, "ipAcct[3] expire time is incorrect");