diff --git a/contracts/interfaces/workflows/IGroupingWorkflows.sol b/contracts/interfaces/workflows/IGroupingWorkflows.sol index 06e32f7..f8e3471 100644 --- a/contracts/interfaces/workflows/IGroupingWorkflows.sol +++ b/contracts/interfaces/workflows/IGroupingWorkflows.sol @@ -81,4 +81,64 @@ interface IGroupingWorkflows { address[] calldata currencyTokens, address[] calldata memberIpIds ) external returns (uint256[] memory collectedRoyalties); + + //////////////////////////////////////////////////////////////////////////// + // DEPRECATED, WILL BE REMOVED IN V1.4 // + //////////////////////////////////////////////////////////////////////////// + + /// @notice Mint an NFT from a SPGNFT collection, register it with metadata as an IP, + /// attach license terms to the registered IP, and add it to a group IP. + /// @notice THIS VERSION OF THE FUNCTION IS DEPRECATED, WILL BE REMOVED IN V1.4 + function mintAndRegisterIpAndAttachLicenseAndAddToGroup( + address spgNftContract, + address groupId, + address recipient, + address licenseTemplate, + uint256 licenseTermsId, + WorkflowStructs.IPMetadata calldata ipMetadata, + WorkflowStructs.SignatureData calldata sigAddToGroup + ) external returns (address ipId, uint256 tokenId); + + /// @notice Register an NFT as IP with metadata, attach license terms to the registered IP, + /// and add it to a group IP. + /// @notice THIS VERSION OF THE FUNCTION IS DEPRECATED, WILL BE REMOVED IN V1.4 + /// @dev UPDATE REQUIRED: The sigMetadataAndAttachAndConfig permission signature data must be updated and include permissions for + /// metadata setting, license attachment, and licensing configuration permissions + function registerIpAndAttachLicenseAndAddToGroup( + address nftContract, + uint256 tokenId, + address groupId, + address licenseTemplate, + uint256 licenseTermsId, + WorkflowStructs.IPMetadata calldata ipMetadata, + WorkflowStructs.SignatureData calldata sigMetadataAndAttachAndConfig, + WorkflowStructs.SignatureData calldata sigAddToGroup + ) external returns (address ipId); + + /// @notice Register a group IP with a group reward pool and attach license terms to the group IP + /// @notice THIS VERSION OF THE FUNCTION IS DEPRECATED, WILL BE REMOVED IN V1.4 + function registerGroupAndAttachLicense( + address groupPool, + address licenseTemplate, + uint256 licenseTermsId + ) external returns (address groupId); + + /// @notice Register a group IP with a group reward pool, attach license terms to the group IP, + /// and add individual IPs to the group IP. + /// @notice THIS VERSION OF THE FUNCTION IS DEPRECATED, WILL BE REMOVED IN V1.4 + function registerGroupAndAttachLicenseAndAddIps( + address groupPool, + address[] calldata ipIds, + address licenseTemplate, + uint256 licenseTermsId + ) external returns (address groupId); + + /// @notice Collect royalties for the entire group and distribute the rewards to each member IP's royalty vault + /// @notice THIS VERSION OF THE FUNCTION IS DEPRECATED, WILL BE REMOVED IN V1.4 + function collectRoyaltiesAndClaimReward( + address groupIpId, + address[] calldata currencyTokens, + uint256[] calldata groupSnapshotIds, + address[] calldata memberIpIds + ) external returns (uint256[] memory collectedRoyalties); } diff --git a/contracts/workflows/GroupingWorkflows.sol b/contracts/workflows/GroupingWorkflows.sol index 24b9267..0f8f886 100644 --- a/contracts/workflows/GroupingWorkflows.sol +++ b/contracts/workflows/GroupingWorkflows.sol @@ -10,8 +10,11 @@ import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils import { ICoreMetadataModule } from "@storyprotocol/core/interfaces/modules/metadata/ICoreMetadataModule.sol"; import { IGroupingModule } from "@storyprotocol/core/interfaces/modules/grouping/IGroupingModule.sol"; +import { IGroupIPAssetRegistry } from "@storyprotocol/core/interfaces/registries/IGroupIPAssetRegistry.sol"; import { ILicensingModule } from "@storyprotocol/core/interfaces/modules/licensing/ILicensingModule.sol"; +import { IPILicenseTemplate, PILTerms } from "@storyprotocol/core/interfaces/modules/licensing/IPILicenseTemplate.sol"; import { GroupNFT } from "@storyprotocol/core/GroupNFT.sol"; +import { Licensing } from "@storyprotocol/core/lib/Licensing.sol"; import { RoyaltyModule } from "@storyprotocol/core/modules/royalty/RoyaltyModule.sol"; import { BaseWorkflow } from "../BaseWorkflow.sol"; @@ -315,7 +318,7 @@ contract GroupingWorkflows is /// @dev Attaches licenses to the given IP and sets their licensing configurations. /// @param ipId The ID of the IP. /// @param licensesData The data of the licenses and their configurations to be attached to the IP. - function _attachLicensesAndSetConfigs(address ipId, WorkflowStructs.LicenseData[] calldata licensesData) private { + function _attachLicensesAndSetConfigs(address ipId, WorkflowStructs.LicenseData[] memory licensesData) private { for (uint256 i; i < licensesData.length; i++) { LicensingHelper.attachLicenseTermsAndSetConfigs( ipId, @@ -341,4 +344,219 @@ contract GroupingWorkflows is /// @dev Hook to authorize the upgrade according to UUPSUpgradeable /// @param newImplementation The address of the new implementation function _authorizeUpgrade(address newImplementation) internal override restricted {} + + //////////////////////////////////////////////////////////////////////////// + // DEPRECATED, WILL BE REMOVED IN V1.4 // + //////////////////////////////////////////////////////////////////////////// + + /// @notice Mint an NFT from a SPGNFT collection, register it with metadata as an IP, attach + /// license terms to the registered IP, and add it to a group IP. + /// @notice THIS VERSION OF THE FUNCTION IS DEPRECATED, WILL BE REMOVED IN V1.4 + function mintAndRegisterIpAndAttachLicenseAndAddToGroup( + address spgNftContract, + address groupId, + address recipient, + address licenseTemplate, + uint256 licenseTermsId, + WorkflowStructs.IPMetadata calldata ipMetadata, + WorkflowStructs.SignatureData calldata sigAddToGroup + ) external onlyMintAuthorized(spgNftContract) returns (address ipId, uint256 tokenId) { + tokenId = ISPGNFT(spgNftContract).mintByPeriphery({ + to: address(this), + payer: msg.sender, + nftMetadataURI: ipMetadata.nftMetadataURI, + nftMetadataHash: "", + allowDuplicates: true + }); + ipId = IP_ASSET_REGISTRY.register(block.chainid, spgNftContract, tokenId); + MetadataHelper.setMetadata(ipId, address(CORE_METADATA_MODULE), ipMetadata); + + _prepConfigAndAttachLicenseAndSetConfig(ipId, groupId, licenseTemplate, licenseTermsId); + + PermissionHelper.setPermissionForModule( + groupId, + address(GROUPING_MODULE), + address(ACCESS_CONTROLLER), + IGroupingModule.addIp.selector, + sigAddToGroup + ); + + address[] memory ipIds = new address[](1); + ipIds[0] = ipId; + GROUPING_MODULE.addIp(groupId, ipIds); + + ISPGNFT(spgNftContract).safeTransferFrom(address(this), recipient, tokenId, ""); + } + + /// @notice Register an NFT as IP with metadata, attach license terms to the registered IP, + /// and add it to a group IP. + /// @notice THIS VERSION OF THE FUNCTION IS DEPRECATED, WILL BE REMOVED IN V1.4 + /// @dev UPDATE REQUIRED: The sigMetadataAndAttachAndConfig permission signature data must be updated and include permissions for + /// metadata setting, license attachment, and licensing configuration permissions + function registerIpAndAttachLicenseAndAddToGroup( + address nftContract, + uint256 tokenId, + address groupId, + address licenseTemplate, + uint256 licenseTermsId, + WorkflowStructs.IPMetadata calldata ipMetadata, + WorkflowStructs.SignatureData calldata sigMetadataAndAttachAndConfig, + WorkflowStructs.SignatureData calldata sigAddToGroup + ) external returns (address ipId) { + ipId = IP_ASSET_REGISTRY.register(block.chainid, nftContract, tokenId); + + address[] memory modules = new address[](3); + bytes4[] memory selectors = new bytes4[](3); + modules[0] = address(CORE_METADATA_MODULE); + modules[1] = address(LICENSING_MODULE); + modules[2] = address(LICENSING_MODULE); + selectors[0] = ICoreMetadataModule.setAll.selector; + selectors[1] = ILicensingModule.attachLicenseTerms.selector; + selectors[2] = ILicensingModule.setLicensingConfig.selector; + + PermissionHelper.setBatchPermissionForModules( + ipId, + address(ACCESS_CONTROLLER), + modules, + selectors, + sigMetadataAndAttachAndConfig + ); + + MetadataHelper.setMetadata(ipId, address(CORE_METADATA_MODULE), ipMetadata); + + _prepConfigAndAttachLicenseAndSetConfig(ipId, groupId, licenseTemplate, licenseTermsId); + + PermissionHelper.setPermissionForModule( + groupId, + address(GROUPING_MODULE), + address(ACCESS_CONTROLLER), + IGroupingModule.addIp.selector, + sigAddToGroup + ); + + address[] memory ipIds = new address[](1); + ipIds[0] = ipId; + GROUPING_MODULE.addIp(groupId, ipIds); + } + + /// @notice Register a group IP with a group reward pool and attach license terms to the group IP + /// @notice THIS VERSION OF THE FUNCTION IS DEPRECATED, WILL BE REMOVED IN V1.4 + function registerGroupAndAttachLicense( + address groupPool, + address licenseTemplate, + uint256 licenseTermsId + ) external returns (address groupId) { + groupId = GROUPING_MODULE.registerGroup(groupPool); + + _prepConfigAndAttachLicenseAndSetConfigForGroup(groupId, groupPool, licenseTemplate, licenseTermsId); + + GROUP_NFT.safeTransferFrom(address(this), msg.sender, GROUP_NFT.totalSupply() - 1); + } + + /// @notice Register a group IP with a group reward pool, attach license terms to the group IP, + /// and add individual IPs to the group IP. + /// @notice THIS VERSION OF THE FUNCTION IS DEPRECATED, WILL BE REMOVED IN V1.4 + function registerGroupAndAttachLicenseAndAddIps( + address groupPool, + address[] calldata ipIds, + address licenseTemplate, + uint256 licenseTermsId + ) external returns (address groupId) { + groupId = GROUPING_MODULE.registerGroup(groupPool); + + _prepConfigAndAttachLicenseAndSetConfigForGroup(groupId, groupPool, licenseTemplate, licenseTermsId); + + GROUPING_MODULE.addIp(groupId, ipIds); + + GROUP_NFT.safeTransferFrom(address(this), msg.sender, GROUP_NFT.totalSupply() - 1); + } + + /// @notice Collect royalties for the entire group and distribute the rewards to each member IP's royalty vault + /// @notice THIS VERSION OF THE FUNCTION IS DEPRECATED, WILL BE REMOVED IN V1.4 + function collectRoyaltiesAndClaimReward( + address groupIpId, + address[] calldata currencyTokens, + uint256[] calldata groupSnapshotIds, + address[] calldata memberIpIds + ) external returns (uint256[] memory collectedRoyalties) { + (address groupLicenseTemplate, uint256 groupLicenseTermsId) = LICENSE_REGISTRY.getAttachedLicenseTerms( + groupIpId, + 0 + ); + + for (uint256 i = 0; i < memberIpIds.length; i++) { + // check if given member IPs already have a royalty vault + if (ROYALTY_MODULE.ipRoyaltyVaults(memberIpIds[i]) == address(0)) { + // mint license tokens to the member IPs if they don't have a royalty vault + LICENSING_MODULE.mintLicenseTokens({ + licensorIpId: memberIpIds[i], + licenseTemplate: groupLicenseTemplate, + licenseTermsId: groupLicenseTermsId, + amount: 1, + receiver: msg.sender, + royaltyContext: "", + maxMintingFee: 0, + maxRevenueShare: 0 + }); + } + } + + collectedRoyalties = new uint256[](currencyTokens.length); + for (uint256 i = 0; i < currencyTokens.length; i++) { + if (currencyTokens[i] == address(0)) revert Errors.GroupingWorkflows__ZeroAddressParam(); + collectedRoyalties[i] = GROUPING_MODULE.collectRoyalties(groupIpId, currencyTokens[i]); + GROUPING_MODULE.claimReward(groupIpId, currencyTokens[i], memberIpIds); + } + } + + function _prepConfigAndAttachLicenseAndSetConfig( + address ipId, + address groupId, + address licenseTemplate, + uint256 licenseTermsId + ) private { + PILTerms memory terms = IPILicenseTemplate(licenseTemplate).getLicenseTerms(licenseTermsId); + WorkflowStructs.LicenseData[] memory licensesData = new WorkflowStructs.LicenseData[](1); + licensesData[0] = WorkflowStructs.LicenseData({ + licenseTemplate: licenseTemplate, + licenseTermsId: licenseTermsId, + licensingConfig: Licensing.LicensingConfig({ + isSet: true, + mintingFee: terms.defaultMintingFee, + licensingHook: address(0), + hookData: "", + commercialRevShare: terms.commercialRevShare, + disabled: false, + expectMinimumGroupRewardShare: 0, + expectGroupRewardPool: IGroupIPAssetRegistry(address(IP_ASSET_REGISTRY)).getGroupRewardPool(groupId) + }) + }); + _attachLicensesAndSetConfigs(ipId, licensesData); + } + + function _prepConfigAndAttachLicenseAndSetConfigForGroup( + address groupId, + address groupRewardPool, + address licenseTemplate, + uint256 licenseTermsId + ) private { + PILTerms memory terms = IPILicenseTemplate(licenseTemplate).getLicenseTerms(licenseTermsId); + Licensing.LicensingConfig memory licensingConfig = Licensing.LicensingConfig({ + isSet: true, + mintingFee: terms.defaultMintingFee, + licensingHook: address(0), + hookData: "", + commercialRevShare: terms.commercialRevShare, + disabled: false, + expectMinimumGroupRewardShare: 0, + expectGroupRewardPool: address(0) + }); + LicensingHelper.attachLicenseTermsAndSetConfigs( + groupId, + address(LICENSING_MODULE), + licenseTemplate, + licenseTermsId, + licensingConfig + ); + } } diff --git a/test/integration/workflows/GroupingIntegration.t.sol b/test/integration/workflows/GroupingIntegration.t.sol index 6d2ecea..ad2dcee 100644 --- a/test/integration/workflows/GroupingIntegration.t.sol +++ b/test/integration/workflows/GroupingIntegration.t.sol @@ -318,7 +318,11 @@ contract GroupingIntegration is BaseIntegration { bytes[] memory data = new bytes[](numCalls); for (uint256 i = 0; i < numCalls; i++) { data[i] = abi.encodeWithSelector( - groupingWorkflows.mintAndRegisterIpAndAttachLicenseAndAddToGroup.selector, + bytes4( + keccak256( + "mintAndRegisterIpAndAttachLicenseAndAddToGroup(address,address,address,(address,uint256,(bool,uint256,address,bytes,uint32,bool,uint32,address))[],(string,bytes32,string,bytes32),(address,uint256,bytes),bool)" + ) + ), address(spgNftContract), groupId, testSender, @@ -414,7 +418,11 @@ contract GroupingIntegration is BaseIntegration { bytes[] memory data = new bytes[](numCalls); for (uint256 i = 0; i < numCalls; i++) { data[i] = abi.encodeWithSelector( - groupingWorkflows.registerIpAndAttachLicenseAndAddToGroup.selector, + bytes4( + keccak256( + "registerIpAndAttachLicenseAndAddToGroup(address,uint256,address,(address,uint256,(bool,uint256,address,bytes,uint32,bool,uint32,address))[],(string,bytes32,string,bytes32),(address,uint256,bytes),(address,uint256,bytes))" + ) + ), address(spgNftContract), tokenIds[i], groupId, diff --git a/test/workflows/GroupingWorkflows.t.sol b/test/workflows/GroupingWorkflows.t.sol index d4c6e6c..630cad3 100644 --- a/test/workflows/GroupingWorkflows.t.sol +++ b/test/workflows/GroupingWorkflows.t.sol @@ -428,7 +428,11 @@ contract GroupingWorkflowsTest is BaseTest, ERC721Holder { bytes[] memory data = new bytes[](10); for (uint256 i = 0; i < 10; i++) { data[i] = abi.encodeWithSelector( - groupingWorkflows.mintAndRegisterIpAndAttachLicenseAndAddToGroup.selector, + bytes4( + keccak256( + "mintAndRegisterIpAndAttachLicenseAndAddToGroup(address,address,address,(address,uint256,(bool,uint256,address,bytes,uint32,bool,uint32,address))[],(string,bytes32,string,bytes32),(address,uint256,bytes),bool)" + ) + ), address(spgNftPublic), groupId, minter, @@ -507,7 +511,11 @@ contract GroupingWorkflowsTest is BaseTest, ERC721Holder { bytes[] memory data = new bytes[](10); for (uint256 i = 0; i < 10; i++) { data[i] = abi.encodeWithSelector( - groupingWorkflows.registerIpAndAttachLicenseAndAddToGroup.selector, + bytes4( + keccak256( + "registerIpAndAttachLicenseAndAddToGroup(address,uint256,address,(address,uint256,(bool,uint256,address,bytes,uint32,bool,uint32,address))[],(string,bytes32,string,bytes32),(address,uint256,bytes),(address,uint256,bytes))" + ) + ), mockNft, tokenIds[i], groupId, @@ -636,4 +644,451 @@ contract GroupingWorkflowsTest is BaseTest, ERC721Holder { } vm.stopPrank(); } + + //////////////////////////////////////////////////////////////////////////// + // DEPRECATED, WILL BE REMOVED IN V1.4 // + //////////////////////////////////////////////////////////////////////////// + function test_GroupingWorkflows_mintAndRegisterIpAndAttachLicenseAndAddToGroup_DEPR() public { + uint256 deadline = block.timestamp + 1000; + + // Get the signature for setting the permission for calling `addIp` function in `GroupingModule` + // from the Group IP owner + (bytes memory sigAddToGroup, bytes32 expectedState, ) = _getSetPermissionSigForPeriphery({ + ipId: groupId, + to: address(groupingWorkflows), + module: address(groupingModule), + selector: IGroupingModule.addIp.selector, + deadline: deadline, + state: IIPAccount(payable(groupId)).state(), + signerSk: groupOwnerSk + }); + + vm.startPrank(minter); + (address ipId, uint256 tokenId) = groupingWorkflows.mintAndRegisterIpAndAttachLicenseAndAddToGroup({ + spgNftContract: address(spgNftPublic), + groupId: groupId, + recipient: minter, + licenseTemplate: address(pilTemplate), + ipMetadata: ipMetadataDefault, + licenseTermsId: testLicensesData[0].licenseTermsId, + sigAddToGroup: WorkflowStructs.SignatureData({ + signer: groupOwner, + deadline: deadline, + signature: sigAddToGroup + }) + }); + vm.stopPrank(); + + // check the group IP account state matches the expected state + assertEq(IIPAccount(payable(groupId)).state(), expectedState); + + // check the IP is registered + assertTrue(IIPAssetRegistry(ipAssetRegistry).isRegistered(ipId)); + + // check the IP is added to the group + assertTrue(IGroupIPAssetRegistry(ipAssetRegistry).containsIp(groupId, ipId)); + + // check the NFT metadata is correctly set + assertEq(spgNftPublic.tokenURI(tokenId), string.concat(testBaseURI, ipMetadataDefault.nftMetadataURI)); + + // check the IP metadata is correctly set + assertMetadata(ipId, ipMetadataDefault); + + // check the license terms is correctly attached + (address licenseTemplate, uint256 licenseTermsId) = ILicenseRegistry(address(licenseRegistry)) + .getAttachedLicenseTerms(ipId, 0); + assertEq(licenseTemplate, address(pilTemplate)); + assertEq(licenseTermsId, testLicensesData[0].licenseTermsId); + } + + // Register IP → Attach license terms → Add new IP to group IPA + function test_GroupingWorkflows_registerIpAndAttachLicenseAndAddToGroup_DEPR() public { + // mint a NFT from the mock ERC721 contract + vm.startPrank(minter); + uint256 tokenId = MockERC721(mockNft).mint(minter); + vm.stopPrank(); + + // get the expected IP ID + address expectedIpId = IIPAssetRegistry(ipAssetRegistry).ipId(block.chainid, address(mockNft), tokenId); + + uint256 deadline = block.timestamp + 1000; + + // Get the signature for setting the permission for calling `setAll` (IP metadata) and `attachLicenseTerms` + // functions in `coreMetadataModule` and `licensingModule` from the IP owner + (bytes memory sigMetadataAndAttach, , ) = _getSetBatchPermissionSigForPeriphery({ + ipId: expectedIpId, + permissionList: _getMetadataAndAttachTermsAndConfigPermissionList(expectedIpId, address(groupingWorkflows)), + deadline: deadline, + state: bytes32(0), + signerSk: minterSk + }); + + // Get the signature for setting the permission for calling `addIp` function in `GroupingModule` + // from the Group IP owner + (bytes memory sigAddToGroup, , ) = _getSetPermissionSigForPeriphery({ + ipId: groupId, + to: address(groupingWorkflows), + module: address(groupingModule), + selector: IGroupingModule.addIp.selector, + deadline: deadline, + state: IIPAccount(payable(groupId)).state(), + signerSk: groupOwnerSk + }); + + address ipId = groupingWorkflows.registerIpAndAttachLicenseAndAddToGroup({ + nftContract: address(mockNft), + tokenId: tokenId, + groupId: groupId, + licenseTemplate: address(pilTemplate), + licenseTermsId: testLicensesData[0].licenseTermsId, + ipMetadata: ipMetadataDefault, + sigMetadataAndAttachAndConfig: WorkflowStructs.SignatureData({ + signer: minter, + deadline: deadline, + signature: sigMetadataAndAttach + }), + sigAddToGroup: WorkflowStructs.SignatureData({ + signer: groupOwner, + deadline: deadline, + signature: sigAddToGroup + }) + }); + + // check the IP id matches the expected IP id + assertEq(ipId, expectedIpId); + + // check the IP is registered + assertTrue(IIPAssetRegistry(ipAssetRegistry).isRegistered(ipId)); + + // check the IP is added to the group + assertTrue(IGroupIPAssetRegistry(ipAssetRegistry).containsIp(groupId, ipId)); + + // check the IP metadata is correctly set + assertMetadata(ipId, ipMetadataDefault); + + // check the license terms is correctly attached + (address licenseTemplate, uint256 licenseTermsId) = ILicenseRegistry(licenseRegistry).getAttachedLicenseTerms( + ipId, + 0 + ); + assertEq(licenseTemplate, address(pilTemplate)); + assertEq(licenseTermsId, testLicensesData[0].licenseTermsId); + } + + // Register group IP → Attach license terms to group IPA + function test_GroupingWorkflows_registerGroupAndAttachLicense_DEPR() public { + vm.startPrank(groupOwner); + address newGroupId = groupingWorkflows.registerGroupAndAttachLicense({ + groupPool: address(evenSplitGroupPool), + licenseTemplate: address(pilTemplate), + licenseTermsId: testLicensesData[0].licenseTermsId + }); + vm.stopPrank(); + + // check the group IPA is registered + assertTrue(IGroupIPAssetRegistry(ipAssetRegistry).isRegisteredGroup(newGroupId)); + + // check the license terms is correctly attached to the group IPA + (address licenseTemplate, uint256 licenseTermsId) = ILicenseRegistry(licenseRegistry).getAttachedLicenseTerms( + newGroupId, + 0 + ); + assertEq(licenseTemplate, address(pilTemplate)); + assertEq(licenseTermsId, testLicensesData[0].licenseTermsId); + } + + // Register group IP → Attach license terms to group IPA → Add existing IPs to the new group IPA + function test_GroupingWorkflows_registerGroupAndAttachLicenseAndAddIps_DEPR() public { + vm.startPrank(groupOwner); + address newGroupId = groupingWorkflows.registerGroupAndAttachLicenseAndAddIps({ + groupPool: address(evenSplitGroupPool), + ipIds: ipIds, + licenseTemplate: address(pilTemplate), + licenseTermsId: testLicensesData[0].licenseTermsId + }); + vm.stopPrank(); + + // check the group IPA is registered + assertTrue(IGroupIPAssetRegistry(ipAssetRegistry).isRegisteredGroup(newGroupId)); + + // check all the individual IPs are added to the new group + assertEq(IGroupIPAssetRegistry(ipAssetRegistry).totalMembers(newGroupId), ipIds.length); + for (uint256 i = 0; i < ipIds.length; i++) { + assertTrue(IGroupIPAssetRegistry(ipAssetRegistry).containsIp(newGroupId, ipIds[i])); + } + + // check the license terms is correctly attached to the group IPA + (address licenseTemplate, uint256 licenseTermsId) = ILicenseRegistry(licenseRegistry).getAttachedLicenseTerms( + newGroupId, + 0 + ); + assertEq(licenseTemplate, address(pilTemplate)); + assertEq(licenseTermsId, testLicensesData[0].licenseTermsId); + } + + // Collect royalties for the entire group and distribute to each member IP's royalty vault + function test_GroupingWorkflows_collectRoyaltiesAndClaimReward_DEPR() public { + address ipOwner1 = u.bob; + address ipOwner2 = u.carl; + + vm.startPrank(groupOwner); + address newGroupId = groupingWorkflows.registerGroupAndAttachLicenseAndAddIps({ + groupPool: address(evenSplitGroupPool), + ipIds: ipIds, + licenseTemplate: address(pilTemplate), + licenseTermsId: testLicensesData[0].licenseTermsId + }); + vm.stopPrank(); + + assertEq(ipAssetRegistry.totalMembers(newGroupId), 10); + assertEq(evenSplitGroupPool.getTotalIps(newGroupId), 10); + + address[] memory parentIpIds = new address[](1); + parentIpIds[0] = newGroupId; + uint256[] memory licenseTermsIds = new uint256[](1); + licenseTermsIds[0] = testLicensesData[0].licenseTermsId; + + vm.startPrank(ipOwner1); + // approve nft minting fee + mockToken.mint(ipOwner1, 1 * 10 ** mockToken.decimals()); + mockToken.approve(address(spgNftPublic), 1 * 10 ** mockToken.decimals()); + + (address ipId1, ) = derivativeWorkflows.mintAndRegisterIpAndMakeDerivative({ + spgNftContract: address(spgNftPublic), + derivData: WorkflowStructs.MakeDerivativeDEPR({ + parentIpIds: parentIpIds, + licenseTermsIds: licenseTermsIds, + licenseTemplate: address(pilTemplate), + royaltyContext: "" + }), + ipMetadata: ipMetadataDefault, + recipient: ipOwner1 + }); + vm.stopPrank(); + + vm.startPrank(ipOwner2); + // approve nft minting fee + mockToken.mint(ipOwner2, 1 * 10 ** mockToken.decimals()); + mockToken.approve(address(spgNftPublic), 1 * 10 ** mockToken.decimals()); + + (address ipId2, ) = derivativeWorkflows.mintAndRegisterIpAndMakeDerivative({ + spgNftContract: address(spgNftPublic), + derivData: WorkflowStructs.MakeDerivativeDEPR({ + parentIpIds: parentIpIds, + licenseTermsIds: licenseTermsIds, + licenseTemplate: address(pilTemplate), + royaltyContext: "" + }), + ipMetadata: ipMetadataDefault, + recipient: ipOwner2 + }); + vm.stopPrank(); + + uint256 amount1 = 1_000 * 10 ** mockToken.decimals(); // 1,000 tokens + mockToken.mint(ipOwner1, amount1); + vm.startPrank(ipOwner1); + mockToken.approve(address(royaltyModule), amount1); + royaltyModule.payRoyaltyOnBehalf(ipId1, ipOwner1, address(mockToken), amount1); + royaltyPolicyLAP.transferToVault(ipId1, newGroupId, address(mockToken)); + vm.stopPrank(); + + uint256 amount2 = 10_000 * 10 ** mockToken.decimals(); // 10,000 tokens + mockToken.mint(ipOwner2, amount2); + vm.startPrank(ipOwner2); + mockToken.approve(address(royaltyModule), amount2); + royaltyModule.payRoyaltyOnBehalf(ipId2, ipOwner2, address(mockToken), amount2); + royaltyPolicyLAP.transferToVault(ipId2, newGroupId, address(mockToken)); + vm.stopPrank(); + + uint256[] memory snapshotIds = new uint256[](2); + address[] memory royaltyTokens = new address[](1); + royaltyTokens[0] = address(mockToken); + + uint256[] memory collectedRoyalties = groupingWorkflows.collectRoyaltiesAndClaimReward( + newGroupId, + royaltyTokens, + snapshotIds, + ipIds + ); + + assertEq(collectedRoyalties.length, 1); + assertEq( + collectedRoyalties[0], + (amount1 * revShare) / royaltyModule.maxPercent() + (amount2 * revShare) / royaltyModule.maxPercent() + ); + + // check each member IP received the reward in their IP royalty vault + for (uint256 i = 0; i < ipIds.length; i++) { + assertEq( + MockERC20(mockToken).balanceOf(royaltyModule.ipRoyaltyVaults(ipIds[i])), + collectedRoyalties[0] / ipIds.length // even split between all member IPs + ); + } + } + + // Revert if currency token contains zero address + function test_GroupingWorkflows_revert_collectRoyaltiesAndClaimReward_zeroAddressParam_DEPR() public { + address[] memory currencyTokens = new address[](1); + currencyTokens[0] = address(0); + + uint256[] memory snapshotIds = new uint256[](1); + snapshotIds[0] = 0; + + vm.expectRevert(Errors.GroupingWorkflows__ZeroAddressParam.selector); + groupingWorkflows.collectRoyaltiesAndClaimReward(groupId, currencyTokens, snapshotIds, ipIds); + } + + // Multicall (mint → Register IP → Attach PIL terms → Add new IP to group IPA) + function test_GroupingWorkflows_multicall_mintAndRegisterIpAndAttachLicenseAndAddToGroup_DEPR() public { + uint256 deadline = block.timestamp + 1000; + + // Get the signatures for setting the permission for calling `addIp` function in `GroupingModule` + // from the Group IP owner + bytes[] memory sigsAddToGroup = new bytes[](10); + bytes32 expectedStates = IIPAccount(payable(groupId)).state(); + for (uint256 i = 0; i < 10; i++) { + (sigsAddToGroup[i], expectedStates, ) = _getSetPermissionSigForPeriphery({ + ipId: groupId, + to: address(groupingWorkflows), + module: address(groupingModule), + selector: IGroupingModule.addIp.selector, + deadline: deadline, + state: expectedStates, + signerSk: groupOwnerSk + }); + } + + // setup call data for batch calling 10 `mintAndRegisterIpAndAttachLicenseAndAddToGroup` + bytes[] memory data = new bytes[](10); + for (uint256 i = 0; i < 10; i++) { + data[i] = abi.encodeWithSelector( + bytes4( + keccak256( + "mintAndRegisterIpAndAttachLicenseAndAddToGroup(address,address,address,address,uint256,(string,bytes32,string,bytes32),(address,uint256,bytes))" + ) + ), + address(spgNftPublic), + groupId, + minter, + pilTemplate, + testLicensesData[0].licenseTermsId, + ipMetadataDefault, + WorkflowStructs.SignatureData({ signer: groupOwner, deadline: deadline, signature: sigsAddToGroup[i] }) + ); + } + + // batch call `mintAndRegisterIpAndAttachLicenseAndAddToGroup` + vm.startPrank(minter); + bytes[] memory results = groupingWorkflows.multicall(data); + vm.stopPrank(); + + // check each IP is registered, added to the group, and metadata is set, license terms are attached + address ipId; + uint256 tokenId; + for (uint256 i = 0; i < 10; i++) { + (ipId, tokenId) = abi.decode(results[i], (address, uint256)); + assertTrue(IIPAssetRegistry(ipAssetRegistry).isRegistered(ipId)); + assertTrue(IGroupIPAssetRegistry(ipAssetRegistry).containsIp(groupId, ipId)); + assertEq(spgNftPublic.tokenURI(tokenId), string.concat(testBaseURI, ipMetadataDefault.nftMetadataURI)); + assertMetadata(ipId, ipMetadataDefault); + (address licenseTemplate, uint256 licenseTermsId) = ILicenseRegistry(licenseRegistry) + .getAttachedLicenseTerms(ipId, 0); + assertEq(licenseTemplate, address(pilTemplate)); + assertEq(licenseTermsId, testLicensesData[0].licenseTermsId); + } + } + + // Multicall (Register IP → Attach PIL terms → Add new IP to group IPA) + function test_GroupingWorkflows_multicall_registerIpAndAttachLicenseAndAddToGroup_DEPR() public { + // mint a NFT from the mock ERC721 contract + uint256[] memory tokenIds = new uint256[](10); + vm.startPrank(minter); + for (uint256 i = 0; i < 10; i++) { + tokenIds[i] = MockERC721(mockNft).mint(minter); + } + vm.stopPrank(); + + // get the expected IP ID + address[] memory expectedIpIds = new address[](10); + for (uint256 i = 0; i < 10; i++) { + expectedIpIds[i] = IIPAssetRegistry(ipAssetRegistry).ipId(block.chainid, address(mockNft), tokenIds[i]); + } + + uint256 deadline = block.timestamp + 10000; + + // Get the signatures for setting the permission for calling `setAll` (IP metadata) and `attachLicenseTerms` + // functions in `coreMetadataModule` and `licensingModule` from the IP owner + bytes[] memory sigsMetadataAndAttach = new bytes[](10); + for (uint256 i = 0; i < 10; i++) { + (sigsMetadataAndAttach[i], , ) = _getSetBatchPermissionSigForPeriphery({ + ipId: expectedIpIds[i], + permissionList: _getMetadataAndAttachTermsAndConfigPermissionList( + expectedIpIds[i], + address(groupingWorkflows) + ), + deadline: deadline, + state: bytes32(0), + signerSk: minterSk + }); + } + + // Get the signatures for setting the permission for calling `addIp` function in `GroupingModule` + // from the Group IP owner + bytes[] memory sigsAddToGroup = new bytes[](10); + bytes32 expectedStates = IIPAccount(payable(groupId)).state(); + for (uint256 i = 0; i < 10; i++) { + (sigsAddToGroup[i], expectedStates, ) = _getSetPermissionSigForPeriphery({ + ipId: groupId, + to: address(groupingWorkflows), + module: address(groupingModule), + selector: IGroupingModule.addIp.selector, + deadline: deadline, + state: expectedStates, + signerSk: groupOwnerSk + }); + } + + // setup call data for batch calling 10 `registerIpAndAttachLicenseAndAddToGroup` + bytes[] memory data = new bytes[](10); + for (uint256 i = 0; i < 10; i++) { + data[i] = abi.encodeWithSelector( + bytes4( + keccak256( + "registerIpAndAttachLicenseAndAddToGroup(address,uint256,address,address,uint256,(string,bytes32,string,bytes32),(address,uint256,bytes),(address,uint256,bytes))" + ) + ), + mockNft, + tokenIds[i], + groupId, + pilTemplate, + testLicensesData[0].licenseTermsId, + ipMetadataDefault, + WorkflowStructs.SignatureData({ + signer: minter, + deadline: deadline, + signature: sigsMetadataAndAttach[i] + }), + WorkflowStructs.SignatureData({ signer: groupOwner, deadline: deadline, signature: sigsAddToGroup[i] }) + ); + } + + // batch call `registerIpAndAttachLicenseAndAddToGroup` + vm.startPrank(minter); + bytes[] memory results = groupingWorkflows.multicall(data); + vm.stopPrank(); + + // check each IP is registered, added to the group, and metadata is set, license terms are attached + address ipId; + for (uint256 i = 0; i < 10; i++) { + ipId = abi.decode(results[i], (address)); + assertEq(ipId, expectedIpIds[i]); + assertTrue(IIPAssetRegistry(ipAssetRegistry).isRegistered(ipId)); + assertTrue(IGroupIPAssetRegistry(ipAssetRegistry).containsIp(groupId, ipId)); + assertMetadata(ipId, ipMetadataDefault); + (address licenseTemplate, uint256 licenseTermsId) = ILicenseRegistry(licenseRegistry) + .getAttachedLicenseTerms(ipId, 0); + assertEq(licenseTemplate, address(pilTemplate)); + assertEq(licenseTermsId, testLicensesData[0].licenseTermsId); + } + } }