diff --git a/contracts/lib/Errors.sol b/contracts/lib/Errors.sol index b5d8f8a2..2b6ee6c7 100644 --- a/contracts/lib/Errors.sol +++ b/contracts/lib/Errors.sol @@ -271,6 +271,32 @@ library Errors { /// @notice The empty group cannot mint license token. error LicenseRegistry__EmptyGroupCannotMintLicenseToken(address groupId); + /// @notice The group can only attach one license terms which is common for all members. + error LicenseRegistry__GroupIpAlreadyHasLicenseTerms(address groupId); + + /// @notice The license template cannot be Zero address. + error LicenseRegistry__LicenseTemplateCannotBeZeroAddress(); + + /// @notice license minting fee configured in IP must be identical to the group minting fee. + error LicenseRegistry__IpMintingFeeNotMatchWithGroup(address ipId, uint256 mintingFee, uint256 groupMintingFee); + + /// @notice licensing hook configured in IP must be identical to the group licensing hook. + error LicenseRegistry__IpLicensingHookNotMatchWithGroup( + address ipId, + address licensingHook, + address groupLicensingHook + ); + + /// @notice licensing hook data configured in IP must be identical to the group licensing hook data. + error LicenseRegistry__IpLicensingHookDataNotMatchWithGroup(address ipId, bytes hookData, bytes groupHookData); + + /// @notice commercial revenue share configured in group must be NOT less than the IP commercial revenue share. + error LicenseRegistry__GroupIpCommercialRevShareConfigMustNotLessThanIp( + address groupId, + uint32 ipCommercialRevShare, + uint32 groupCommercialRevShare + ); + //////////////////////////////////////////////////////////////////////////// // License Token // //////////////////////////////////////////////////////////////////////////// @@ -402,6 +428,28 @@ library Errors { /// @notice register derivative require all parent IP to have the same royalty policy. error LicensingModule__RoyaltyPolicyMismatch(address royaltyPolicy, address anotherRoyaltyPolicy); + /// @notice The group IP cannot enable/disable the licensing configuration once it has members. + error LicensingModule__GroupIpCannotChangeIsSet(address groupId); + + /// @notice The group IP cannot change minting fee once it has members. + error LicensingModule__GroupIpCannotChangeMintingFee(address groupId); + + /// @notice The group IP cannot change licensing hook once it has members. + error LicensingModule__GroupIpCannotChangeLicensingHook(address groupId); + + /// @notice The group IP cannot change hook data once it has members. + error LicensingModule__GroupIpCannotChangeHookData(address groupId); + + /// @notice The group Ip cannot specify expect group reward pool, as a group cannot be added to another group. + error LicensingModule__GroupIpCannotSetExpectGroupRewardPool(address groupId); + + /// @notice GroupIP cannot decrease the royalty percentage. + error LicensingModule__GroupIpCannotDecreaseRoyalty( + address groupId, + uint32 newRoyaltyPercent, + uint32 oldRoyaltyPercent + ); + //////////////////////////////////////////////////////////////////////////// // Dispute Module // //////////////////////////////////////////////////////////////////////////// diff --git a/contracts/modules/licensing/LicensingModule.sol b/contracts/modules/licensing/LicensingModule.sol index c70a33c1..bdff9956 100644 --- a/contracts/modules/licensing/LicensingModule.sol +++ b/contracts/modules/licensing/LicensingModule.sol @@ -12,6 +12,7 @@ import { IIPAccount } from "../../interfaces/IIPAccount.sol"; import { IModule } from "../../interfaces/modules/base/IModule.sol"; import { ILicensingModule } from "../../interfaces/modules/licensing/ILicensingModule.sol"; import { IIPAssetRegistry } from "../../interfaces/registries/IIPAssetRegistry.sol"; +import { IGroupIPAssetRegistry } from "../../interfaces/registries/IGroupIPAssetRegistry.sol"; import { IDisputeModule } from "../../interfaces/modules/dispute/IDisputeModule.sol"; import { ILicenseRegistry } from "../../interfaces/registries/ILicenseRegistry.sol"; import { Errors } from "../../lib/Errors.sol"; @@ -403,6 +404,10 @@ contract LicensingModule is revert Errors.LicensingModule__LicenseTemplateCannotBeZeroAddressToOverrideRoyaltyPercent(); } + if (IGroupIPAssetRegistry(address(IP_ASSET_REGISTRY)).isRegisteredGroup(ipId)) { + _verifyGroupIpConfig(ipId, licenseTemplate, licenseTermsId, licensingConfig); + } + if (licensingConfig.commercialRevShare != 0) { ILicenseTemplate lct = ILicenseTemplate(licenseTemplate); if (!LICENSE_REGISTRY.isRegisteredLicenseTemplate(licenseTemplate)) { @@ -726,6 +731,53 @@ contract LicensingModule is } } + /// @dev Verifies the group IP licensing configuration + function _verifyGroupIpConfig( + address groupId, + address licenseTemplate, + uint256 licenseTermsId, + Licensing.LicensingConfig memory licensingConfig + ) private { + if (licenseTemplate == address(0)) { + revert Errors.LicenseRegistry__LicenseTemplateCannotBeZeroAddress(); + } + if (licensingConfig.expectGroupRewardPool != address(0)) { + revert Errors.LicensingModule__GroupIpCannotSetExpectGroupRewardPool(groupId); + } + // Some configuration cannot be changed once the group has members + if (IGroupIPAssetRegistry(address(IP_ASSET_REGISTRY)).totalMembers(groupId) == 0) { + return; + } + Licensing.LicensingConfig memory oldLicensingConfig = LICENSE_REGISTRY.getLicensingConfig( + groupId, + licenseTemplate, + licenseTermsId + ); + if (oldLicensingConfig.isSet != licensingConfig.isSet) { + revert Errors.LicensingModule__GroupIpCannotChangeIsSet(groupId); + } + if (oldLicensingConfig.mintingFee != licensingConfig.mintingFee) { + revert Errors.LicensingModule__GroupIpCannotChangeMintingFee(groupId); + } + if (oldLicensingConfig.licensingHook != licensingConfig.licensingHook) { + revert Errors.LicensingModule__GroupIpCannotChangeLicensingHook(groupId); + } + // check hood data are the same + if ( + oldLicensingConfig.hookData.length != licensingConfig.hookData.length || + keccak256(oldLicensingConfig.hookData) != keccak256(licensingConfig.hookData) + ) { + revert Errors.LicensingModule__GroupIpCannotChangeHookData(groupId); + } + if (licensingConfig.commercialRevShare < oldLicensingConfig.commercialRevShare) { + revert Errors.LicensingModule__GroupIpCannotDecreaseRoyalty( + groupId, + licensingConfig.commercialRevShare, + oldLicensingConfig.commercialRevShare + ); + } + } + /// @dev Hook to authorize the upgrade according to UUPSUpgradeable /// @param newImplementation The address of the new implementation function _authorizeUpgrade(address newImplementation) internal override restricted {} diff --git a/contracts/registries/LicenseRegistry.sol b/contracts/registries/LicenseRegistry.sol index b84a4b8e..e1c67fc1 100644 --- a/contracts/registries/LicenseRegistry.sol +++ b/contracts/registries/LicenseRegistry.sol @@ -197,6 +197,13 @@ contract LicenseRegistry is ILicenseRegistry, AccessManagedUpgradeable, UUPSUpgr if (!_exists(licenseTemplate, licenseTermsId)) { revert Errors.LicensingModule__LicenseTermsNotFound(licenseTemplate, licenseTermsId); } + // The group can only attach one license terms which is common for all members. + if ( + GROUP_IP_ASSET_REGISTRY.isRegisteredGroup(ipId) && + _getLicenseRegistryStorage().attachedLicenseTerms[ipId].length() > 0 + ) { + revert Errors.LicenseRegistry__GroupIpAlreadyHasLicenseTerms(ipId); + } if (_isExpiredNow(ipId)) { revert Errors.LicenseRegistry__IpExpired(ipId); @@ -326,6 +333,7 @@ contract LicenseRegistry is ILicenseRegistry, AccessManagedUpgradeable, UUPSUpgr /// @param groupLicenseTermsId The ID of the license terms attached to the group. /// the IP must have this license terms. /// @return ipLicensingConfig The configuration for license attached to the IP. + // solhint-disable code-complexity function verifyGroupAddIp( address groupId, address groupRewardPool, @@ -356,6 +364,39 @@ contract LicenseRegistry is ILicenseRegistry, AccessManagedUpgradeable, UUPSUpgr if (_getExpireTime(ipId) != 0) { revert Errors.LicenseRegistry__CannotAddIpWithExpirationToGroup(ipId); } + // ipId must have the same license config items with group IP + Licensing.LicensingConfig memory groupLct = _getLicensingConfig( + groupId, + groupLicenseTemplate, + groupLicenseTermsId + ); + // minting fee must be the same + if (lct.mintingFee != groupLct.mintingFee) { + revert Errors.LicenseRegistry__IpMintingFeeNotMatchWithGroup(ipId, lct.mintingFee, groupLct.mintingFee); + } + // hook must be the same + if (lct.licensingHook != groupLct.licensingHook) { + revert Errors.LicenseRegistry__IpLicensingHookNotMatchWithGroup( + ipId, + lct.licensingHook, + groupLct.licensingHook + ); + } + // hook data must be the same + if ( + lct.hookData.length != groupLct.hookData.length || keccak256(lct.hookData) != keccak256(groupLct.hookData) + ) { + revert Errors.LicenseRegistry__IpLicensingHookDataNotMatchWithGroup(ipId, lct.hookData, groupLct.hookData); + } + // group commercial revenue share must be greater than or equal to IP commercial revenue share + if (groupLct.commercialRevShare < lct.commercialRevShare) { + revert Errors.LicenseRegistry__GroupIpCommercialRevShareConfigMustNotLessThanIp( + ipId, + lct.commercialRevShare, + groupLct.commercialRevShare + ); + } + ipLicensingConfig = lct; } diff --git a/test/foundry/integration/flows/grouping/Grouping.t.sol b/test/foundry/integration/flows/grouping/Grouping.t.sol index c6c5952c..f26ecf50 100644 --- a/test/foundry/integration/flows/grouping/Grouping.t.sol +++ b/test/foundry/integration/flows/grouping/Grouping.t.sol @@ -69,13 +69,18 @@ contract Flows_Integration_Grouping is BaseIntegration, ERC721Holder { expectGroupRewardPool: address(evenSplitGroupPool) }); + licensingConfig.expectGroupRewardPool = address(0); + { vm.startPrank(groupOwner); groupId = groupingModule.registerGroup(address(evenSplitGroupPool)); vm.label(groupId, "Group1"); licensingModule.attachLicenseTerms(groupId, address(pilTemplate), commRemixTermsId); + licensingModule.setLicensingConfig(groupId, address(pilTemplate), commRemixTermsId, licensingConfig); vm.stopPrank(); } + + licensingConfig.expectGroupRewardPool = address(evenSplitGroupPool); { vm.startPrank(u.alice); ipAcct[1] = registerIpAccount(mockNFT, 1, u.alice); diff --git a/test/foundry/modules/grouping/GroupingModule.t.sol b/test/foundry/modules/grouping/GroupingModule.t.sol index e358df54..605b696f 100644 --- a/test/foundry/modules/grouping/GroupingModule.t.sol +++ b/test/foundry/modules/grouping/GroupingModule.t.sol @@ -160,6 +160,8 @@ contract GroupingModuleTest is BaseTest, ERC721Holder { vm.startPrank(alice); licensingModule.attachLicenseTerms(groupId, address(pilTemplate), termsId); + licensingConfig.expectGroupRewardPool = address(0); + licensingModule.setLicensingConfig(groupId, address(pilTemplate), termsId, licensingConfig); address[] memory ipIds = new address[](2); ipIds[0] = ipId1; ipIds[1] = ipId2; @@ -204,8 +206,10 @@ contract GroupingModuleTest is BaseTest, ERC721Holder { licensingModule.setLicensingConfig(ipId2, address(pilTemplate), termsId, licensingConfig); vm.stopPrank(); + licensingConfig.expectGroupRewardPool = address(0); vm.startPrank(alice); licensingModule.attachLicenseTerms(groupId, address(pilTemplate), termsId); + licensingModule.setLicensingConfig(groupId, address(pilTemplate), termsId, licensingConfig); address[] memory ipIds = new address[](2); ipIds[0] = ipId1; ipIds[1] = ipId2; @@ -254,8 +258,10 @@ contract GroupingModuleTest is BaseTest, ERC721Holder { licensingModule.mintLicenseTokens(ipId2, address(pilTemplate), termsId, 1, address(this), "", 0); vm.stopPrank(); + licensingConfig.expectGroupRewardPool = address(0); vm.startPrank(alice); licensingModule.attachLicenseTerms(groupId, address(pilTemplate), termsId); + licensingModule.setLicensingConfig(groupId, address(pilTemplate), termsId, licensingConfig); address[] memory ipIds = new address[](2); ipIds[0] = ipId1; ipIds[1] = ipId2; @@ -332,8 +338,10 @@ contract GroupingModuleTest is BaseTest, ERC721Holder { licensingModule.mintLicenseTokens(ipId2, address(pilTemplate), termsId, 1, address(this), "", 0); vm.stopPrank(); + licensingConfig.expectGroupRewardPool = address(0); vm.startPrank(alice); licensingModule.attachLicenseTerms(groupId, address(pilTemplate), termsId); + licensingModule.setLicensingConfig(groupId, address(pilTemplate), termsId, licensingConfig); address[] memory ipIds = new address[](2); ipIds[0] = ipId1; ipIds[1] = ipId2; @@ -650,10 +658,6 @@ contract GroupingModuleTest is BaseTest, ERC721Holder { royaltyPolicy: address(royaltyPolicyLAP) }) ); - vm.startPrank(alice); - address groupId1 = groupingModule.registerGroup(address(rewardPool)); - licensingModule.attachLicenseTerms(groupId1, address(pilTemplate), termsId); - vm.stopPrank(); Licensing.LicensingConfig memory licensingConfig = Licensing.LicensingConfig({ isSet: true, @@ -676,6 +680,13 @@ contract GroupingModuleTest is BaseTest, ERC721Holder { licensingModule.setLicensingConfig(ipId2, address(pilTemplate), termsId, licensingConfig); vm.stopPrank(); + licensingConfig.expectGroupRewardPool = address(0); + vm.startPrank(alice); + address groupId1 = groupingModule.registerGroup(address(rewardPool)); + licensingModule.attachLicenseTerms(groupId1, address(pilTemplate), termsId); + licensingModule.setLicensingConfig(groupId1, address(pilTemplate), termsId, licensingConfig); + vm.stopPrank(); + address[] memory ipIds = new address[](1); ipIds[0] = ipId1; vm.prank(alice); @@ -760,11 +771,6 @@ contract GroupingModuleTest is BaseTest, ERC721Holder { }) ); - vm.startPrank(alice); - address groupId = groupingModule.registerGroup(address(rewardPool)); - licensingModule.attachLicenseTerms(groupId, address(pilTemplate), termsId); - vm.stopPrank(); - vm.prank(ipOwner1); licensingModule.attachLicenseTerms(ipId1, address(pilTemplate), termsId); vm.prank(ipOwner2); @@ -783,6 +789,13 @@ contract GroupingModuleTest is BaseTest, ERC721Holder { vm.prank(ipOwner1); licensingModule.setLicensingConfig(ipId1, address(pilTemplate), termsId, licensingConfig); + licensingConfig.expectGroupRewardPool = address(0); + vm.startPrank(alice); + address groupId = groupingModule.registerGroup(address(rewardPool)); + licensingModule.attachLicenseTerms(groupId, address(pilTemplate), termsId); + licensingModule.setLicensingConfig(groupId, address(pilTemplate), termsId, licensingConfig); + vm.stopPrank(); + address[] memory ipIds = new address[](1); ipIds[0] = ipId1; vm.prank(alice); @@ -922,9 +935,11 @@ contract GroupingModuleTest is BaseTest, ERC721Holder { licensingModule.setLicensingConfig(ipId2, address(pilTemplate), termsId, licensingConfig); vm.stopPrank(); + licensingConfig.expectGroupRewardPool = address(0); vm.startPrank(alice); address groupId = groupingModule.registerGroup(address(rewardPool)); licensingModule.attachLicenseTerms(groupId, address(pilTemplate), termsId); + licensingModule.setLicensingConfig(groupId, address(pilTemplate), termsId, licensingConfig); address[] memory ipIds = new address[](2); ipIds[0] = ipId1; ipIds[1] = ipId2;