diff --git a/contracts/interfaces/workflows/IRoyaltyTokenDistributionWorkflows.sol b/contracts/interfaces/workflows/IRoyaltyTokenDistributionWorkflows.sol index 7125288..3882966 100644 --- a/contracts/interfaces/workflows/IRoyaltyTokenDistributionWorkflows.sol +++ b/contracts/interfaces/workflows/IRoyaltyTokenDistributionWorkflows.sol @@ -17,15 +17,15 @@ interface IRoyaltyTokenDistributionWorkflows { /// @param allowDuplicates Set to true to allow minting an NFT with a duplicate metadata hash. /// @return ipId The ID of the registered IP. /// @return tokenId The ID of the minted NFT. - /// @return licenseTermsId The ID of the attached PIL terms. + /// @return licenseTermsIds The IDs of the attached PIL terms. function mintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokens( address spgNftContract, address recipient, WorkflowStructs.IPMetadata calldata ipMetadata, - WorkflowStructs.LicenseTermsData calldata licenseTermsData, + WorkflowStructs.LicenseTermsData[] calldata licenseTermsData, WorkflowStructs.RoyaltyShare[] calldata royaltyShares, bool allowDuplicates - ) external returns (address ipId, uint256 tokenId, uint256 licenseTermsId); + ) external returns (address ipId, uint256 tokenId, uint256[] memory licenseTermsIds); /// @notice Mint an NFT and register the IP, make a derivative, and distribute royalty tokens. /// @param spgNftContract The address of the SPG NFT contract. @@ -53,15 +53,15 @@ interface IRoyaltyTokenDistributionWorkflows { /// @param sigMetadataAndAttachAndConfig Signature data for setAll (metadata), attachLicenseTerms, and /// setLicensingConfig to the IP via the Core Metadata Module and Licensing Module. /// @return ipId The ID of the registered IP. - /// @return licenseTermsId The ID of the attached PIL terms. + /// @return licenseTermsIds The IDs of the attached PIL terms. /// @return ipRoyaltyVault The address of the deployed royalty vault. function registerIpAndAttachPILTermsAndDeployRoyaltyVault( address nftContract, uint256 tokenId, WorkflowStructs.IPMetadata calldata ipMetadata, - WorkflowStructs.LicenseTermsData calldata licenseTermsData, + WorkflowStructs.LicenseTermsData[] calldata licenseTermsData, WorkflowStructs.SignatureData calldata sigMetadataAndAttachAndConfig - ) external returns (address ipId, uint256 licenseTermsId, address ipRoyaltyVault); + ) external returns (address ipId, uint256[] memory licenseTermsIds, address ipRoyaltyVault); /// @notice Register an IP, make a derivative, and deploy a royalty vault. /// @param nftContract The address of the NFT contract. @@ -114,5 +114,4 @@ interface IRoyaltyTokenDistributionWorkflows { WorkflowStructs.SignatureData calldata sigMetadata, WorkflowStructs.SignatureData calldata sigAttach ) external returns (address ipId, uint256[] memory licenseTermsIds, address ipRoyaltyVault); - } diff --git a/contracts/lib/Errors.sol b/contracts/lib/Errors.sol index a059fe7..f477c33 100644 --- a/contracts/lib/Errors.sol +++ b/contracts/lib/Errors.sol @@ -68,6 +68,9 @@ library Errors { /// @notice Royalty vault not deployed. error RoyaltyTokenDistributionWorkflows__RoyaltyVaultNotDeployed(); + /// @notice License terms data list is empty. + error RoyaltyTokenDistributionWorkflows__NoLicenseTermsData(); + //////////////////////////////////////////////////////////////////////////// // SPGNFT // //////////////////////////////////////////////////////////////////////////// @@ -141,10 +144,4 @@ library Errors { /// @param ipId The address of the already tokenized IP /// @param token The address of the fractionalized token for the IP error TokenizerModule__IpAlreadyTokenized(address ipId, address token); - - //////////////////////////////////////////////////////////////////////////// - // DEPRECATED, WILL BE REMOVED IN V1.4 // - //////////////////////////////////////////////////////////////////////////// - - error RoyaltyTokenDistributionWorkflows__TotalPercentagesExceeds100Percent(); } diff --git a/contracts/lib/LicensingHelper.sol b/contracts/lib/LicensingHelper.sol index 2c0f607..e4919da 100644 --- a/contracts/lib/LicensingHelper.sol +++ b/contracts/lib/LicensingHelper.sol @@ -17,6 +17,27 @@ import { WorkflowStructs } from "./WorkflowStructs.sol"; library LicensingHelper { using SafeERC20 for IERC20; + /// @notice Registers multiple PIL terms and attaches them to the given IP and sets their licensing configurations. + /// @param ipId The ID of the IP. + /// @param licenseTermsData The PIL terms and licensing configuration data to be attached to the IP. + /// @return licenseTermsIds The IDs of the newly registered PIL terms. + function registerMultiplePILTermsAndAttachAndSetConfigs( + address ipId, + address pilTemplate, + address licensingModule, + WorkflowStructs.LicenseTermsData[] calldata licenseTermsData + ) internal returns (uint256[] memory licenseTermsIds) { + licenseTermsIds = new uint256[](licenseTermsData.length); + for (uint256 i; i < licenseTermsData.length; i++) { + licenseTermsIds[i] = LicensingHelper.registerPILTermsAndAttachAndSetConfigs( + ipId, + pilTemplate, + licensingModule, + licenseTermsData[i] + ); + } + } + /// @dev Registers PIL License Terms and attaches them to the given IP. /// @param ipId The ID of the IP. /// @param pilTemplate The address of the PIL License Template. diff --git a/contracts/workflows/LicenseAttachmentWorkflows.sol b/contracts/workflows/LicenseAttachmentWorkflows.sol index 0f8fecd..4b91c14 100644 --- a/contracts/workflows/LicenseAttachmentWorkflows.sol +++ b/contracts/workflows/LicenseAttachmentWorkflows.sol @@ -116,7 +116,12 @@ contract LicenseAttachmentWorkflows is sigData: sigAttachAndConfig }); - licenseTermsIds = _registerMultiplePILTermsAndAttachAndSetConfigs(ipId, licenseTermsData); + licenseTermsIds = LicensingHelper.registerMultiplePILTermsAndAttachAndSetConfigs({ + ipId: ipId, + pilTemplate: address(PIL_TEMPLATE), + licensingModule: address(LICENSING_MODULE), + licenseTermsData: licenseTermsData + }); } /// @notice Mint an NFT from a SPGNFT collection, register it with metadata as an IP, @@ -154,7 +159,12 @@ contract LicenseAttachmentWorkflows is ipId = IP_ASSET_REGISTRY.register(block.chainid, spgNftContract, tokenId); MetadataHelper.setMetadata(ipId, address(CORE_METADATA_MODULE), ipMetadata); - licenseTermsIds = _registerMultiplePILTermsAndAttachAndSetConfigs(ipId, licenseTermsData); + licenseTermsIds = LicensingHelper.registerMultiplePILTermsAndAttachAndSetConfigs({ + ipId: ipId, + pilTemplate: address(PIL_TEMPLATE), + licensingModule: address(LICENSING_MODULE), + licenseTermsData: licenseTermsData + }); ISPGNFT(spgNftContract).safeTransferFrom(address(this), recipient, tokenId, ""); } @@ -199,26 +209,12 @@ contract LicenseAttachmentWorkflows is MetadataHelper.setMetadata(ipId, address(CORE_METADATA_MODULE), ipMetadata); - licenseTermsIds = _registerMultiplePILTermsAndAttachAndSetConfigs(ipId, licenseTermsData); - } - - /// @notice Registers multiple PIL terms and attaches them to the given IP and sets their licensing configurations. - /// @param ipId The ID of the IP. - /// @param licenseTermsData The PIL terms and licensing configuration data to be attached to the IP. - /// @return licenseTermsIds The IDs of the newly registered PIL terms. - function _registerMultiplePILTermsAndAttachAndSetConfigs( - address ipId, - WorkflowStructs.LicenseTermsData[] calldata licenseTermsData - ) private returns (uint256[] memory licenseTermsIds) { - licenseTermsIds = new uint256[](licenseTermsData.length); - for (uint256 i; i < licenseTermsData.length; i++) { - licenseTermsIds[i] = LicensingHelper.registerPILTermsAndAttachAndSetConfigs( - ipId, - address(PIL_TEMPLATE), - address(LICENSING_MODULE), - licenseTermsData[i] - ); - } + licenseTermsIds = LicensingHelper.registerMultiplePILTermsAndAttachAndSetConfigs({ + ipId: ipId, + pilTemplate: address(PIL_TEMPLATE), + licensingModule: address(LICENSING_MODULE), + licenseTermsData: licenseTermsData + }); } // diff --git a/contracts/workflows/RoyaltyTokenDistributionWorkflows.sol b/contracts/workflows/RoyaltyTokenDistributionWorkflows.sol index 2666855..c496a08 100644 --- a/contracts/workflows/RoyaltyTokenDistributionWorkflows.sol +++ b/contracts/workflows/RoyaltyTokenDistributionWorkflows.sol @@ -126,15 +126,21 @@ contract RoyaltyTokenDistributionWorkflows is /// @param allowDuplicates Set to true to allow minting an NFT with a duplicate metadata hash. /// @return ipId The ID of the registered IP. /// @return tokenId The ID of the minted NFT. - /// @return licenseTermsId The ID of the attached PIL terms. + /// @return licenseTermsIds The IDs of the attached PIL terms. function mintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokens( address spgNftContract, address recipient, WorkflowStructs.IPMetadata calldata ipMetadata, - WorkflowStructs.LicenseTermsData calldata licenseTermsData, + WorkflowStructs.LicenseTermsData[] calldata licenseTermsData, WorkflowStructs.RoyaltyShare[] calldata royaltyShares, bool allowDuplicates - ) external onlyMintAuthorized(spgNftContract) returns (address ipId, uint256 tokenId, uint256 licenseTermsId) { + ) + external + onlyMintAuthorized(spgNftContract) + returns (address ipId, uint256 tokenId, uint256[] memory licenseTermsIds) + { + if (licenseTermsData.length == 0) revert Errors.RoyaltyTokenDistributionWorkflows__NoLicenseTermsData(); + tokenId = ISPGNFT(spgNftContract).mintByPeriphery({ to: address(this), payer: msg.sender, @@ -146,7 +152,7 @@ contract RoyaltyTokenDistributionWorkflows is ipId = IP_ASSET_REGISTRY.register(block.chainid, spgNftContract, tokenId); MetadataHelper.setMetadata(ipId, address(CORE_METADATA_MODULE), ipMetadata); - licenseTermsId = LicensingHelper.registerPILTermsAndAttachAndSetConfigs({ + licenseTermsIds = LicensingHelper.registerMultiplePILTermsAndAttachAndSetConfigs({ ipId: ipId, pilTemplate: address(PIL_TEMPLATE), licensingModule: address(LICENSING_MODULE), @@ -217,15 +223,17 @@ contract RoyaltyTokenDistributionWorkflows is /// @param sigMetadataAndAttachAndConfig Signature data for setAll (metadata), attachLicenseTerms, and /// setLicensingConfig to the IP via the Core Metadata Module and Licensing Module. /// @return ipId The ID of the registered IP. - /// @return licenseTermsId The ID of the attached PIL terms. + /// @return licenseTermsIds The IDs of the attached PIL terms. /// @return ipRoyaltyVault The address of the deployed royalty vault. function registerIpAndAttachPILTermsAndDeployRoyaltyVault( address nftContract, uint256 tokenId, WorkflowStructs.IPMetadata calldata ipMetadata, - WorkflowStructs.LicenseTermsData calldata licenseTermsData, + WorkflowStructs.LicenseTermsData[] calldata licenseTermsData, WorkflowStructs.SignatureData calldata sigMetadataAndAttachAndConfig - ) external returns (address ipId, uint256 licenseTermsId, address ipRoyaltyVault) { + ) external returns (address ipId, uint256[] memory licenseTermsIds, address ipRoyaltyVault) { + if (licenseTermsData.length == 0) revert Errors.RoyaltyTokenDistributionWorkflows__NoLicenseTermsData(); + ipId = IP_ASSET_REGISTRY.register(block.chainid, nftContract, tokenId); address[] memory modules = new address[](3); @@ -246,7 +254,7 @@ contract RoyaltyTokenDistributionWorkflows is MetadataHelper.setMetadata(ipId, address(CORE_METADATA_MODULE), ipMetadata); - licenseTermsId = LicensingHelper.registerPILTermsAndAttachAndSetConfigs({ + licenseTermsIds = LicensingHelper.registerMultiplePILTermsAndAttachAndSetConfigs({ ipId: ipId, pilTemplate: address(PIL_TEMPLATE), licensingModule: address(LICENSING_MODULE), diff --git a/test/workflows/RoyaltyTokenDistributionWorkflows.t.sol b/test/workflows/RoyaltyTokenDistributionWorkflows.t.sol index f75d6fc..3371ff1 100644 --- a/test/workflows/RoyaltyTokenDistributionWorkflows.t.sol +++ b/test/workflows/RoyaltyTokenDistributionWorkflows.t.sol @@ -10,8 +10,6 @@ import { Licensing } from "@storyprotocol/core/lib/Licensing.sol"; import { MetaTx } from "@storyprotocol/core/lib/MetaTx.sol"; import { PILFlavors } from "@storyprotocol/core/lib/PILFlavors.sol"; import { PILTerms } from "@storyprotocol/core/interfaces/modules/licensing/IPILicenseTemplate.sol"; -import { ILicensingModule } from "@storyprotocol/core/interfaces/modules/licensing/ILicensingModule.sol"; -import { ICoreMetadataModule } from "@storyprotocol/core/interfaces/modules/metadata/ICoreMetadataModule.sol"; // contracts import { Errors } from "../../contracts/lib/Errors.sol"; @@ -27,7 +25,7 @@ contract RoyaltyTokenDistributionWorkflowsTest is BaseTest { uint256 private nftMintingFee; uint256 private licenseMintingFee; - WorkflowStructs.LicenseTermsData private commRemixTermsData; + WorkflowStructs.LicenseTermsData[] private commRemixTermsData; WorkflowStructs.RoyaltyShare[] private royaltyShares; WorkflowStructs.MakeDerivative private derivativeData; @@ -49,7 +47,7 @@ contract RoyaltyTokenDistributionWorkflowsTest is BaseTest { mockToken.mint(u.alice, nftMintingFee); mockToken.approve(address(spgNftPublic), nftMintingFee); - (address ipId, uint256 tokenId, uint256 licenseTermsId) = royaltyTokenDistributionWorkflows + (address ipId, uint256 tokenId, uint256[] memory licenseTermsIds) = royaltyTokenDistributionWorkflows .mintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokens({ spgNftContract: address(spgNftPublic), recipient: u.alice, @@ -64,7 +62,7 @@ contract RoyaltyTokenDistributionWorkflowsTest is BaseTest { assertEq(tokenId, 3); assertEq(spgNftPublic.tokenURI(tokenId), string.concat(testBaseURI, ipMetadataDefault.nftMetadataURI)); assertMetadata(ipId, ipMetadataDefault); - _assertAttachedLicenseTerms(ipId, licenseTermsId); + _assertAttachedLicenseTerms(ipId, licenseTermsIds); _assertRoyaltyTokenDistribution(ipId); } @@ -125,7 +123,7 @@ contract RoyaltyTokenDistributionWorkflowsTest is BaseTest { // register IP, attach PIL terms, and deploy royalty vault vm.startPrank(u.alice); - (address ipId, uint256 licenseTermsId, address ipRoyaltyVault) = royaltyTokenDistributionWorkflows + (address ipId, uint256[] memory licenseTermsIds, address ipRoyaltyVault) = royaltyTokenDistributionWorkflows .registerIpAndAttachPILTermsAndDeployRoyaltyVault({ nftContract: address(mockNft), tokenId: tokenId, @@ -167,7 +165,7 @@ contract RoyaltyTokenDistributionWorkflowsTest is BaseTest { assertTrue(ipAssetRegistry.isRegistered(ipId)); assertMetadata(ipId, ipMetadataDefault); - _assertAttachedLicenseTerms(ipId, licenseTermsId); + _assertAttachedLicenseTerms(ipId, licenseTermsIds); _assertRoyaltyTokenDistribution(ipId); } @@ -286,41 +284,86 @@ contract RoyaltyTokenDistributionWorkflowsTest is BaseTest { uint32 testCommRevShare = 5 * 10 ** 6; // 5% - commRemixTermsData = WorkflowStructs.LicenseTermsData({ - terms: PILFlavors.commercialRemix({ - mintingFee: licenseMintingFee, - commercialRevShare: testCommRevShare, - royaltyPolicy: address(royaltyPolicyLAP), - currencyToken: address(mockToken) - }), - licensingConfig: Licensing.LicensingConfig({ - isSet: true, - mintingFee: licenseMintingFee, - licensingHook: address(0), - hookData: "", - commercialRevShare: testCommRevShare, // 5% - disabled: false, - expectMinimumGroupRewardShare: 0, - expectGroupRewardPool: address(evenSplitGroupPool) + commRemixTermsData.push( + WorkflowStructs.LicenseTermsData({ + terms: PILFlavors.commercialRemix({ + mintingFee: licenseMintingFee, + commercialRevShare: testCommRevShare, + royaltyPolicy: address(royaltyPolicyLAP), + currencyToken: address(mockToken) + }), + licensingConfig: Licensing.LicensingConfig({ + isSet: true, + mintingFee: licenseMintingFee, + licensingHook: address(0), + hookData: "", + commercialRevShare: testCommRevShare, // 5% + disabled: false, + expectMinimumGroupRewardShare: 0, + expectGroupRewardPool: address(evenSplitGroupPool) + }) }) - }); + ); + + commRemixTermsData.push( + WorkflowStructs.LicenseTermsData({ + terms: PILFlavors.commercialRemix({ + mintingFee: licenseMintingFee, + commercialRevShare: 5_000_000, // 5% + royaltyPolicy: address(royaltyPolicyLRP), + currencyToken: address(mockToken) + }), + licensingConfig: Licensing.LicensingConfig({ + isSet: true, + mintingFee: licenseMintingFee, + licensingHook: address(0), + hookData: "", + commercialRevShare: 5_000_000, // 5% + disabled: false, + expectMinimumGroupRewardShare: 0, + expectGroupRewardPool: address(evenSplitGroupPool) + }) + }) + ); + + commRemixTermsData.push( + WorkflowStructs.LicenseTermsData({ + terms: PILFlavors.commercialRemix({ + mintingFee: licenseMintingFee, + commercialRevShare: 8_000_000, // 8% + royaltyPolicy: address(royaltyPolicyLAP), + currencyToken: address(mockToken) + }), + licensingConfig: Licensing.LicensingConfig({ + isSet: true, + mintingFee: licenseMintingFee, + licensingHook: address(0), + hookData: "", + commercialRevShare: 8_000_000, // 8% + disabled: false, + expectMinimumGroupRewardShare: 0, + expectGroupRewardPool: address(evenSplitGroupPool) + }) + }) + ); - WorkflowStructs.LicenseTermsData[] memory licenseTermsDataParent = new WorkflowStructs.LicenseTermsData[](1); - licenseTermsDataParent[0] = commRemixTermsData; address[] memory ipIdParent = new address[](1); - uint256[] memory licenseTermsIdsParent; + uint256[] memory licenseTermsIds; vm.startPrank(u.alice); mockToken.mint(u.alice, licenseMintingFee); mockToken.approve(address(spgNftPublic), licenseMintingFee); - (ipIdParent[0], , licenseTermsIdsParent) = licenseAttachmentWorkflows.mintAndRegisterIpAndAttachPILTerms({ + (ipIdParent[0], , licenseTermsIds) = licenseAttachmentWorkflows.mintAndRegisterIpAndAttachPILTerms({ spgNftContract: address(spgNftPublic), recipient: u.alice, ipMetadata: ipMetadataDefault, - licenseTermsData: licenseTermsDataParent, + licenseTermsData: commRemixTermsData, allowDuplicates: true }); vm.stopPrank(); + uint256[] memory licenseTermsIdsParent = new uint256[](1); + licenseTermsIdsParent[0] = licenseTermsIds[0]; + derivativeData = WorkflowStructs.MakeDerivative({ parentIpIds: ipIdParent, licenseTemplate: address(pilTemplate), @@ -360,27 +403,36 @@ contract RoyaltyTokenDistributionWorkflowsTest is BaseTest { ); } - function _assertAttachedLicenseTerms(address ipId, uint256 licenseTermsId) private { - (address licenseTemplate, uint256 licenseTermsIdAttached) = licenseRegistry.getAttachedLicenseTerms(ipId, 0); - assertEq(licenseTermsId, licenseTermsIdAttached); - assertEq(licenseTemplate, address(pilTemplate)); - assertEq(licenseTermsIdAttached, pilTemplate.getLicenseTermsId(commRemixTermsData.terms)); - Licensing.LicensingConfig memory licensingConfig = licenseRegistry.getLicensingConfig( - ipId, - licenseTemplate, - licenseTermsIdAttached - ); - assertEq(licensingConfig.isSet, commRemixTermsData.licensingConfig.isSet); - assertEq(licensingConfig.mintingFee, commRemixTermsData.licensingConfig.mintingFee); - assertEq(licensingConfig.licensingHook, commRemixTermsData.licensingConfig.licensingHook); - assertEq(licensingConfig.hookData, commRemixTermsData.licensingConfig.hookData); - assertEq(licensingConfig.commercialRevShare, commRemixTermsData.licensingConfig.commercialRevShare); - assertEq(licensingConfig.disabled, commRemixTermsData.licensingConfig.disabled); - assertEq(licensingConfig.expectGroupRewardPool, commRemixTermsData.licensingConfig.expectGroupRewardPool); - assertEq( - licensingConfig.expectMinimumGroupRewardShare, - commRemixTermsData.licensingConfig.expectMinimumGroupRewardShare - ); + function _assertAttachedLicenseTerms(address ipId, uint256[] memory licenseTermsIds) private { + for (uint256 i = 0; i < commRemixTermsData.length; i++) { + (address licenseTemplate, uint256 licenseTermsIdAttached) = licenseRegistry.getAttachedLicenseTerms( + ipId, + i + ); + assertEq(licenseTermsIds[i], licenseTermsIdAttached); + assertEq(licenseTemplate, address(pilTemplate)); + assertEq(licenseTermsIdAttached, licenseTermsIds[i]); + assertEq(licenseTermsIdAttached, pilTemplate.getLicenseTermsId(commRemixTermsData[i].terms)); + Licensing.LicensingConfig memory licensingConfig = licenseRegistry.getLicensingConfig( + ipId, + licenseTemplate, + licenseTermsIdAttached + ); + assertEq(licensingConfig.isSet, commRemixTermsData[i].licensingConfig.isSet); + assertEq(licensingConfig.mintingFee, commRemixTermsData[i].licensingConfig.mintingFee); + assertEq(licensingConfig.licensingHook, commRemixTermsData[i].licensingConfig.licensingHook); + assertEq(licensingConfig.hookData, commRemixTermsData[i].licensingConfig.hookData); + assertEq(licensingConfig.commercialRevShare, commRemixTermsData[i].licensingConfig.commercialRevShare); + assertEq(licensingConfig.disabled, commRemixTermsData[i].licensingConfig.disabled); + assertEq( + licensingConfig.expectGroupRewardPool, + commRemixTermsData[i].licensingConfig.expectGroupRewardPool + ); + assertEq( + licensingConfig.expectMinimumGroupRewardShare, + commRemixTermsData[i].licensingConfig.expectMinimumGroupRewardShare + ); + } } /// @dev Assert that the royalty tokens have been distributed correctly.