diff --git a/.github/workflows/foundry_ci.yml b/.github/workflows/foundry_ci.yml index 9fec3ac..02e4f08 100644 --- a/.github/workflows/foundry_ci.yml +++ b/.github/workflows/foundry_ci.yml @@ -22,7 +22,7 @@ jobs: with: submodules: recursive fetch-depth: 0 - + - name: Run install uses: borales/actions-yarn@v4 with: diff --git a/contracts/SPGNFT.sol b/contracts/SPGNFT.sol index cb24395..dcd3fd5 100644 --- a/contracts/SPGNFT.sol +++ b/contracts/SPGNFT.sol @@ -6,12 +6,15 @@ import { AccessControlUpgradeable } from "@openzeppelin/contracts-upgradeable/ac import { ERC721URIStorageUpgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721URIStorageUpgradeable.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { ISPGNFT } from "./interfaces/ISPGNFT.sol"; import { Errors } from "./lib/Errors.sol"; import { SPGNFTLib } from "./lib/SPGNFTLib.sol"; contract SPGNFT is ISPGNFT, ERC721URIStorageUpgradeable, AccessControlUpgradeable { + using SafeERC20 for IERC20; + /// @dev Storage structure for the SPGNFTSotrage. /// @param _maxSupply The maximum supply of the collection. /// @param _totalSupply The total minted supply of the collection. @@ -117,6 +120,7 @@ contract SPGNFT is ISPGNFT, ERC721URIStorageUpgradeable, AccessControlUpgradeabl $._contractURI = initParams.contractURI; __ERC721_init(initParams.name, initParams.symbol); + __AccessControl_init(); } /// @notice Returns the total minted supply of the collection. @@ -187,8 +191,8 @@ contract SPGNFT is ISPGNFT, ERC721URIStorageUpgradeable, AccessControlUpgradeabl /// @dev Only callable by the fee recipient. /// @param newFeeRecipient The new fee recipient. function setMintFeeRecipient(address newFeeRecipient) external { - if (msg.sender != _getSPGNFTStorage()._mintFeeRecipient) { - revert Errors.SPGNFT__CallerNotFeeRecipient(); + if (msg.sender != _getSPGNFTStorage()._mintFeeRecipient && !hasRole(SPGNFTLib.ADMIN_ROLE, msg.sender)) { + revert Errors.SPGNFT__CallerNotFeeRecipientOrAdmin(); } _getSPGNFTStorage()._mintFeeRecipient = newFeeRecipient; } @@ -225,7 +229,7 @@ contract SPGNFT is ISPGNFT, ERC721URIStorageUpgradeable, AccessControlUpgradeabl emit ContractURIUpdated(); } - /// @notice Mints an NFT from the collection. Only callable by the minter role. + /// @notice Mints an NFT from the collection. Only callable when public minting is enabled or when the caller has minter role. /// @param to The address of the recipient of the minted NFT. /// @param nftMetadataURI OPTIONAL. The URI of the desired metadata for the newly minted NFT. /// @param nftMetadataHash OPTIONAL. A bytes32 hash of the NFT's metadata. This metadata is accessible via the NFT's tokenURI. @@ -275,7 +279,7 @@ contract SPGNFT is ISPGNFT, ERC721URIStorageUpgradeable, AccessControlUpgradeabl /// @dev Withdraws the contract's token balance to the fee recipient. /// @param token The token to withdraw. function withdrawToken(address token) public { - IERC20(token).transfer(_getSPGNFTStorage()._mintFeeRecipient, IERC20(token).balanceOf(address(this))); + IERC20(token).safeTransfer(_getSPGNFTStorage()._mintFeeRecipient, IERC20(token).balanceOf(address(this))); } /// @dev Supports ERC165 interface. @@ -314,7 +318,7 @@ contract SPGNFT is ISPGNFT, ERC721URIStorageUpgradeable, AccessControlUpgradeabl } if ($._mintFeeToken != address(0) && $._mintFee > 0) { - IERC20($._mintFeeToken).transferFrom(payer, address(this), $._mintFee); + IERC20($._mintFeeToken).safeTransferFrom(payer, address(this), $._mintFee); } tokenId = ++$._totalSupply; @@ -323,7 +327,7 @@ contract SPGNFT is ISPGNFT, ERC721URIStorageUpgradeable, AccessControlUpgradeabl $._nftMetadataHashToTokenId[nftMetadataHash] = tokenId; } - _mint(to, tokenId); + _safeMint(to, tokenId); if (bytes(nftMetadataURI).length > 0) _setTokenURI(tokenId, nftMetadataURI); } @@ -356,10 +360,6 @@ contract SPGNFT is ISPGNFT, ERC721URIStorageUpgradeable, AccessControlUpgradeabl _grantRole(SPGNFTLib.MINTER_ROLE, ROYALTY_TOKEN_DISTRIBUTION_WORKFLOWS_ADDRESS); } - // - // Upgrade - // - /// @dev Returns the storage struct of SPGNFT. function _getSPGNFTStorage() private pure returns (SPGNFTStorage storage $) { assembly { diff --git a/contracts/hooks/LockLicenseHook.sol b/contracts/hooks/LockLicenseHook.sol index cc698a1..21e2e45 100644 --- a/contracts/hooks/LockLicenseHook.sol +++ b/contracts/hooks/LockLicenseHook.sol @@ -6,7 +6,7 @@ import { BaseModule } from "@storyprotocol/core/modules/BaseModule.sol"; import { ILicensingHook } from "@storyprotocol/core/interfaces/modules/licensing/ILicensingHook.sol"; contract LockLicenseHook is BaseModule, ILicensingHook { - string public constant override name = "LockLicenseHook"; + string public constant override name = "LOCK_LICENSE_HOOK"; /// @notice Emitted when attempting to perform an action on a locked license /// @param licensorIpId The licensor IP id that is locked diff --git a/contracts/hooks/TotalLicenseTokenLimitHook.sol b/contracts/hooks/TotalLicenseTokenLimitHook.sol index 90fce4a..1868a10 100644 --- a/contracts/hooks/TotalLicenseTokenLimitHook.sol +++ b/contracts/hooks/TotalLicenseTokenLimitHook.sol @@ -10,7 +10,7 @@ import { ILicenseToken } from "@storyprotocol/core/interfaces/ILicenseToken.sol" import { ILicenseTemplate } from "@storyprotocol/core/interfaces/modules/licensing/ILicenseTemplate.sol"; contract TotalLicenseTokenLimitHook is BaseModule, AccessControlled, ILicensingHook { - string public constant override name = "TotalLicenseTokenLimitHook"; + string public constant override name = "TOTAL_LICENSE_TOKEN_LIMIT_HOOK"; /// @notice The address of the License Registry. ILicenseRegistry public immutable LICENSE_REGISTRY; @@ -75,7 +75,8 @@ contract TotalLicenseTokenLimitHook is BaseModule, AccessControlled, ILicensingH ) external verifyPermission(licensorIpId) { bytes32 key = keccak256(abi.encodePacked(licensorIpId, licenseTemplate, licenseTermsId)); uint256 totalSupply = _getTotalSupply(licensorIpId); - if (limit < totalSupply) revert TotalLicenseTokenLimitHook_LimitLowerThanTotalSupply(totalSupply, limit); + if (limit != 0 && limit < totalSupply) + revert TotalLicenseTokenLimitHook_LimitLowerThanTotalSupply(totalSupply, limit); totalLicenseTokenLimit[key] = limit; emit SetTotalLicenseTokenLimit(licensorIpId, licenseTemplate, licenseTermsId, limit); } diff --git a/contracts/interfaces/story-nft/IOrgStoryNFTFactory.sol b/contracts/interfaces/story-nft/IOrgStoryNFTFactory.sol index becb129..bed12bf 100644 --- a/contracts/interfaces/story-nft/IOrgStoryNFTFactory.sol +++ b/contracts/interfaces/story-nft/IOrgStoryNFTFactory.sol @@ -78,7 +78,7 @@ interface IOrgStoryNFTFactory { /// @param orgNftRecipient The address of the recipient of the organization NFT. /// @param orgName The name of the organization. /// @param orgIpMetadata OPTIONAL. The desired metadata for the newly minted OrgNFT and registered IP. - /// @param signature The signature from the OrgStoryNFTFactory's whitelist signer. This signautre is generated by + /// @param signature The signature from the OrgStoryNFTFactory's whitelist signer. This signature is generated by /// having the whitelist signer sign the caller's address (msg.sender) for this `deployOrgStoryNft` function. /// @param storyNftInitParams The initialization parameters for StoryNFT {see {IStoryNFT-StoryNftInitParams}}. /// @return orgNft The address of the organization NFT. diff --git a/contracts/interfaces/story-nft/IStoryBadgeNFT.sol b/contracts/interfaces/story-nft/IStoryBadgeNFT.sol index 225971b..5e98338 100644 --- a/contracts/interfaces/story-nft/IStoryBadgeNFT.sol +++ b/contracts/interfaces/story-nft/IStoryBadgeNFT.sol @@ -60,7 +60,7 @@ interface IStoryBadgeNFT is IStoryNFT, IERC721Metadata, IERC5192 { /// @notice Mints a badge for the given recipient, registers it as an IP, /// and makes it a derivative of the organization IP. /// @param recipient The address of the recipient of the badge NFT. - /// @param signature The signature from the whitelist signer. This signautre is generated by having the whitelist + /// @param signature The signature from the whitelist signer. This signature is generated by having the whitelist /// signer sign the caller's address (msg.sender) for this `mint` function. /// @return tokenId The token ID of the minted badge NFT. /// @return ipId The ID of the badge NFT IP. diff --git a/contracts/interfaces/workflows/IDerivativeWorkflows.sol b/contracts/interfaces/workflows/IDerivativeWorkflows.sol index deb47da..1d4f1e1 100644 --- a/contracts/interfaces/workflows/IDerivativeWorkflows.sol +++ b/contracts/interfaces/workflows/IDerivativeWorkflows.sol @@ -81,4 +81,50 @@ interface IDerivativeWorkflows { WorkflowStructs.IPMetadata calldata ipMetadata, WorkflowStructs.SignatureData calldata sigMetadataAndRegister ) external returns (address ipId); + + //////////////////////////////////////////////////////////////////////////// + // DEPRECATED, WILL BE REMOVED IN V1.4 // + //////////////////////////////////////////////////////////////////////////// + + /// @notice Mint an NFT from a SPGNFT collection and register it as a derivative IP without license tokens. + /// @notice THIS VERSION OF THE FUNCTION IS DEPRECATED, WILL BE REMOVED IN V1.4 + function mintAndRegisterIpAndMakeDerivative_deprecated( + address spgNftContract, + WorkflowStructs.MakeDerivativeDEPR calldata derivData, + WorkflowStructs.IPMetadata calldata ipMetadata, + address recipient + ) external returns (address ipId, uint256 tokenId); + + /// @notice Register the given NFT as a derivative IP with metadata without license tokens. + /// @notice THIS VERSION OF THE FUNCTION IS DEPRECATED, WILL BE REMOVED IN V1.4 + function registerIpAndMakeDerivative_deprecated( + address nftContract, + uint256 tokenId, + WorkflowStructs.MakeDerivativeDEPR calldata derivData, + WorkflowStructs.IPMetadata calldata ipMetadata, + WorkflowStructs.SignatureData calldata sigMetadata, + WorkflowStructs.SignatureData calldata sigRegister + ) external returns (address ipId); + + /// @notice Mint an NFT from a SPGNFT collection and register it as a derivative IP using license tokens. + /// @notice THIS VERSION OF THE FUNCTION IS DEPRECATED, WILL BE REMOVED IN V1.4 + function mintAndRegisterIpAndMakeDerivativeWithLicenseTokens_deprecated( + address spgNftContract, + uint256[] calldata licenseTokenIds, + bytes calldata royaltyContext, + WorkflowStructs.IPMetadata calldata ipMetadata, + address recipient + ) external returns (address ipId, uint256 tokenId); + + /// @notice Register the given NFT as a derivative IP using license tokens. + /// @notice THIS VERSION OF THE FUNCTION IS DEPRECATED, WILL BE REMOVED IN V1.4 + function registerIpAndMakeDerivativeWithLicenseTokens_deprecated( + address nftContract, + uint256 tokenId, + uint256[] calldata licenseTokenIds, + bytes calldata royaltyContext, + WorkflowStructs.IPMetadata calldata ipMetadata, + WorkflowStructs.SignatureData calldata sigMetadata, + WorkflowStructs.SignatureData calldata sigRegister + ) external returns (address ipId); } diff --git a/contracts/interfaces/workflows/IGroupingWorkflows.sol b/contracts/interfaces/workflows/IGroupingWorkflows.sol index 06e32f7..747d87d 100644 --- a/contracts/interfaces/workflows/IGroupingWorkflows.sol +++ b/contracts/interfaces/workflows/IGroupingWorkflows.sol @@ -12,6 +12,7 @@ interface IGroupingWorkflows { /// @param spgNftContract The address of the SPGNFT collection. /// @param groupId The ID of the group IP to add the newly registered IP. /// @param recipient The address of the recipient of the minted NFT. + /// @param maxAllowedRewardShare The maximum reward share percentage that can be allocated to the new IP. /// @param licensesData The data of the licenses and their configurations to be attached to the new IP. /// @param ipMetadata OPTIONAL. The desired metadata for the newly minted NFT and registered IP. /// @param sigAddToGroup Signature data for addIp to the group IP via the Grouping Module. @@ -22,6 +23,7 @@ interface IGroupingWorkflows { address spgNftContract, address groupId, address recipient, + uint256 maxAllowedRewardShare, WorkflowStructs.LicenseData[] calldata licensesData, WorkflowStructs.IPMetadata calldata ipMetadata, WorkflowStructs.SignatureData calldata sigAddToGroup, @@ -33,6 +35,7 @@ interface IGroupingWorkflows { /// @param nftContract The address of the NFT collection. /// @param tokenId The ID of the NFT. /// @param groupId The ID of the group IP to add the newly registered IP. + /// @param maxAllowedRewardShare The maximum reward share percentage that can be allocated to the new IP. /// @param licensesData The data of the licenses and their configurations to be attached to the new IP. /// @param ipMetadata OPTIONAL. The desired metadata for the newly registered IP. /// @param sigMetadataAndAttachAndConfig Signature data for setAll (metadata), attachLicenseTerms, and @@ -43,6 +46,7 @@ interface IGroupingWorkflows { address nftContract, uint256 tokenId, address groupId, + uint256 maxAllowedRewardShare, WorkflowStructs.LicenseData[] calldata licensesData, WorkflowStructs.IPMetadata calldata ipMetadata, WorkflowStructs.SignatureData calldata sigMetadataAndAttachAndConfig, @@ -63,11 +67,13 @@ interface IGroupingWorkflows { /// @dev ipIds must be have the same license terms as the group IP. /// @param groupPool The address of the group reward pool. /// @param ipIds The IDs of the IPs to add to the newly registered group IP. + /// @param maxAllowedRewardShare The maximum reward share percentage that can be allocated to each member IP. /// @param licenseData The data of the license and its configuration to be attached to the new group IP. /// @return groupId The ID of the newly registered group IP. function registerGroupAndAttachLicenseAndAddIps( address groupPool, address[] calldata ipIds, + uint256 maxAllowedRewardShare, WorkflowStructs.LicenseData calldata licenseData ) external returns (address groupId); @@ -81,4 +87,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_deprecated( + 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_deprecated( + 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_deprecated( + 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_deprecated( + 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_deprecated( + address groupIpId, + address[] calldata currencyTokens, + uint256[] calldata groupSnapshotIds, + address[] calldata memberIpIds + ) external returns (uint256[] memory collectedRoyalties); } diff --git a/contracts/interfaces/workflows/ILicenseAttachmentWorkflows.sol b/contracts/interfaces/workflows/ILicenseAttachmentWorkflows.sol index 225e502..0d74f9f 100644 --- a/contracts/interfaces/workflows/ILicenseAttachmentWorkflows.sol +++ b/contracts/interfaces/workflows/ILicenseAttachmentWorkflows.sol @@ -56,4 +56,94 @@ interface ILicenseAttachmentWorkflows { WorkflowStructs.LicenseTermsData[] calldata licenseTermsData, WorkflowStructs.SignatureData calldata sigMetadataAndAttachAndConfig ) external returns (address ipId, uint256[] memory licenseTermsIds); + + /// @notice Mint an NFT from a SPGNFT collection, register it as an IP, and attach default license terms. + /// @param spgNftContract The address of the SPGNFT collection. + /// @param recipient The address of the recipient of the minted NFT. + /// @param ipMetadata OPTIONAL. The desired metadata for the newly minted NFT and registered IP. + /// @param allowDuplicates Set to true to allow minting an NFT with a duplicate metadata hash. + /// @return ipId The ID of the newly registered IP. + /// @return tokenId The ID of the newly minted NFT. + function mintAndRegisterIpAndAttachDefaultTerms( + address spgNftContract, + address recipient, + WorkflowStructs.IPMetadata calldata ipMetadata, + bool allowDuplicates + ) external returns (address ipId, uint256 tokenId); + + /// @notice Register a given NFT as an IP and attach default license terms. + /// @param nftContract The address of the NFT collection. + /// @param tokenId The ID of the NFT. + /// @param ipMetadata OPTIONAL. The desired metadata for the newly registered IP. + /// @param sigMetadataAndDefaultTerms Signature data for setAll (metadata) and attachDefaultLicenseTerms + /// to the IP via the Core Metadata Module and Licensing Module. + /// @return ipId The ID of the newly registered IP. + function registerIpAndAttachDefaultTerms( + address nftContract, + uint256 tokenId, + WorkflowStructs.IPMetadata calldata ipMetadata, + WorkflowStructs.SignatureData calldata sigMetadataAndDefaultTerms + ) external returns (address ipId); + + //////////////////////////////////////////////////////////////////////////// + // DEPRECATED // + //////////////////////////////////////////////////////////////////////////// + + /// @notice Register Programmable IP License Terms (if unregistered) and attach it to IP. + /// @notice THIS VERSION OF THE FUNCTION IS DEPRECATED, WILL BE REMOVED IN V1.4 + function registerPILTermsAndAttach_deprecated( + address ipId, + PILTerms[] calldata terms, + WorkflowStructs.SignatureData calldata sigAttach + ) external returns (uint256[] memory licenseTermsIds); + + /// @notice Mint an NFT from a SPGNFT collection, register it with metadata as an IP, + /// register Programmable IPLicense + /// @notice THIS VERSION OF THE FUNCTION IS DEPRECATED, WILL BE REMOVED IN V1.4 + function mintAndRegisterIpAndAttachPILTerms_deprecated( + address spgNftContract, + address recipient, + WorkflowStructs.IPMetadata calldata ipMetadata, + PILTerms[] calldata terms + ) external returns (address ipId, uint256 tokenId, uint256[] memory licenseTermsIds); + + /// @notice Register a given NFT as an IP and attach Programmable IP License Terms. + /// @notice THIS VERSION OF THE FUNCTION IS DEPRECATED, WILL BE REMOVED IN V1.4 + function registerIpAndAttachPILTerms_deprecated( + address nftContract, + uint256 tokenId, + WorkflowStructs.IPMetadata calldata ipMetadata, + PILTerms[] calldata terms, + WorkflowStructs.SignatureData calldata sigMetadata, + WorkflowStructs.SignatureData calldata sigAttach + ) external returns (address ipId, uint256[] memory licenseTermsIds); + + /// @notice Register Programmable IP License Terms (if unregistered) and attach it to IP. + /// @notice THIS VERSION OF THE FUNCTION IS DEPRECATED, WILL BE REMOVED IN V1.4 + function registerPILTermsAndAttach_deprecated( + address ipId, + PILTerms calldata terms, + WorkflowStructs.SignatureData calldata sigAttach + ) external returns (uint256 licenseTermsId); + + /// Mint an NFT from a SPGNFT collection, register it with metadata as an IP, + /// register Programmable IPLicense + /// @notice THIS VERSION OF THE FUNCTION IS DEPRECATED, WILL BE REMOVED IN V1.4 + function mintAndRegisterIpAndAttachPILTerms_deprecated( + address spgNftContract, + address recipient, + WorkflowStructs.IPMetadata calldata ipMetadata, + PILTerms calldata terms + ) external returns (address ipId, uint256 tokenId, uint256 licenseTermsId); + + /// @notice Register a given NFT as an IP and attach Programmable IP License Terms. + /// @notice THIS VERSION OF THE FUNCTION IS DEPRECATED, WILL BE REMOVED IN V1.4 + function registerIpAndAttachPILTerms_deprecated( + address nftContract, + uint256 tokenId, + WorkflowStructs.IPMetadata calldata ipMetadata, + PILTerms calldata terms, + WorkflowStructs.SignatureData calldata sigMetadata, + WorkflowStructs.SignatureData calldata sigAttach + ) external returns (address ipId, uint256 licenseTermsId); } diff --git a/contracts/interfaces/workflows/IRegistrationWorkflows.sol b/contracts/interfaces/workflows/IRegistrationWorkflows.sol index cefecd1..97be6be 100644 --- a/contracts/interfaces/workflows/IRegistrationWorkflows.sol +++ b/contracts/interfaces/workflows/IRegistrationWorkflows.sol @@ -44,4 +44,16 @@ interface IRegistrationWorkflows { WorkflowStructs.IPMetadata calldata ipMetadata, WorkflowStructs.SignatureData calldata sigMetadata ) external returns (address ipId); + + //////////////////////////////////////////////////////////////////////////// + // DEPRECATED, WILL BE REMOVED IN V1.4 // + //////////////////////////////////////////////////////////////////////////// + + /// @notice Mint an NFT from a SPGNFT collection and register it with metadata as an IP. + /// @notice THIS VERSION OF THE FUNCTION IS DEPRECATED, WILL BE REMOVED IN V1.4 + function mintAndRegisterIp_deprecated( + address spgNftContract, + address recipient, + WorkflowStructs.IPMetadata calldata ipMetadata + ) external returns (address ipId, uint256 tokenId); } diff --git a/contracts/interfaces/workflows/IRoyaltyTokenDistributionWorkflows.sol b/contracts/interfaces/workflows/IRoyaltyTokenDistributionWorkflows.sol index 18ca1d3..49c52e6 100644 --- a/contracts/interfaces/workflows/IRoyaltyTokenDistributionWorkflows.sol +++ b/contracts/interfaces/workflows/IRoyaltyTokenDistributionWorkflows.sol @@ -89,4 +89,29 @@ interface IRoyaltyTokenDistributionWorkflows { WorkflowStructs.RoyaltyShare[] calldata royaltyShares, WorkflowStructs.SignatureData calldata sigApproveRoyaltyTokens ) external; + + //////////////////////////////////////////////////////////////////////////// + // DEPRECATED, WILL BE REMOVED IN V1.4 // + //////////////////////////////////////////////////////////////////////////// + + /// @notice Mint an NFT and register the IP, attach PIL terms, and distribute royalty tokens. + /// @dev THIS VERSION OF THE FUNCTION IS DEPRECATED, WILL BE REMOVED IN V1.4 + function mintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokens_deprecated( + address spgNftContract, + address recipient, + WorkflowStructs.IPMetadata calldata ipMetadata, + PILTerms[] calldata terms, + WorkflowStructs.RoyaltyShare[] calldata royaltyShares + ) external returns (address ipId, uint256 tokenId, uint256[] memory licenseTermsIds); + + /// @notice Register an IP, attach PIL terms, and deploy a royalty vault. + /// @dev THIS VERSION OF THE FUNCTION IS DEPRECATED, WILL BE REMOVED IN V1.4 + function registerIpAndAttachPILTermsAndDeployRoyaltyVault_deprecated( + address nftContract, + uint256 tokenId, + WorkflowStructs.IPMetadata calldata ipMetadata, + PILTerms[] calldata terms, + 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 f477c33..4a68a48 100644 --- a/contracts/lib/Errors.sol +++ b/contracts/lib/Errors.sol @@ -14,6 +14,9 @@ library Errors { /// @notice Zero address provided as a param to the RegistrationWorkflows. error RegistrationWorkflows__ZeroAddressParam(); + /// @notice Caller is not the signer in the signature data. + error RegistrationWorkflows__CallerNotSigner(address caller, address signer); + //////////////////////////////////////////////////////////////////////////// // LicenseAttachmentWorkflows // //////////////////////////////////////////////////////////////////////////// @@ -24,6 +27,9 @@ library Errors { /// @notice License terms data list is empty. error LicenseAttachmentWorkflows__NoLicenseTermsData(); + /// @notice Caller is not the signer in the signature data. + error LicenseAttachmentWorkflows__CallerNotSigner(address caller, address signer); + //////////////////////////////////////////////////////////////////////////// // DerivativeWorkflows // //////////////////////////////////////////////////////////////////////////// @@ -37,6 +43,9 @@ library Errors { /// @notice Caller is not the owner of the license token. error DerivativeWorkflows__CallerAndNotTokenOwner(uint256 tokenId, address caller, address actualTokenOwner); + /// @notice Caller is not the signer in the signature data. + error DerivativeWorkflows__CallerNotSigner(address caller, address signer); + //////////////////////////////////////////////////////////////////////////// // Grouping Workflows // //////////////////////////////////////////////////////////////////////////// @@ -47,6 +56,9 @@ library Errors { /// @notice License data list is empty. error GroupingWorkflows__NoLicenseData(); + /// @notice Caller is not the signer in the signature data. + error GroupingWorkflows__CallerNotSigner(address caller, address signer); + //////////////////////////////////////////////////////////////////////////// // Royalty Workflows // //////////////////////////////////////////////////////////////////////////// @@ -71,6 +83,9 @@ library Errors { /// @notice License terms data list is empty. error RoyaltyTokenDistributionWorkflows__NoLicenseTermsData(); + /// @notice Caller is not the signer in the signature data. + error RoyaltyTokenDistributionWorkflows__CallerNotSigner(address caller, address signer); + //////////////////////////////////////////////////////////////////////////// // SPGNFT // //////////////////////////////////////////////////////////////////////////// @@ -87,8 +102,8 @@ library Errors { /// @notice Minting is denied if the public minting is false (=> private) but caller does not have the minter role. error SPGNFT__MintingDenied(); - /// @notice Caller is not the fee recipient. - error SPGNFT__CallerNotFeeRecipient(); + /// @notice Caller is not the fee recipient or admin. + error SPGNFT__CallerNotFeeRecipientOrAdmin(); /// @notice Minting is closed. error SPGNFT__MintingClosed(); diff --git a/contracts/lib/LicensingHelper.sol b/contracts/lib/LicensingHelper.sol index e4919da..0edff43 100644 --- a/contracts/lib/LicensingHelper.sol +++ b/contracts/lib/LicensingHelper.sol @@ -114,7 +114,14 @@ library LicensingHelper { Licensing.LicensingConfig memory licensingConfig ) internal { attachLicenseTerms(ipId, licensingModule, licenseTemplate, licenseTermsId); - ILicensingModule(licensingModule).setLicensingConfig(ipId, licenseTemplate, licenseTermsId, licensingConfig); + if (licensingConfig.isSet) { + ILicensingModule(licensingModule).setLicensingConfig( + ipId, + licenseTemplate, + licenseTermsId, + licensingConfig + ); + } } /// @dev Collects mint fees and registers a derivative. diff --git a/contracts/lib/MetadataHelper.sol b/contracts/lib/MetadataHelper.sol index cde76fd..5a8a292 100644 --- a/contracts/lib/MetadataHelper.sol +++ b/contracts/lib/MetadataHelper.sol @@ -35,7 +35,6 @@ library MetadataHelper { setMetadata(ipId, coreMetadataModule, ipMetadata); } - /// @dev Sets the metadata for the given IP if metadata is non-empty. /// @dev Sets the metadata for the given IP if metadata is non-empty. /// @param ipId The ID of the IP. /// @param coreMetadataModule The address of the Core Metadata Module. diff --git a/contracts/lib/WorkflowStructs.sol b/contracts/lib/WorkflowStructs.sol index d979d0a..b4f9ea7 100644 --- a/contracts/lib/WorkflowStructs.sol +++ b/contracts/lib/WorkflowStructs.sol @@ -73,4 +73,17 @@ library WorkflowStructs { address recipient; uint32 percentage; } + + //////////////////////////////////////////////////////////////////////////// + // DEPRECATED, WILL BE REMOVED IN V1.4 // + //////////////////////////////////////////////////////////////////////////// + + /// @notice Struct for creating a derivative IP without license tokens. + /// @notice THIS VERSION OF THE STRUCT IS DEPRECATED, WILL BE REMOVED IN V1.4 + struct MakeDerivativeDEPR { + address[] parentIpIds; + address licenseTemplate; + uint256[] licenseTermsIds; + bytes royaltyContext; + } } diff --git a/contracts/modules/tokenizer/TokenizerModule.sol b/contracts/modules/tokenizer/TokenizerModule.sol index 58acef6..daf9e63 100644 --- a/contracts/modules/tokenizer/TokenizerModule.sol +++ b/contracts/modules/tokenizer/TokenizerModule.sol @@ -106,7 +106,7 @@ contract TokenizerModule is address ipId, address tokenTemplate, bytes calldata initData - ) external verifyPermission(ipId) nonReentrant returns (address token) { + ) external nonReentrant verifyPermission(ipId) returns (address token) { if (DISPUTE_MODULE.isIpTagged(ipId)) revert Errors.TokenizerModule__DisputedIpId(ipId); if (LICENSE_REGISTRY.isExpiredNow(ipId)) revert Errors.TokenizerModule__IpExpired(ipId); diff --git a/contracts/story-nft/CachableNFT.sol b/contracts/story-nft/CachableNFT.sol index b447044..736755c 100644 --- a/contracts/story-nft/CachableNFT.sol +++ b/contracts/story-nft/CachableNFT.sol @@ -1,9 +1,7 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.26; -// two mode passthrough and cache -// passthrough will just forward the call to the nft contract -// cache contrat has three modes +// cache contract has three modes // 1. cache mode // 2. passthrough mode // 3. auto mode diff --git a/contracts/story-nft/OrgNFT.sol b/contracts/story-nft/OrgNFT.sol index c201da6..a117583 100644 --- a/contracts/story-nft/OrgNFT.sol +++ b/contracts/story-nft/OrgNFT.sol @@ -120,6 +120,9 @@ contract OrgNFT is IOrgNFT, ERC721URIStorageUpgradeable, AccessManagedUpgradeabl (rootOrgTokenId, rootOrgIpId) = _mintAndRegisterIp(address(this), orgIpMetadata); $.rootOrgIpId = rootOrgIpId; + // attach default license terms + LICENSING_MODULE.attachDefaultLicenseTerms(rootOrgIpId); + _safeTransfer(address(this), recipient, rootOrgTokenId); } diff --git a/contracts/story-nft/OrgStoryNFTFactory.sol b/contracts/story-nft/OrgStoryNFTFactory.sol index 344ee15..0bcd13a 100644 --- a/contracts/story-nft/OrgStoryNFTFactory.sol +++ b/contracts/story-nft/OrgStoryNFTFactory.sol @@ -116,7 +116,7 @@ contract OrgStoryNFTFactory is IOrgStoryNFTFactory, AccessManagedUpgradeable, UU /// @param orgNftRecipient The address of the recipient of the organization NFT. /// @param orgName The name of the organization. /// @param orgIpMetadata OPTIONAL. The desired metadata for the newly minted OrgNFT and registered IP. - /// @param signature The signature from the OrgStoryNFTFactory's whitelist signer. This signautre is genreated by + /// @param signature The signature from the OrgStoryNFTFactory's whitelist signer. This signature is genreated by /// having the whitelist signer sign the caller's address (msg.sender) for this `deployStoryNft` function. /// @param storyNftInitParams The initialization data for the OrgStoryNFT (see {IOrgStoryNFT-InitParams}). /// @return orgNft The address of the organization NFT. diff --git a/contracts/story-nft/StoryBadgeNFT.sol b/contracts/story-nft/StoryBadgeNFT.sol index c7ad341..3df5792 100644 --- a/contracts/story-nft/StoryBadgeNFT.sol +++ b/contracts/story-nft/StoryBadgeNFT.sol @@ -81,7 +81,7 @@ contract StoryBadgeNFT is IStoryBadgeNFT, BaseOrgStoryNFT, CachableNFT, ERC721Ho /// @notice Mints a badge for the given recipient, registers it as an IP, /// and makes it a derivative of the organization IP. /// @param recipient The address of the recipient of the badge. - /// @param signature The signature from the whitelist signer. This signautre is genreated by having the whitelist + /// @param signature The signature from the whitelist signer. This signature is genreated by having the whitelist /// signer sign the caller's address (msg.sender) for this `mint` function. /// @return tokenId The token ID of the minted badge NFT. /// @return ipId The ID of the badge NFT IP. diff --git a/contracts/workflows/DerivativeWorkflows.sol b/contracts/workflows/DerivativeWorkflows.sol index 6cb0656..fd64015 100644 --- a/contracts/workflows/DerivativeWorkflows.sol +++ b/contracts/workflows/DerivativeWorkflows.sol @@ -87,6 +87,7 @@ contract DerivativeWorkflows is if (accessManager == address(0)) revert Errors.DerivativeWorkflows__ZeroAddressParam(); __AccessManaged_init(accessManager); __UUPSUpgradeable_init(); + __Multicall_init(); } /// @notice Mint an NFT from a SPGNFT collection and register it as a derivative IP without license tokens. @@ -142,6 +143,9 @@ contract DerivativeWorkflows is WorkflowStructs.IPMetadata calldata ipMetadata, WorkflowStructs.SignatureData calldata sigMetadataAndRegister ) external returns (address ipId) { + if (msg.sender != sigMetadataAndRegister.signer) + revert Errors.DerivativeWorkflows__CallerNotSigner(msg.sender, sigMetadataAndRegister.signer); + ipId = IP_ASSET_REGISTRY.register(block.chainid, nftContract, tokenId); address[] memory modules = new address[](2); @@ -226,6 +230,9 @@ contract DerivativeWorkflows is WorkflowStructs.IPMetadata calldata ipMetadata, WorkflowStructs.SignatureData calldata sigMetadataAndRegister ) external returns (address ipId) { + if (msg.sender != sigMetadataAndRegister.signer) + revert Errors.DerivativeWorkflows__CallerNotSigner(msg.sender, sigMetadataAndRegister.signer); + _collectLicenseTokens(licenseTokenIds, address(LICENSE_TOKEN)); ipId = IP_ASSET_REGISTRY.register(block.chainid, nftContract, tokenId); @@ -271,4 +278,176 @@ contract DerivativeWorkflows 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 and register it as a derivative IP without license tokens. + /// @notice THIS VERSION OF THE FUNCTION IS DEPRECATED, WILL BE REMOVED IN V1.4 + function mintAndRegisterIpAndMakeDerivative_deprecated( + address spgNftContract, + WorkflowStructs.MakeDerivativeDEPR calldata derivData, + WorkflowStructs.IPMetadata calldata ipMetadata, + address recipient + ) 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); + + LicensingHelper.collectMintFeesAndSetApproval( + msg.sender, + address(ROYALTY_MODULE), + address(LICENSING_MODULE), + derivData.licenseTemplate, + derivData.parentIpIds, + derivData.licenseTermsIds + ); + + LICENSING_MODULE.registerDerivative({ + childIpId: ipId, + parentIpIds: derivData.parentIpIds, + licenseTermsIds: derivData.licenseTermsIds, + licenseTemplate: derivData.licenseTemplate, + royaltyContext: derivData.royaltyContext, + maxMintingFee: 0, // no limit + maxRts: ROYALTY_MODULE.maxPercent(), // no limit + maxRevenueShare: ROYALTY_MODULE.maxPercent() // no limit + }); + + ISPGNFT(spgNftContract).safeTransferFrom(address(this), recipient, tokenId, ""); + } + + /// @notice Register the given NFT as a derivative IP with metadata without license tokens. + /// @notice THIS VERSION OF THE FUNCTION IS DEPRECATED, WILL BE REMOVED IN V1.4 + function registerIpAndMakeDerivative_deprecated( + address nftContract, + uint256 tokenId, + WorkflowStructs.MakeDerivativeDEPR calldata derivData, + WorkflowStructs.IPMetadata calldata ipMetadata, + WorkflowStructs.SignatureData calldata sigMetadata, + WorkflowStructs.SignatureData calldata sigRegister + ) external returns (address ipId) { + if (msg.sender != sigMetadata.signer) + revert Errors.DerivativeWorkflows__CallerNotSigner(msg.sender, sigMetadata.signer); + if (msg.sender != sigRegister.signer) + revert Errors.DerivativeWorkflows__CallerNotSigner(msg.sender, sigRegister.signer); + + ipId = IP_ASSET_REGISTRY.register(block.chainid, nftContract, tokenId); + MetadataHelper.setMetadataWithSig( + ipId, + address(CORE_METADATA_MODULE), + address(ACCESS_CONTROLLER), + ipMetadata, + sigMetadata + ); + + PermissionHelper.setPermissionForModule( + ipId, + address(LICENSING_MODULE), + address(ACCESS_CONTROLLER), + ILicensingModule.registerDerivative.selector, + sigRegister + ); + + LicensingHelper.collectMintFeesAndSetApproval( + msg.sender, + address(ROYALTY_MODULE), + address(LICENSING_MODULE), + derivData.licenseTemplate, + derivData.parentIpIds, + derivData.licenseTermsIds + ); + + LICENSING_MODULE.registerDerivative({ + childIpId: ipId, + parentIpIds: derivData.parentIpIds, + licenseTermsIds: derivData.licenseTermsIds, + licenseTemplate: derivData.licenseTemplate, + royaltyContext: derivData.royaltyContext, + maxMintingFee: 0, // no limit + maxRts: ROYALTY_MODULE.maxPercent(), // no limit + maxRevenueShare: ROYALTY_MODULE.maxPercent() // no limit + }); + } + + /// @notice Mint an NFT from a collection and register it as a derivative IP using license tokens + /// @notice THIS VERSION OF THE FUNCTION IS DEPRECATED, WILL BE REMOVED IN V1.4 + function mintAndRegisterIpAndMakeDerivativeWithLicenseTokens_deprecated( + address spgNftContract, + uint256[] calldata licenseTokenIds, + bytes calldata royaltyContext, + WorkflowStructs.IPMetadata calldata ipMetadata, + address recipient + ) external onlyMintAuthorized(spgNftContract) returns (address ipId, uint256 tokenId) { + _collectLicenseTokens(licenseTokenIds, address(LICENSE_TOKEN)); + + 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); + + LICENSING_MODULE.registerDerivativeWithLicenseTokens( + ipId, + licenseTokenIds, + royaltyContext, + ROYALTY_MODULE.maxPercent() + ); + + ISPGNFT(spgNftContract).safeTransferFrom(address(this), recipient, tokenId, ""); + } + + /// @notice Register the given NFT as a derivative IP using license tokens. + /// @notice THIS VERSION OF THE FUNCTION IS DEPRECATED, WILL BE REMOVED IN V1.4 + function registerIpAndMakeDerivativeWithLicenseTokens_deprecated( + address nftContract, + uint256 tokenId, + uint256[] calldata licenseTokenIds, + bytes calldata royaltyContext, + WorkflowStructs.IPMetadata calldata ipMetadata, + WorkflowStructs.SignatureData calldata sigMetadata, + WorkflowStructs.SignatureData calldata sigRegister + ) external returns (address ipId) { + if (msg.sender != sigMetadata.signer) + revert Errors.DerivativeWorkflows__CallerNotSigner(msg.sender, sigMetadata.signer); + if (msg.sender != sigRegister.signer) + revert Errors.DerivativeWorkflows__CallerNotSigner(msg.sender, sigRegister.signer); + + _collectLicenseTokens(licenseTokenIds, address(LICENSE_TOKEN)); + + ipId = IP_ASSET_REGISTRY.register(block.chainid, nftContract, tokenId); + MetadataHelper.setMetadataWithSig( + ipId, + address(CORE_METADATA_MODULE), + address(ACCESS_CONTROLLER), + ipMetadata, + sigMetadata + ); + + PermissionHelper.setPermissionForModule( + ipId, + address(LICENSING_MODULE), + address(ACCESS_CONTROLLER), + ILicensingModule.registerDerivativeWithLicenseTokens.selector, + sigRegister + ); + LICENSING_MODULE.registerDerivativeWithLicenseTokens( + ipId, + licenseTokenIds, + royaltyContext, + ROYALTY_MODULE.maxPercent() + ); + } } diff --git a/contracts/workflows/GroupingWorkflows.sol b/contracts/workflows/GroupingWorkflows.sol index b76bfef..63770b5 100644 --- a/contracts/workflows/GroupingWorkflows.sol +++ b/contracts/workflows/GroupingWorkflows.sol @@ -96,6 +96,7 @@ contract GroupingWorkflows is if (accessManager == address(0)) revert Errors.GroupingWorkflows__ZeroAddressParam(); __AccessManaged_init(accessManager); __UUPSUpgradeable_init(); + __Multicall_init(); } /// @notice Mint an NFT from a SPGNFT collection, register it with metadata as an IP, attach @@ -104,6 +105,7 @@ contract GroupingWorkflows is /// @param spgNftContract The address of the SPGNFT collection. /// @param groupId The ID of the group IP to add the newly registered IP. /// @param recipient The address of the recipient of the minted NFT. + /// @param maxAllowedRewardShare The maximum reward share percentage that can be allocated to each member IP. /// @param licensesData The data of the licenses and their configurations to be attached to the new IP. /// @param ipMetadata OPTIONAL. The desired metadata for the newly minted NFT and registered IP. /// @param sigAddToGroup Signature data for addIp to the group IP via the Grouping Module. @@ -114,12 +116,15 @@ contract GroupingWorkflows is address spgNftContract, address groupId, address recipient, + uint256 maxAllowedRewardShare, WorkflowStructs.LicenseData[] calldata licensesData, WorkflowStructs.IPMetadata calldata ipMetadata, WorkflowStructs.SignatureData calldata sigAddToGroup, bool allowDuplicates ) external onlyMintAuthorized(spgNftContract) returns (address ipId, uint256 tokenId) { if (licensesData.length == 0) revert Errors.GroupingWorkflows__NoLicenseData(); + if (msg.sender != sigAddToGroup.signer) + revert Errors.GroupingWorkflows__CallerNotSigner(msg.sender, sigAddToGroup.signer); tokenId = ISPGNFT(spgNftContract).mintByPeriphery({ to: address(this), @@ -144,7 +149,7 @@ contract GroupingWorkflows is address[] memory ipIds = new address[](1); ipIds[0] = ipId; - GROUPING_MODULE.addIp(groupId, ipIds); + GROUPING_MODULE.addIp(groupId, ipIds, maxAllowedRewardShare); ISPGNFT(spgNftContract).safeTransferFrom(address(this), recipient, tokenId, ""); } @@ -154,6 +159,7 @@ contract GroupingWorkflows is /// @param nftContract The address of the NFT collection. /// @param tokenId The ID of the NFT. /// @param groupId The ID of the group IP to add the newly registered IP. + /// @param maxAllowedRewardShare The maximum reward share percentage that can be allocated to each member IP. /// @param licensesData The data of the licenses and their configurations to be attached to the new IP. /// @param ipMetadata OPTIONAL. The desired metadata for the newly registered IP. /// @param sigMetadataAndAttachAndConfig Signature data for setAll (metadata), attachLicenseTerms, and @@ -164,12 +170,17 @@ contract GroupingWorkflows is address nftContract, uint256 tokenId, address groupId, + uint256 maxAllowedRewardShare, WorkflowStructs.LicenseData[] calldata licensesData, WorkflowStructs.IPMetadata calldata ipMetadata, WorkflowStructs.SignatureData calldata sigMetadataAndAttachAndConfig, WorkflowStructs.SignatureData calldata sigAddToGroup ) external returns (address ipId) { if (licensesData.length == 0) revert Errors.GroupingWorkflows__NoLicenseData(); + if (msg.sender != sigMetadataAndAttachAndConfig.signer) + revert Errors.GroupingWorkflows__CallerNotSigner(msg.sender, sigMetadataAndAttachAndConfig.signer); + if (msg.sender != sigAddToGroup.signer) + revert Errors.GroupingWorkflows__CallerNotSigner(msg.sender, sigAddToGroup.signer); ipId = IP_ASSET_REGISTRY.register(block.chainid, nftContract, tokenId); @@ -204,7 +215,7 @@ contract GroupingWorkflows is address[] memory ipIds = new address[](1); ipIds[0] = ipId; - GROUPING_MODULE.addIp(groupId, ipIds); + GROUPING_MODULE.addIp(groupId, ipIds, maxAllowedRewardShare); } /// @notice Register a group IP with a group reward pool and attach license terms to the group IP @@ -233,11 +244,13 @@ contract GroupingWorkflows is /// @dev ipIds must have the same PIL terms as the group IP. /// @param groupPool The address of the group reward pool. /// @param ipIds The IDs of the IPs to add to the newly registered group IP. + /// @param maxAllowedRewardShare The maximum reward share percentage that can be allocated to each member IP. /// @param licenseData The data of the license and its configuration to be attached to the new group IP. /// @return groupId The ID of the newly registered group IP. function registerGroupAndAttachLicenseAndAddIps( address groupPool, address[] calldata ipIds, + uint256 maxAllowedRewardShare, WorkflowStructs.LicenseData calldata licenseData ) external returns (address groupId) { groupId = GROUPING_MODULE.registerGroup(groupPool); @@ -250,7 +263,7 @@ contract GroupingWorkflows is licenseData.licensingConfig ); - GROUPING_MODULE.addIp(groupId, ipIds); + GROUPING_MODULE.addIp(groupId, ipIds, maxAllowedRewardShare); GROUP_NFT.safeTransferFrom(address(this), msg.sender, GROUP_NFT.totalSupply() - 1); } @@ -317,4 +330,229 @@ 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_deprecated( + 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) { + if (msg.sender != sigAddToGroup.signer) + revert Errors.GroupingWorkflows__CallerNotSigner(msg.sender, sigAddToGroup.signer); + + 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, 100e6); + + 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_deprecated( + 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) { + if (msg.sender != sigMetadataAndAttachAndConfig.signer) + revert Errors.GroupingWorkflows__CallerNotSigner(msg.sender, sigMetadataAndAttachAndConfig.signer); + if (msg.sender != sigAddToGroup.signer) + revert Errors.GroupingWorkflows__CallerNotSigner(msg.sender, sigAddToGroup.signer); + + 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, 100e6); + } + + /// @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_deprecated( + 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_deprecated( + 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, 100e6); + + 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_deprecated( + 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); + } + } + + /// @notice THIS FUNCTION IS DEPRECATED, WILL BE REMOVED IN V1.4 + 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); + } + + /// @notice THIS FUNCTION IS DEPRECATED, WILL BE REMOVED IN V1.4 + 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/contracts/workflows/LicenseAttachmentWorkflows.sol b/contracts/workflows/LicenseAttachmentWorkflows.sol index 57aa0d7..b39d0e0 100644 --- a/contracts/workflows/LicenseAttachmentWorkflows.sol +++ b/contracts/workflows/LicenseAttachmentWorkflows.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.26; // solhint-disable-next-line max-line-length import { AccessManagedUpgradeable } from "@openzeppelin/contracts-upgradeable/access/manager/AccessManagedUpgradeable.sol"; import { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; +import { ERC721Holder } from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; import { MulticallUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/MulticallUpgradeable.sol"; import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; @@ -28,7 +29,8 @@ contract LicenseAttachmentWorkflows is BaseWorkflow, MulticallUpgradeable, AccessManagedUpgradeable, - UUPSUpgradeable + UUPSUpgradeable, + ERC721Holder { using ERC165Checker for address; @@ -68,6 +70,7 @@ contract LicenseAttachmentWorkflows is if (accessManager == address(0)) revert Errors.LicenseAttachmentWorkflows__ZeroAddressParam(); __AccessManaged_init(accessManager); __UUPSUpgradeable_init(); + __Multicall_init(); } /// @notice Register Programmable IP License Terms (if unregistered) and attach it to IP. @@ -81,6 +84,8 @@ contract LicenseAttachmentWorkflows is WorkflowStructs.SignatureData calldata sigAttachAndConfig ) external returns (uint256[] memory licenseTermsIds) { if (licenseTermsData.length == 0) revert Errors.LicenseAttachmentWorkflows__NoLicenseTermsData(); + if (msg.sender != sigAttachAndConfig.signer) + revert Errors.LicenseAttachmentWorkflows__CallerNotSigner(msg.sender, sigAttachAndConfig.signer); address[] memory modules = new address[](2); bytes4[] memory selectors = new bytes4[](2); @@ -168,6 +173,8 @@ contract LicenseAttachmentWorkflows is WorkflowStructs.SignatureData calldata sigMetadataAndAttachAndConfig ) external returns (address ipId, uint256[] memory licenseTermsIds) { if (licenseTermsData.length == 0) revert Errors.LicenseAttachmentWorkflows__NoLicenseTermsData(); + if (msg.sender != sigMetadataAndAttachAndConfig.signer) + revert Errors.LicenseAttachmentWorkflows__CallerNotSigner(msg.sender, sigMetadataAndAttachAndConfig.signer); ipId = IP_ASSET_REGISTRY.register(block.chainid, nftContract, tokenId); @@ -197,6 +204,73 @@ contract LicenseAttachmentWorkflows is }); } + /// @notice Mint an NFT from a SPGNFT collection, register it with metadata as an IP, + /// and attach default license terms. + /// @param spgNftContract The address of the SPGNFT collection. + /// @param recipient The address of the recipient of the minted NFT. + /// @param ipMetadata OPTIONAL. The desired metadata for the newly minted NFT and registered IP. + /// @param allowDuplicates Set to true to allow minting an NFT with a duplicate metadata hash. + /// @return ipId The ID of the newly registered IP. + /// @return tokenId The ID of the newly minted NFT. + function mintAndRegisterIpAndAttachDefaultTerms( + address spgNftContract, + address recipient, + WorkflowStructs.IPMetadata calldata ipMetadata, + bool allowDuplicates + ) external onlyMintAuthorized(spgNftContract) returns (address ipId, uint256 tokenId) { + tokenId = ISPGNFT(spgNftContract).mintByPeriphery({ + to: address(this), + payer: msg.sender, + nftMetadataURI: ipMetadata.nftMetadataURI, + nftMetadataHash: ipMetadata.nftMetadataHash, + allowDuplicates: allowDuplicates + }); + + ipId = IP_ASSET_REGISTRY.register(block.chainid, spgNftContract, tokenId); + MetadataHelper.setMetadata(ipId, address(CORE_METADATA_MODULE), ipMetadata); + + LICENSING_MODULE.attachDefaultLicenseTerms(ipId); + + ISPGNFT(spgNftContract).safeTransferFrom(address(this), recipient, tokenId, ""); + } + + /// @notice Register a given NFT as an IP and attach default license terms. + /// @param nftContract The address of the NFT collection. + /// @param tokenId The ID of the NFT. + /// @param ipMetadata OPTIONAL. The desired metadata for the newly registered IP. + /// @param sigMetadataAndDefaultTerms Signature data for setAll (metadata) and attachDefaultLicenseTerms + /// to the IP via the Core Metadata Module and Licensing Module. + /// @return ipId The ID of the newly registered IP. + function registerIpAndAttachDefaultTerms( + address nftContract, + uint256 tokenId, + WorkflowStructs.IPMetadata calldata ipMetadata, + WorkflowStructs.SignatureData calldata sigMetadataAndDefaultTerms + ) external returns (address ipId) { + if (msg.sender != sigMetadataAndDefaultTerms.signer) + revert Errors.LicenseAttachmentWorkflows__CallerNotSigner(msg.sender, sigMetadataAndDefaultTerms.signer); + + ipId = IP_ASSET_REGISTRY.register(block.chainid, nftContract, tokenId); + + address[] memory modules = new address[](2); + bytes4[] memory selectors = new bytes4[](2); + modules[0] = address(CORE_METADATA_MODULE); + modules[1] = address(LICENSING_MODULE); + selectors[0] = ICoreMetadataModule.setAll.selector; + selectors[1] = ILicensingModule.attachDefaultLicenseTerms.selector; + PermissionHelper.setBatchPermissionForModules({ + ipId: ipId, + accessController: address(ACCESS_CONTROLLER), + modules: modules, + selectors: selectors, + sigData: sigMetadataAndDefaultTerms + }); + + MetadataHelper.setMetadata(ipId, address(CORE_METADATA_MODULE), ipMetadata); + + LICENSING_MODULE.attachDefaultLicenseTerms(ipId); + } + // // Upgrade // @@ -204,4 +278,208 @@ contract LicenseAttachmentWorkflows 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 // + //////////////////////////////////////////////////////////////////////////// + + /// @notice Register Programmable IP License Terms (if unregistered) and attach it to IP. + /// @notice THIS VERSION OF THE FUNCTION IS DEPRECATED, WILL BE REMOVED IN V1.4 + function registerPILTermsAndAttach_deprecated( + address ipId, + PILTerms[] calldata terms, + WorkflowStructs.SignatureData calldata sigAttach + ) external returns (uint256[] memory licenseTermsIds) { + if (terms.length == 0) revert Errors.LicenseAttachmentWorkflows__NoLicenseTermsData(); + if (msg.sender != sigAttach.signer) + revert Errors.LicenseAttachmentWorkflows__CallerNotSigner(msg.sender, sigAttach.signer); + + PermissionHelper.setPermissionForModule( + ipId, + address(LICENSING_MODULE), + address(ACCESS_CONTROLLER), + ILicensingModule.attachLicenseTerms.selector, + sigAttach + ); + + licenseTermsIds = _registerMultiplePILTermsAndAttach(ipId, terms); + } + + /// @notice Mint an NFT from a SPGNFT collection, register it with metadata as an IP, + /// register Programmable IP License Terms (if unregistered), and attach it to the registered IP. + /// @notice THIS VERSION OF THE FUNCTION IS DEPRECATED, WILL BE REMOVED IN V1.4 + function mintAndRegisterIpAndAttachPILTerms_deprecated( + address spgNftContract, + address recipient, + WorkflowStructs.IPMetadata calldata ipMetadata, + PILTerms[] calldata terms + ) + external + onlyMintAuthorized(spgNftContract) + returns (address ipId, uint256 tokenId, uint256[] memory licenseTermsIds) + { + if (terms.length == 0) revert Errors.LicenseAttachmentWorkflows__NoLicenseTermsData(); + + 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); + + licenseTermsIds = _registerMultiplePILTermsAndAttach(ipId, terms); + + ISPGNFT(spgNftContract).safeTransferFrom(address(this), recipient, tokenId, ""); + } + + /// @notice Register a given NFT as an IP and attach Programmable IP License Terms. + /// @notice THIS VERSION OF THE FUNCTION IS DEPRECATED, WILL BE REMOVED IN V1.4 + function registerIpAndAttachPILTerms_deprecated( + address nftContract, + uint256 tokenId, + WorkflowStructs.IPMetadata calldata ipMetadata, + PILTerms[] calldata terms, + WorkflowStructs.SignatureData calldata sigMetadata, + WorkflowStructs.SignatureData calldata sigAttach + ) external returns (address ipId, uint256[] memory licenseTermsIds) { + if (terms.length == 0) revert Errors.LicenseAttachmentWorkflows__NoLicenseTermsData(); + if (msg.sender != sigMetadata.signer) + revert Errors.LicenseAttachmentWorkflows__CallerNotSigner(msg.sender, sigMetadata.signer); + if (msg.sender != sigAttach.signer) + revert Errors.LicenseAttachmentWorkflows__CallerNotSigner(msg.sender, sigAttach.signer); + + ipId = IP_ASSET_REGISTRY.register(block.chainid, nftContract, tokenId); + MetadataHelper.setMetadataWithSig( + ipId, + address(CORE_METADATA_MODULE), + address(ACCESS_CONTROLLER), + ipMetadata, + sigMetadata + ); + + PermissionHelper.setPermissionForModule( + ipId, + address(LICENSING_MODULE), + address(ACCESS_CONTROLLER), + ILicensingModule.attachLicenseTerms.selector, + sigAttach + ); + + licenseTermsIds = _registerMultiplePILTermsAndAttach(ipId, terms); + } + + /// @notice Register Programmable IP License Terms (if unregistered) and attach it to IP. + /// @notice THIS VERSION OF THE FUNCTION IS DEPRECATED, WILL BE REMOVED IN V1.4 + function registerPILTermsAndAttach_deprecated( + address ipId, + PILTerms calldata terms, + WorkflowStructs.SignatureData calldata sigAttach + ) external returns (uint256 licenseTermsId) { + if (msg.sender != sigAttach.signer) + revert Errors.LicenseAttachmentWorkflows__CallerNotSigner(msg.sender, sigAttach.signer); + + PermissionHelper.setPermissionForModule( + ipId, + address(LICENSING_MODULE), + address(ACCESS_CONTROLLER), + ILicensingModule.attachLicenseTerms.selector, + sigAttach + ); + + licenseTermsId = LicensingHelper.registerPILTermsAndAttach( + ipId, + address(PIL_TEMPLATE), + address(LICENSING_MODULE), + terms + ); + } + + /// @notice Mint an NFT from a SPGNFT collection, register it with metadata as an IP, + /// register Programmable IP License Terms (if unregistered), and attach it to the registered IP. + /// @notice THIS VERSION OF THE FUNCTION IS DEPRECATED, WILL BE REMOVED IN V1.4 + function mintAndRegisterIpAndAttachPILTerms_deprecated( + address spgNftContract, + address recipient, + WorkflowStructs.IPMetadata calldata ipMetadata, + PILTerms calldata terms + ) external onlyMintAuthorized(spgNftContract) returns (address ipId, uint256 tokenId, uint256 licenseTermsId) { + 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); + + licenseTermsId = LicensingHelper.registerPILTermsAndAttach( + ipId, + address(PIL_TEMPLATE), + address(LICENSING_MODULE), + terms + ); + + ISPGNFT(spgNftContract).safeTransferFrom(address(this), recipient, tokenId, ""); + } + + /// @notice Register a given NFT as an IP and attach Programmable IP License Terms. + /// @notice THIS VERSION OF THE FUNCTION IS DEPRECATED, WILL BE REMOVED IN V1.4 + function registerIpAndAttachPILTerms_deprecated( + address nftContract, + uint256 tokenId, + WorkflowStructs.IPMetadata calldata ipMetadata, + PILTerms calldata terms, + WorkflowStructs.SignatureData calldata sigMetadata, + WorkflowStructs.SignatureData calldata sigAttach + ) external returns (address ipId, uint256 licenseTermsId) { + if (msg.sender != sigMetadata.signer) + revert Errors.LicenseAttachmentWorkflows__CallerNotSigner(msg.sender, sigMetadata.signer); + if (msg.sender != sigAttach.signer) + revert Errors.LicenseAttachmentWorkflows__CallerNotSigner(msg.sender, sigAttach.signer); + + ipId = IP_ASSET_REGISTRY.register(block.chainid, nftContract, tokenId); + MetadataHelper.setMetadataWithSig( + ipId, + address(CORE_METADATA_MODULE), + address(ACCESS_CONTROLLER), + ipMetadata, + sigMetadata + ); + + PermissionHelper.setPermissionForModule( + ipId, + address(LICENSING_MODULE), + address(ACCESS_CONTROLLER), + ILicensingModule.attachLicenseTerms.selector, + sigAttach + ); + + licenseTermsId = LicensingHelper.registerPILTermsAndAttach( + ipId, + address(PIL_TEMPLATE), + address(LICENSING_MODULE), + terms + ); + } + + /// @notice THIS FUNCTION IS DEPRECATED, WILL BE REMOVED IN V1.4 + function _registerMultiplePILTermsAndAttach( + address ipId, + PILTerms[] calldata terms + ) private returns (uint256[] memory licenseTermsIds) { + licenseTermsIds = new uint256[](terms.length); + uint256 length = terms.length; + for (uint256 i; i < length; i++) { + licenseTermsIds[i] = LicensingHelper.registerPILTermsAndAttach( + ipId, + address(PIL_TEMPLATE), + address(LICENSING_MODULE), + terms[i] + ); + } + } } diff --git a/contracts/workflows/RegistrationWorkflows.sol b/contracts/workflows/RegistrationWorkflows.sol index 0593841..3f1d53b 100644 --- a/contracts/workflows/RegistrationWorkflows.sol +++ b/contracts/workflows/RegistrationWorkflows.sol @@ -5,6 +5,7 @@ pragma solidity 0.8.26; import { AccessManagedUpgradeable } from "@openzeppelin/contracts-upgradeable/access/manager/AccessManagedUpgradeable.sol"; import { BeaconProxy } from "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol"; import { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; +import { ERC721Holder } from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; import { MulticallUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/MulticallUpgradeable.sol"; import { UpgradeableBeacon } from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; @@ -24,7 +25,8 @@ contract RegistrationWorkflows is BaseWorkflow, MulticallUpgradeable, AccessManagedUpgradeable, - UUPSUpgradeable + UUPSUpgradeable, + ERC721Holder { using ERC165Checker for address; @@ -76,6 +78,7 @@ contract RegistrationWorkflows is if (accessManager == address(0)) revert Errors.RegistrationWorkflows__ZeroAddressParam(); __AccessManaged_init(accessManager); __UUPSUpgradeable_init(); + __Multicall_init(); } /// @dev Sets the NFT contract beacon address. @@ -141,6 +144,9 @@ contract RegistrationWorkflows is WorkflowStructs.IPMetadata calldata ipMetadata, WorkflowStructs.SignatureData calldata sigMetadata ) external returns (address ipId) { + if (msg.sender != address(0) && msg.sender != sigMetadata.signer) + revert Errors.RegistrationWorkflows__CallerNotSigner(msg.sender, sigMetadata.signer); + ipId = IP_ASSET_REGISTRY.register(block.chainid, nftContract, tokenId); MetadataHelper.setMetadataWithSig( ipId, @@ -165,4 +171,26 @@ contract RegistrationWorkflows 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 and register it with metadata as an IP. + /// @notice THIS VERSION OF THE FUNCTION IS DEPRECATED, WILL BE REMOVED IN V1.4 + function mintAndRegisterIp_deprecated( + address spgNftContract, + address recipient, + WorkflowStructs.IPMetadata calldata ipMetadata + ) 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); + ISPGNFT(spgNftContract).safeTransferFrom(address(this), recipient, tokenId, ""); + } } diff --git a/contracts/workflows/RoyaltyTokenDistributionWorkflows.sol b/contracts/workflows/RoyaltyTokenDistributionWorkflows.sol index a1a7d3e..52695ce 100644 --- a/contracts/workflows/RoyaltyTokenDistributionWorkflows.sol +++ b/contracts/workflows/RoyaltyTokenDistributionWorkflows.sol @@ -4,8 +4,10 @@ pragma solidity 0.8.26; // solhint-disable-next-line max-line-length import { AccessManagedUpgradeable } from "@openzeppelin/contracts-upgradeable/access/manager/AccessManagedUpgradeable.sol"; import { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; +import { ERC721Holder } from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { MulticallUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/MulticallUpgradeable.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import { ICoreMetadataModule } from "@storyprotocol/core/interfaces/modules/metadata/ICoreMetadataModule.sol"; @@ -33,9 +35,11 @@ contract RoyaltyTokenDistributionWorkflows is BaseWorkflow, MulticallUpgradeable, AccessManagedUpgradeable, - UUPSUpgradeable + UUPSUpgradeable, + ERC721Holder { using ERC165Checker for address; + using SafeERC20 for IERC20; /// @notice The address of the Royalty Module. /// @custom:oz-upgrades-unsafe-allow state-variable-immutable @@ -95,6 +99,7 @@ contract RoyaltyTokenDistributionWorkflows is if (accessManager == address(0)) revert Errors.RoyaltyTokenDistributionWorkflows__ZeroAddressParam(); __AccessManaged_init(accessManager); __UUPSUpgradeable_init(); + __Multicall_init(); } /// @notice Mint an NFT and register the IP, attach PIL terms, and distribute royalty tokens. @@ -213,6 +218,11 @@ contract RoyaltyTokenDistributionWorkflows is WorkflowStructs.SignatureData calldata sigMetadataAndAttachAndConfig ) external returns (address ipId, uint256[] memory licenseTermsIds, address ipRoyaltyVault) { if (licenseTermsData.length == 0) revert Errors.RoyaltyTokenDistributionWorkflows__NoLicenseTermsData(); + if (msg.sender != sigMetadataAndAttachAndConfig.signer) + revert Errors.RoyaltyTokenDistributionWorkflows__CallerNotSigner( + msg.sender, + sigMetadataAndAttachAndConfig.signer + ); ipId = IP_ASSET_REGISTRY.register(block.chainid, nftContract, tokenId); @@ -260,6 +270,9 @@ contract RoyaltyTokenDistributionWorkflows is WorkflowStructs.MakeDerivative calldata derivData, WorkflowStructs.SignatureData calldata sigMetadataAndRegister ) external returns (address ipId, address ipRoyaltyVault) { + if (msg.sender != sigMetadataAndRegister.signer) + revert Errors.RoyaltyTokenDistributionWorkflows__CallerNotSigner(msg.sender, sigMetadataAndRegister.signer); + ipId = IP_ASSET_REGISTRY.register(block.chainid, nftContract, tokenId); address[] memory modules = new address[](2); @@ -297,6 +310,12 @@ contract RoyaltyTokenDistributionWorkflows is WorkflowStructs.RoyaltyShare[] calldata royaltyShares, WorkflowStructs.SignatureData calldata sigApproveRoyaltyTokens ) external { + if (msg.sender != sigApproveRoyaltyTokens.signer) + revert Errors.RoyaltyTokenDistributionWorkflows__CallerNotSigner( + msg.sender, + sigApproveRoyaltyTokens.signer + ); + _distributeRoyaltyTokens(ipId, royaltyShares, sigApproveRoyaltyTokens); } @@ -387,7 +406,7 @@ contract RoyaltyTokenDistributionWorkflows is // distribute the royalty tokens for (uint256 i; i < royaltyShares.length; i++) { - IERC20(ipRoyaltyVault).transferFrom({ + IERC20(ipRoyaltyVault).safeTransferFrom({ from: ipId, to: royaltyShares[i].recipient, value: royaltyShares[i].percentage @@ -426,4 +445,136 @@ contract RoyaltyTokenDistributionWorkflows 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 and register the IP, attach PIL terms, and distribute royalty tokens. + /// @dev THIS VERSION OF THE FUNCTION IS DEPRECATED, WILL BE REMOVED IN V1.4 + function mintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokens_deprecated( + address spgNftContract, + address recipient, + WorkflowStructs.IPMetadata calldata ipMetadata, + PILTerms[] calldata terms, + WorkflowStructs.RoyaltyShare[] calldata royaltyShares + ) + external + onlyMintAuthorized(spgNftContract) + returns (address ipId, uint256 tokenId, uint256[] memory licenseTermsIds) + { + 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); + + licenseTermsIds = _registerMultiplePILTermsAndAttach(ipId, terms); + + _deployRoyaltyVaultDEPR(ipId, address(PIL_TEMPLATE), licenseTermsIds[0]); + _distributeRoyaltyTokens( + ipId, + royaltyShares, + WorkflowStructs.SignatureData(address(0), 0, "") // no signature required. + ); + + ISPGNFT(spgNftContract).safeTransferFrom(address(this), recipient, tokenId, ""); + } + + /// @notice Register an IP, attach PIL terms, and deploy a royalty vault. + /// @dev THIS VERSION OF THE FUNCTION IS DEPRECATED, WILL BE REMOVED IN V1.4 + function registerIpAndAttachPILTermsAndDeployRoyaltyVault_deprecated( + address nftContract, + uint256 tokenId, + WorkflowStructs.IPMetadata calldata ipMetadata, + PILTerms[] calldata terms, + WorkflowStructs.SignatureData calldata sigMetadata, + WorkflowStructs.SignatureData calldata sigAttach + ) external returns (address ipId, uint256[] memory licenseTermsIds, address ipRoyaltyVault) { + if (msg.sender != sigMetadata.signer) + revert Errors.RoyaltyTokenDistributionWorkflows__CallerNotSigner(msg.sender, sigMetadata.signer); + if (msg.sender != sigAttach.signer) + revert Errors.RoyaltyTokenDistributionWorkflows__CallerNotSigner(msg.sender, sigAttach.signer); + + ipId = IP_ASSET_REGISTRY.register(block.chainid, nftContract, tokenId); + MetadataHelper.setMetadataWithSig( + ipId, + address(CORE_METADATA_MODULE), + address(ACCESS_CONTROLLER), + ipMetadata, + sigMetadata + ); + + PermissionHelper.setPermissionForModule( + ipId, + address(LICENSING_MODULE), + address(ACCESS_CONTROLLER), + ILicensingModule.attachLicenseTerms.selector, + sigAttach + ); + + licenseTermsIds = _registerMultiplePILTermsAndAttach(ipId, terms); + + ipRoyaltyVault = _deployRoyaltyVaultDEPR(ipId, address(PIL_TEMPLATE), licenseTermsIds[0]); + } + + /// @dev Deploys a royalty vault for the IP. + /// @dev THIS FUNCTION IS DEPRECATED, WILL BE REMOVED IN V1.4 + function _deployRoyaltyVaultDEPR( + address ipId, + address licenseTemplate, + uint256 licenseTermsId + ) internal returns (address ipRoyaltyVault) { + // if no royalty vault, mint a license token to trigger the vault deployment + if (ROYALTY_MODULE.ipRoyaltyVaults(ipId) == address(0)) { + address[] memory parentIpIds = new address[](1); + uint256[] memory licenseTermsIds = new uint256[](1); + parentIpIds[0] = ipId; + licenseTermsIds[0] = licenseTermsId; + + LicensingHelper.collectMintFeesAndSetApproval({ + payerAddress: msg.sender, + royaltyModule: address(ROYALTY_MODULE), + licensingModule: address(LICENSING_MODULE), + licenseTemplate: licenseTemplate, + parentIpIds: parentIpIds, + licenseTermsIds: licenseTermsIds + }); + + LICENSING_MODULE.mintLicenseTokens({ + licensorIpId: ipId, + licenseTemplate: licenseTemplate, + licenseTermsId: licenseTermsId, + amount: 1, + receiver: msg.sender, + royaltyContext: "", + maxMintingFee: 0, + maxRevenueShare: 0 + }); + } + + ipRoyaltyVault = ROYALTY_MODULE.ipRoyaltyVaults(ipId); + if (ipRoyaltyVault == address(0)) revert Errors.RoyaltyTokenDistributionWorkflows__RoyaltyVaultNotDeployed(); + } + + /// @notice THIS FUNCTION IS DEPRECATED, WILL BE REMOVED IN V1.4 + function _registerMultiplePILTermsAndAttach( + address ipId, + PILTerms[] calldata terms + ) private returns (uint256[] memory licenseTermsIds) { + licenseTermsIds = new uint256[](terms.length); + uint256 length = terms.length; + for (uint256 i; i < length; i++) { + licenseTermsIds[i] = LicensingHelper.registerPILTermsAndAttach( + ipId, + address(PIL_TEMPLATE), + address(LICENSING_MODULE), + terms[i] + ); + } + } } diff --git a/contracts/workflows/RoyaltyWorkflows.sol b/contracts/workflows/RoyaltyWorkflows.sol index 5be1108..cdb7a70 100644 --- a/contracts/workflows/RoyaltyWorkflows.sol +++ b/contracts/workflows/RoyaltyWorkflows.sol @@ -41,6 +41,7 @@ contract RoyaltyWorkflows is IRoyaltyWorkflows, MulticallUpgradeable, AccessMana if (accessManager == address(0)) revert Errors.RoyaltyWorkflows__ZeroAddressParam(); __AccessManaged_init(accessManager); __UUPSUpgradeable_init(); + __Multicall_init(); } /// @notice Transfers all available royalties from various royalty policies to the royalty diff --git a/docs/WORKFLOWS.md b/docs/WORKFLOWS.md index d2d7283..654fc3f 100644 --- a/docs/WORKFLOWS.md +++ b/docs/WORKFLOWS.md @@ -19,6 +19,10 @@ - Registers an IP → Registers multiple PIL terms → Attaches them to the IP → Sets the licensing configuration for each of the attached PIL terms - `mintAndRegisterIpAndAttachPILTerms`: - Mints a NFT → Registers it as an IP → Registers multiple PIL terms → Attaches them to the IP → Sets the licensing configuration for each of the attached PIL terms +- `mintAndRegisterIpAndAttachDefaultTerms`: + - Mints a NFT → Registers it as an IP → Attaches default license terms to the IP +- `registerIpAndAttachDefaultTerms`: + - Registers an IP → Attaches default license terms to the IP ### [Derivative Workflows](../contracts/interfaces/workflows/IDerivativeWorkflows.sol) diff --git a/package.json b/package.json index 9c99836..c6da4c5 100644 --- a/package.json +++ b/package.json @@ -37,8 +37,8 @@ "typechain": "^8.3.2" }, "dependencies": { - "@openzeppelin/contracts": "5.1.0", - "@openzeppelin/contracts-upgradeable": "5.1.0", + "@openzeppelin/contracts": "5.2.0", + "@openzeppelin/contracts-upgradeable": "5.2.0", "@story-protocol/protocol-core": "github:storyprotocol/protocol-core-v1#main", "erc6551": "^0.3.1", "solady": "^0.0.281" diff --git a/script/deployment/Main.s.sol b/script/deployment/Main.s.sol index 629b7ab..35a5d1e 100644 --- a/script/deployment/Main.s.sol +++ b/script/deployment/Main.s.sol @@ -7,7 +7,7 @@ import { DeployHelper } from "../utils/DeployHelper.sol"; contract Main is DeployHelper { address internal CREATE3_DEPLOYER = 0x9fBB3DF7C40Da2e5A0dE984fFE2CCB7C47cd0ABf; - uint256 private constant CREATE3_DEFAULT_SEED = 12; + uint256 private constant CREATE3_DEFAULT_SEED = 8; constructor() DeployHelper(CREATE3_DEPLOYER){} diff --git a/script/utils/BroadcastManager.s.sol b/script/utils/BroadcastManager.s.sol index 73a2fb0..30831f7 100644 --- a/script/utils/BroadcastManager.s.sol +++ b/script/utils/BroadcastManager.s.sol @@ -14,10 +14,10 @@ contract BroadcastManager is Script { deployer = vm.envAddress("MAINNET_DEPLOYER_ADDRESS"); multisig = vm.envAddress("MAINNET_MULTISIG_ADDRESS"); vm.startBroadcast(deployerPrivateKey); - } else if (block.chainid == 1513 || block.chainid == 1514 || block.chainid == 1516) { - deployerPrivateKey = vm.envUint("TESTNET_PRIVATEKEY"); - deployer = vm.envAddress("TESTNET_DEPLOYER_ADDRESS"); - multisig = vm.envAddress("TESTNET_MULTISIG_ADDRESS"); + } else if (block.chainid == 1315 || block.chainid == 1514 || block.chainid == 1516) { + deployerPrivateKey = vm.envUint("STORY_PRIVATEKEY"); + deployer = vm.envAddress("STORY_DEPLOYER_ADDRESS"); + multisig = vm.envAddress("STORY_MULTISIG_ADDRESS"); vm.startBroadcast(deployerPrivateKey); } else if (block.chainid == 31337) { multisig = address(0x456); diff --git a/script/utils/DeployHelper.sol b/script/utils/DeployHelper.sol index 58f8b12..5f3ca16 100644 --- a/script/utils/DeployHelper.sol +++ b/script/utils/DeployHelper.sol @@ -8,6 +8,7 @@ import { Script } from "forge-std/Script.sol"; import { stdJson } from "forge-std/StdJson.sol"; import { ERC6551Registry } from "erc6551/ERC6551Registry.sol"; import { AccessManager } from "@openzeppelin/contracts/access/manager/AccessManager.sol"; +import { BeaconProxy } from "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol"; import { UpgradeableBeacon } from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; import { ICreate3Deployer } from "@storyprotocol/script/utils/ICreate3Deployer.sol"; import { AccessController } from "@storyprotocol/core/access/AccessController.sol"; @@ -32,7 +33,11 @@ import { RoyaltyModule } from "@storyprotocol/core/modules/royalty/RoyaltyModule import { RoyaltyPolicyLAP } from "@storyprotocol/core/modules/royalty/policies/LAP/RoyaltyPolicyLAP.sol"; import { RoyaltyPolicyLRP } from "@storyprotocol/core/modules/royalty/policies/LRP/RoyaltyPolicyLRP.sol"; import { StorageLayoutChecker } from "@storyprotocol/script/utils/upgrades/StorageLayoutCheck.s.sol"; +import { AccessManager } from "@openzeppelin/contracts/access/manager/AccessManager.sol"; +import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import { TestProxyHelper } from "@storyprotocol/test/utils/TestProxyHelper.sol"; +import { ProtocolAdmin } from "@storyprotocol/core/lib/ProtocolAdmin.sol"; +import { ProtocolPausableUpgradeable } from "@storyprotocol/core/pause/ProtocolPausableUpgradeable.sol"; // contracts import { SPGNFT } from "../../contracts/SPGNFT.sol"; @@ -47,6 +52,8 @@ import { StoryBadgeNFT } from "../../contracts/story-nft/StoryBadgeNFT.sol"; import { OrgStoryNFTFactory } from "../../contracts/story-nft/OrgStoryNFTFactory.sol"; import { OwnableERC20 } from "../../contracts/modules/tokenizer/OwnableERC20.sol"; import { TokenizerModule } from "../../contracts/modules/tokenizer/TokenizerModule.sol"; +import { LockLicenseHook } from "../../contracts/hooks/LockLicenseHook.sol"; +import { TotalLicenseTokenLimitHook } from "../../contracts/hooks/TotalLicenseTokenLimitHook.sol"; // script import { BroadcastManager } from "./BroadcastManager.s.sol"; @@ -73,7 +80,15 @@ contract DeployHelper is bytes32 internal constant IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; // WIP address - address internal wipAddr = 0x1516000000000000000000000000000000000000; + address internal wipAddr = 0x1514000000000000000000000000000000000000; + + // IPAccount contracts + IPAccountImpl internal ipAccountImplCode; + UpgradeableBeacon internal ipAccountImplBeacon; + BeaconProxy internal ipAccountImpl; + string internal constant IP_ACCOUNT_IMPL_CODE = "IPAccountImplCode"; + string internal constant IP_ACCOUNT_IMPL_BEACON = "IPAccountImplBeacon"; + string internal constant IP_ACCOUNT_IMPL_BEACON_PROXY = "IPAccountImplBeaconProxy"; error DeploymentConfigError(string message); @@ -105,6 +120,10 @@ contract DeployHelper is address internal ownableERC20Template; address internal ownableERC20Beacon; + // LicensingHooks + LockLicenseHook internal lockLicenseHook; + TotalLicenseTokenLimitHook internal totalLicenseTokenLimitHook; + // DeployHelper variable bool internal writeDeploys; @@ -178,10 +197,6 @@ contract DeployHelper is if (writeDeploys) _writeDeployment(); // JsonDeploymentHandler.s.sol _endBroadcast(); // BroadcastManager.s.sol - - // Set SPGNFTBeacon for periphery workflow contracts, access controlled - // can't be done in deployment script: - // registrationWorkflows.setNftContractBeacon(address(spgNftBeacon)); } } @@ -490,7 +505,6 @@ contract DeployHelper is ) )); _postdeploy("OwnableERC20Beacon", address(ownableERC20Beacon)); - require( UpgradeableBeacon(ownableERC20Beacon).implementation() == address(ownableERC20Template), "DeployHelper: Invalid beacon implementation" @@ -499,14 +513,62 @@ contract DeployHelper is OwnableERC20(ownableERC20Template).upgradableBeacon() == address(ownableERC20Beacon), "DeployHelper: Invalid beacon address in template" ); + + // LicensingHooks + _predeploy("LockLicenseHook"); + lockLicenseHook = LockLicenseHook( + create3Deployer.deployDeterministic( + abi.encodePacked( + type(LockLicenseHook).creationCode + ), + _getSalt("LockLicenseHook") + ) + ); + _postdeploy("LockLicenseHook", address(lockLicenseHook)); + + _predeploy("TotalLicenseTokenLimitHook"); + totalLicenseTokenLimitHook = TotalLicenseTokenLimitHook( + create3Deployer.deployDeterministic( + abi.encodePacked( + type(TotalLicenseTokenLimitHook).creationCode, + abi.encode( + licenseRegistryAddr, + licenseTokenAddr, + accessControllerAddr, + ipAssetRegistryAddr + ) + ), + _getSalt("TotalLicenseTokenLimitHook") + ) + ); + _postdeploy("TotalLicenseTokenLimitHook", address(totalLicenseTokenLimitHook)); } function _configurePeripheryContracts() private { // Transfer ownership of beacon proxy to RegistrationWorkflows spgNftBeacon.transferOwnership(address(registrationWorkflows)); + registrationWorkflows.setNftContractBeacon(address(spgNftBeacon)); tokenizerModule.whitelistTokenTemplate(address(ownableERC20Template), true); IModuleRegistry(moduleRegistryAddr).registerModule("TOKENIZER_MODULE", address(tokenizerModule)); - // more configurations may be added here + IModuleRegistry(moduleRegistryAddr).registerModule("LOCK_LICENSE_HOOK", address(lockLicenseHook)); + IModuleRegistry(moduleRegistryAddr).registerModule("TOTAL_LICENSE_TOKEN_LIMIT_HOOK", address(totalLicenseTokenLimitHook)); + // add upgrade role and pause role to tokenizer module + bytes4[] memory selectors = new bytes4[](1); + selectors[0] = UUPSUpgradeable.upgradeToAndCall.selector; + AccessManager(protocolAccessManagerAddr).setTargetFunctionRole( + address(tokenizerModule), + selectors, + ProtocolAdmin.UPGRADER_ROLE + ); + + selectors = new bytes4[](2); + selectors[0] = ProtocolPausableUpgradeable.pause.selector; + selectors[1] = ProtocolPausableUpgradeable.unpause.selector; + AccessManager(protocolAccessManagerAddr).setTargetFunctionRole( + address(tokenizerModule), + selectors, + ProtocolAdmin.PAUSE_ADMIN_ROLE + ); } function _deployMockCoreContracts() private { @@ -551,8 +613,9 @@ contract DeployHelper is impl = address( new IPAssetRegistry( address(erc6551Registry), - _getDeployedAddress(type(IPAccountImpl).name), - _getDeployedAddress(type(GroupingModule).name) + _getDeployedAddress(IP_ACCOUNT_IMPL_BEACON_PROXY), + _getDeployedAddress(type(GroupingModule).name), + _getDeployedAddress(IP_ACCOUNT_IMPL_BEACON) ) ); ipAssetRegistry = IPAssetRegistry( @@ -614,8 +677,8 @@ contract DeployHelper is ); require(_loadProxyImpl(address(licenseRegistry)) == impl, "LicenseRegistry Proxy Implementation Mismatch"); - // ipAccountImpl - bytes memory ipAccountImplCode = abi.encodePacked( + // IPAccountImpl contracts + bytes memory ipAccountImplCodeBytes = abi.encodePacked( type(IPAccountImpl).creationCode, abi.encode( address(accessController), @@ -624,21 +687,44 @@ contract DeployHelper is address(moduleRegistry) ) ); - IPAccountImpl ipAccountImpl = IPAccountImpl( - payable(create3Deployer.deployDeterministic( - ipAccountImplCode, - _getSalt(type(IPAccountImpl).name) - )) + _predeploy(IP_ACCOUNT_IMPL_CODE); + ipAccountImplCode = IPAccountImpl( + payable(create3Deployer.deployDeterministic(ipAccountImplCodeBytes, _getSalt(IP_ACCOUNT_IMPL_CODE))) ); + _postdeploy(IP_ACCOUNT_IMPL_CODE, address(ipAccountImplCode)); require( - _getDeployedAddress(type(IPAccountImpl).name) == address(ipAccountImpl), - "Deploy: IP Account Impl Address Mismatch" + _getDeployedAddress(IP_ACCOUNT_IMPL_CODE) == address(ipAccountImplCode), + "Deploy: IP Account Impl Code Address Mismatch" + ); + + _predeploy(IP_ACCOUNT_IMPL_BEACON); + ipAccountImplBeacon = UpgradeableBeacon( + create3Deployer.deployDeterministic( + abi.encodePacked( + type(UpgradeableBeacon).creationCode, + abi.encode(address(ipAccountImplCode), deployer) + ), + _getSalt(IP_ACCOUNT_IMPL_BEACON) + ) ); + _postdeploy(IP_ACCOUNT_IMPL_BEACON, address(ipAccountImplBeacon)); + + _predeploy(IP_ACCOUNT_IMPL_BEACON_PROXY); + ipAccountImpl = BeaconProxy(payable( + create3Deployer.deployDeterministic( + abi.encodePacked( + type(BeaconProxy).creationCode, + abi.encode(address(ipAccountImplBeacon), "") + ), + _getSalt(IP_ACCOUNT_IMPL_BEACON_PROXY) + )) + ); + _postdeploy(IP_ACCOUNT_IMPL_BEACON_PROXY, address(ipAccountImpl)); // disputeModule impl = address(0); // Make sure we don't deploy wrong impl impl = address( - new DisputeModule(address(accessController), address(ipAssetRegistry), address(licenseRegistry)) + new DisputeModule(address(accessController), address(ipAssetRegistry), address(licenseRegistry), address(ipGraphACL)) ); disputeModule = DisputeModule( TestProxyHelper.deployUUPSProxy( @@ -663,7 +749,8 @@ contract DeployHelper is _getDeployedAddress(type(LicensingModule).name), address(disputeModule), address(licenseRegistry), - address(ipAssetRegistry) + address(ipAssetRegistry), + address(ipGraphACL) ) ); royaltyModule = RoyaltyModule( @@ -671,7 +758,7 @@ contract DeployHelper is create3Deployer, _getSalt(type(RoyaltyModule).name), impl, - abi.encodeCall(RoyaltyModule.initialize, (address(protocolAccessManager), 1024, 1024, 10)) + abi.encodeCall(RoyaltyModule.initialize, (address(protocolAccessManager), uint256(15))) ) ); royaltyModuleAddr = address(royaltyModule); @@ -691,7 +778,8 @@ contract DeployHelper is address(royaltyModule), address(licenseRegistry), address(disputeModule), - _getDeployedAddress(type(LicenseToken).name) + _getDeployedAddress(type(LicenseToken).name), + address(ipGraphACL) ) ); licensingModule = LicensingModule( @@ -797,7 +885,8 @@ contract DeployHelper is address(accessController), address(ipAccountRegistry), address(licenseRegistry), - address(royaltyModule) + address(royaltyModule), + address(moduleRegistry) ) ); pilTemplate = PILicenseTemplate( @@ -929,9 +1018,12 @@ contract DeployHelper is moduleRegistry.registerModule("CORE_METADATA_VIEW_MODULE", address(coreMetadataViewModule)); moduleRegistry.registerModule("GROUPING_MODULE", address(groupingModule)); - ipGraphACL.whitelistAddress(_getDeployedAddress(type(RoyaltyPolicyLAP).name)); - ipGraphACL.whitelistAddress(_getDeployedAddress(type(RoyaltyPolicyLRP).name)); - ipGraphACL.whitelistAddress(_getDeployedAddress(type(LicenseRegistry).name)); + ipGraphACL.whitelistAddress(address(licenseRegistry)); + ipGraphACL.whitelistAddress(address(royaltyPolicyLAP)); + ipGraphACL.whitelistAddress(address(royaltyPolicyLRP)); + ipGraphACL.whitelistAddress(address(royaltyModule)); + ipGraphACL.whitelistAddress(address(disputeModule)); + ipGraphACL.whitelistAddress(address(licensingModule)); coreMetadataViewModule.updateCoreMetadataModule(); diff --git a/test/SPGNFT.t.sol b/test/SPGNFT.t.sol index e390a00..7829200 100644 --- a/test/SPGNFT.t.sol +++ b/test/SPGNFT.t.sol @@ -370,4 +370,33 @@ contract SPGNFTTest is BaseTest { vm.stopPrank(); } + + function test_SPGNFT_setMintFeeRecipient() public { + // alice is admin and fee recipient + vm.startPrank(u.alice); + nftContract.setMintFeeRecipient(u.bob); + vm.stopPrank(); + + // alice is admin, bob is fee recipient + vm.startPrank(u.bob); + nftContract.setMintFeeRecipient(u.carl); + vm.stopPrank(); + + // alice is admin, carl is fee recipient + vm.startPrank(u.alice); + nftContract.setMintFeeRecipient(u.bob); + vm.stopPrank(); + } + + function test_SPGNFT_setMintFeeRecipient_revert_callerNotFeeRecipientOrAdmin() public { + vm.startPrank(u.bob); + vm.expectRevert(Errors.SPGNFT__CallerNotFeeRecipientOrAdmin.selector); + nftContract.setMintFeeRecipient(u.carl); + vm.stopPrank(); + + vm.startPrank(u.carl); + vm.expectRevert(Errors.SPGNFT__CallerNotFeeRecipientOrAdmin.selector); + nftContract.setMintFeeRecipient(u.bob); + vm.stopPrank(); + } } diff --git a/test/hooks/LockLicenseHook.t.sol b/test/hooks/LockLicenseHook.t.sol index 3bcbdc0..ba7b074 100644 --- a/test/hooks/LockLicenseHook.t.sol +++ b/test/hooks/LockLicenseHook.t.sol @@ -21,11 +21,9 @@ contract LockLicenseHookTest is BaseTest { function test_LockLicenseHook_revert_beforeMintLicenseTokens() public { uint256 socialRemixTermsId = pilTemplate.registerLicenseTerms(PILFlavors.nonCommercialSocialRemixing()); - LockLicenseHook lockLicenseHook = new LockLicenseHook(); - vm.prank(u.admin); - moduleRegistry.registerModule("LockLicenseHook", address(lockLicenseHook)); vm.startPrank(ipOwner); + licensingModule.attachLicenseTerms(ipId, address(pilTemplate), socialRemixTermsId); Licensing.LicensingConfig memory licensingConfig = Licensing.LicensingConfig({ isSet: true, mintingFee: 0, @@ -62,11 +60,9 @@ contract LockLicenseHookTest is BaseTest { function test_LockLicenseHook_revert_beforeRegisterDerivative() public { uint256 socialRemixTermsId = pilTemplate.registerLicenseTerms(PILFlavors.nonCommercialSocialRemixing()); - LockLicenseHook lockLicenseHook = new LockLicenseHook(); - vm.prank(u.admin); - moduleRegistry.registerModule("LockLicenseHook", address(lockLicenseHook)); vm.startPrank(ipOwner); + licensingModule.attachLicenseTerms(ipId, address(pilTemplate), socialRemixTermsId); Licensing.LicensingConfig memory licensingConfig = Licensing.LicensingConfig({ isSet: true, mintingFee: 0, @@ -109,14 +105,12 @@ contract LockLicenseHookTest is BaseTest { function test_LockLicenseHook_calculateMintingFee() public { uint256 socialRemixTermsId = pilTemplate.registerLicenseTerms(PILFlavors.nonCommercialSocialRemixing()); - LockLicenseHook lockLicenseHook = new LockLicenseHook(); - vm.prank(u.admin); - moduleRegistry.registerModule("LockLicenseHook", address(lockLicenseHook)); vm.startPrank(ipOwner); + licensingModule.attachLicenseTerms(ipId, address(pilTemplate), socialRemixTermsId); Licensing.LicensingConfig memory licensingConfig = Licensing.LicensingConfig({ isSet: true, - mintingFee: 1000, + mintingFee: 0, licensingHook: address(lockLicenseHook), hookData: "", commercialRevShare: 0, diff --git a/test/hooks/TotalLicenseTokenLimitHook.t.sol b/test/hooks/TotalLicenseTokenLimitHook.t.sol index 65eb5ad..b49cefd 100644 --- a/test/hooks/TotalLicenseTokenLimitHook.t.sol +++ b/test/hooks/TotalLicenseTokenLimitHook.t.sol @@ -44,19 +44,10 @@ contract TotalLicenseTokenLimitHookTest is BaseTest { } function test_TotalLicenseTokenLimitHook_setLimit() public { - TotalLicenseTokenLimitHook totalLimitHook = new TotalLicenseTokenLimitHook( - address(licenseRegistry), - address(licenseToken), - address(accessController), - address(ipAssetRegistry) - ); - - vm.prank(u.admin); - moduleRegistry.registerModule("TotalLicenseTokenLimitHook", address(totalLimitHook)); Licensing.LicensingConfig memory licensingConfig = Licensing.LicensingConfig({ isSet: true, mintingFee: 100, - licensingHook: address(totalLimitHook), + licensingHook: address(totalLicenseTokenLimitHook), hookData: "", commercialRevShare: 0, disabled: false, @@ -66,19 +57,19 @@ contract TotalLicenseTokenLimitHookTest is BaseTest { vm.startPrank(ipOwner1); licensingModule.setLicensingConfig(ipId1, address(pilTemplate), commUseTermsId, licensingConfig); - totalLimitHook.setTotalLicenseTokenLimit(ipId1, address(pilTemplate), commUseTermsId, 10); - assertEq(totalLimitHook.getTotalLicenseTokenLimit(ipId1, address(pilTemplate), commUseTermsId), 10); + totalLicenseTokenLimitHook.setTotalLicenseTokenLimit(ipId1, address(pilTemplate), commUseTermsId, 10); + assertEq(totalLicenseTokenLimitHook.getTotalLicenseTokenLimit(ipId1, address(pilTemplate), commUseTermsId), 10); vm.stopPrank(); vm.startPrank(ipOwner2); - licensingModule.setLicensingConfig(ipId2, address(0), 0, licensingConfig); - totalLimitHook.setTotalLicenseTokenLimit(ipId2, address(pilTemplate), commUseTermsId, 20); - assertEq(totalLimitHook.getTotalLicenseTokenLimit(ipId2, address(pilTemplate), commUseTermsId), 20); + licensingModule.setLicensingConfig(ipId2, address(pilTemplate), commUseTermsId, licensingConfig); + totalLicenseTokenLimitHook.setTotalLicenseTokenLimit(ipId2, address(pilTemplate), commUseTermsId, 20); + assertEq(totalLicenseTokenLimitHook.getTotalLicenseTokenLimit(ipId2, address(pilTemplate), commUseTermsId), 20); vm.stopPrank(); vm.startPrank(ipOwner3); licensingModule.setLicensingConfig(ipId3, address(pilTemplate), commUseTermsId, licensingConfig); - assertEq(totalLimitHook.getTotalLicenseTokenLimit(ipId3, address(pilTemplate), commUseTermsId), 0); + assertEq(totalLicenseTokenLimitHook.getTotalLicenseTokenLimit(ipId3, address(pilTemplate), commUseTermsId), 0); vm.stopPrank(); licensingModule.mintLicenseTokens({ @@ -152,19 +143,10 @@ contract TotalLicenseTokenLimitHookTest is BaseTest { } function test_TotalLicenseTokenLimitHook_revert_nonIpOwner_setLimit() public { - TotalLicenseTokenLimitHook totalLimitHook = new TotalLicenseTokenLimitHook( - address(licenseRegistry), - address(licenseToken), - address(accessController), - address(ipAssetRegistry) - ); - - vm.prank(u.admin); - moduleRegistry.registerModule("TotalLicenseTokenLimitHook", address(totalLimitHook)); Licensing.LicensingConfig memory licensingConfig = Licensing.LicensingConfig({ isSet: true, mintingFee: 100, - licensingHook: address(totalLimitHook), + licensingHook: address(totalLicenseTokenLimitHook), hookData: "", commercialRevShare: 0, disabled: false, @@ -174,8 +156,8 @@ contract TotalLicenseTokenLimitHookTest is BaseTest { vm.startPrank(ipOwner1); licensingModule.setLicensingConfig(ipId1, address(pilTemplate), commUseTermsId, licensingConfig); - totalLimitHook.setTotalLicenseTokenLimit(ipId1, address(pilTemplate), commUseTermsId, 10); - assertEq(totalLimitHook.getTotalLicenseTokenLimit(ipId1, address(pilTemplate), commUseTermsId), 10); + totalLicenseTokenLimitHook.setTotalLicenseTokenLimit(ipId1, address(pilTemplate), commUseTermsId, 10); + assertEq(totalLicenseTokenLimitHook.getTotalLicenseTokenLimit(ipId1, address(pilTemplate), commUseTermsId), 10); vm.stopPrank(); vm.startPrank(ipOwner2); @@ -184,27 +166,18 @@ contract TotalLicenseTokenLimitHookTest is BaseTest { Errors.AccessController__PermissionDenied.selector, ipId1, ipOwner2, - address(totalLimitHook), - totalLimitHook.setTotalLicenseTokenLimit.selector + address(totalLicenseTokenLimitHook), + totalLicenseTokenLimitHook.setTotalLicenseTokenLimit.selector ) ); - totalLimitHook.setTotalLicenseTokenLimit(ipId1, address(pilTemplate), commUseTermsId, 20); + totalLicenseTokenLimitHook.setTotalLicenseTokenLimit(ipId1, address(pilTemplate), commUseTermsId, 20); } function test_TotalLicenseTokenLimitHook_revert_limitLowerThanTotalSupply_setLimit() public { - TotalLicenseTokenLimitHook totalLimitHook = new TotalLicenseTokenLimitHook( - address(licenseRegistry), - address(licenseToken), - address(accessController), - address(ipAssetRegistry) - ); - - vm.prank(u.admin); - moduleRegistry.registerModule("TotalLicenseTokenLimitHook", address(totalLimitHook)); Licensing.LicensingConfig memory licensingConfig = Licensing.LicensingConfig({ isSet: true, mintingFee: 100, - licensingHook: address(totalLimitHook), + licensingHook: address(totalLicenseTokenLimitHook), hookData: "", commercialRevShare: 0, disabled: false, @@ -214,8 +187,8 @@ contract TotalLicenseTokenLimitHookTest is BaseTest { vm.startPrank(ipOwner1); licensingModule.setLicensingConfig(ipId1, address(pilTemplate), commUseTermsId, licensingConfig); - totalLimitHook.setTotalLicenseTokenLimit(ipId1, address(pilTemplate), commUseTermsId, 10); - assertEq(totalLimitHook.getTotalLicenseTokenLimit(ipId1, address(pilTemplate), commUseTermsId), 10); + totalLicenseTokenLimitHook.setTotalLicenseTokenLimit(ipId1, address(pilTemplate), commUseTermsId, 10); + assertEq(totalLicenseTokenLimitHook.getTotalLicenseTokenLimit(ipId1, address(pilTemplate), commUseTermsId), 10); licensingModule.mintLicenseTokens({ licensorIpId: ipId1, @@ -235,6 +208,6 @@ contract TotalLicenseTokenLimitHookTest is BaseTest { 5 ) ); - totalLimitHook.setTotalLicenseTokenLimit(ipId1, address(pilTemplate), commUseTermsId, 5); + totalLicenseTokenLimitHook.setTotalLicenseTokenLimit(ipId1, address(pilTemplate), commUseTermsId, 5); } } diff --git a/test/integration/BaseIntegration.t.sol b/test/integration/BaseIntegration.t.sol index c3db37c..d0a2141 100644 --- a/test/integration/BaseIntegration.t.sol +++ b/test/integration/BaseIntegration.t.sol @@ -60,7 +60,7 @@ contract BaseIntegration is Test, Script, StoryProtocolCoreAddressManager, Story RoyaltyTokenDistributionWorkflows internal royaltyTokenDistributionWorkflows; /// @dev Story USD - SUSD internal StoryUSD = SUSD(0x48D80f8b87F7f1B6f2fBF3A7C45Eb7De6C8374F9); + SUSD internal StoryUSD = SUSD(0xF2104833d386a2734a4eB3B8ad6FC6812F29E38E); /// @dev Test data string internal testCollectionName; diff --git a/test/integration/workflows/GroupingIntegration.t.sol b/test/integration/workflows/GroupingIntegration.t.sol index 05976b0..4c2535b 100644 --- a/test/integration/workflows/GroupingIntegration.t.sol +++ b/test/integration/workflows/GroupingIntegration.t.sol @@ -72,6 +72,7 @@ contract GroupingIntegration is BaseIntegration { spgNftContract: address(spgNftContract), groupId: groupId, recipient: testSender, + maxAllowedRewardShare: 100e6, // 100% ipMetadata: testIpMetadata, licensesData: testLicensesData, sigAddToGroup: WorkflowStructs.SignatureData({ @@ -138,6 +139,7 @@ contract GroupingIntegration is BaseIntegration { nftContract: address(spgNftContract), tokenId: tokenId, groupId: groupId, + maxAllowedRewardShare: 100e6, // 100% licensesData: testLicensesData, ipMetadata: testIpMetadata, sigMetadataAndAttachAndConfig: WorkflowStructs.SignatureData({ @@ -188,6 +190,7 @@ contract GroupingIntegration is BaseIntegration { address newGroupId = groupingWorkflows.registerGroupAndAttachLicenseAndAddIps({ groupPool: evenSplitGroupPoolAddr, ipIds: ipIds, + maxAllowedRewardShare: 100e6, // 100% licenseData: testGroupLicenseData[0] }); @@ -213,6 +216,7 @@ contract GroupingIntegration is BaseIntegration { address newGroupId = groupingWorkflows.registerGroupAndAttachLicenseAndAddIps({ groupPool: evenSplitGroupPoolAddr, ipIds: ipIds, + maxAllowedRewardShare: 100e6, // 100% licenseData: testGroupLicenseData[0] }); @@ -264,13 +268,13 @@ contract GroupingIntegration is BaseIntegration { StoryUSD.mint(testSender, amount1); StoryUSD.approve(address(royaltyModule), amount1); royaltyModule.payRoyaltyOnBehalf(ipId1, testSender, address(StoryUSD), amount1); - IGraphAwareRoyaltyPolicy(royaltyPolicyLAPAddr).transferToVault(ipId1, newGroupId, address(StoryUSD)); + IGraphAwareRoyaltyPolicy(royaltyPolicyLRPAddr).transferToVault(ipId1, newGroupId, address(StoryUSD)); uint256 amount2 = 10_000 * 10 ** StoryUSD.decimals(); // 10,000 tokens StoryUSD.mint(testSender, amount2); StoryUSD.approve(address(royaltyModule), amount2); royaltyModule.payRoyaltyOnBehalf(ipId2, testSender, address(StoryUSD), amount2); - IGraphAwareRoyaltyPolicy(royaltyPolicyLAPAddr).transferToVault(ipId2, newGroupId, address(StoryUSD)); + IGraphAwareRoyaltyPolicy(royaltyPolicyLRPAddr).transferToVault(ipId2, newGroupId, address(StoryUSD)); address[] memory royaltyTokens = new address[](1); royaltyTokens[0] = address(StoryUSD); @@ -321,12 +325,13 @@ contract GroupingIntegration is BaseIntegration { data[i] = abi.encodeWithSelector( bytes4( keccak256( - "mintAndRegisterIpAndAttachLicenseAndAddToGroup(address,address,address,(address,uint256,(bool,uint256,address,bytes,uint32,bool,uint32,address))[],(string,bytes32,string,bytes32),(address,uint256,bytes),bool)" + "mintAndRegisterIpAndAttachLicenseAndAddToGroup(address,address,address,uint256,(address,uint256,(bool,uint256,address,bytes,uint32,bool,uint32,address))[],(string,bytes32,string,bytes32),(address,uint256,bytes),bool)" ) ), address(spgNftContract), groupId, testSender, + 100e6, // 100% testLicensesData, testIpMetadata, WorkflowStructs.SignatureData({ signer: testSender, deadline: deadline, signature: sigsAddToGroup[i] }) @@ -421,12 +426,13 @@ contract GroupingIntegration is BaseIntegration { data[i] = abi.encodeWithSelector( 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))" + "registerIpAndAttachLicenseAndAddToGroup(address,uint256,address,uint256,(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, + 100e6, // 100% testLicensesData, testIpMetadata, WorkflowStructs.SignatureData({ @@ -467,7 +473,7 @@ contract GroupingIntegration is BaseIntegration { PILFlavors.commercialRemix({ mintingFee: 0, commercialRevShare: revShare, - royaltyPolicy: royaltyPolicyLAPAddr, + royaltyPolicy: royaltyPolicyLRPAddr, currencyToken: address(StoryUSD) }) ), diff --git a/test/integration/workflows/LicenseAttachmentIntegration.t.sol b/test/integration/workflows/LicenseAttachmentIntegration.t.sol index adfae6f..78e2ef7 100644 --- a/test/integration/workflows/LicenseAttachmentIntegration.t.sol +++ b/test/integration/workflows/LicenseAttachmentIntegration.t.sol @@ -30,6 +30,8 @@ contract LicenseAttachmentIntegration is BaseIntegration { _test_LicenseAttachmentIntegration_registerPILTermsAndAttach(); _test_LicenseAttachmentIntegration_mintAndRegisterIpAndAttachPILTerms(); _test_LicenseAttachmentIntegration_registerIpAndAttachPILTerms(); + _test_LicenseAttachmentIntegration_mintAndRegisterIpAndAttachDefaultTerms(); + _test_LicenseAttachmentIntegration_registerIpAndAttachDefaultTerms(); _endBroadcast(); } @@ -180,6 +182,79 @@ contract LicenseAttachmentIntegration is BaseIntegration { } } + function _test_LicenseAttachmentIntegration_mintAndRegisterIpAndAttachDefaultTerms() + private + logTest("test_LicenseAttachmentWorkflows_mintAndRegisterIpAndAttachDefaultTerms") + { + StoryUSD.mint(testSender, testMintFee); + StoryUSD.approve(address(spgNftContract), testMintFee); + + (address ipId1, uint256 tokenId1) = licenseAttachmentWorkflows.mintAndRegisterIpAndAttachDefaultTerms({ + spgNftContract: address(spgNftContract), + recipient: testSender, + ipMetadata: testIpMetadata, + allowDuplicates: true + }); + assertTrue(ipAssetRegistry.isRegistered(ipId1)); + assertEq(tokenId1, spgNftContract.totalSupply()); + assertEq(spgNftContract.tokenURI(tokenId1), string.concat(testBaseURI, testIpMetadata.nftMetadataURI)); + assertMetadata(ipId1, testIpMetadata); + (address defaultLicenseTemplate, uint256 defaultLicenseTermsId) = licenseRegistry.getDefaultLicenseTerms(); + (address licenseTemplate, uint256 licenseTermsId) = licenseRegistry.getAttachedLicenseTerms(ipId1, 0); + assertEq(defaultLicenseTemplate, defaultLicenseTemplate); + assertEq(defaultLicenseTermsId, defaultLicenseTermsId); + assertEq(licenseTermsId, defaultLicenseTermsId); + } + + function _test_LicenseAttachmentIntegration_registerIpAndAttachDefaultTerms() + private + logTest("test_LicenseAttachmentWorkflows_registerIpAndAttachDefaultTerms") + { + StoryUSD.mint(testSender, testMintFee); + StoryUSD.approve(address(spgNftContract), testMintFee); + + uint256 tokenId = spgNftContract.mint({ + to: testSender, + nftMetadataURI: "", + nftMetadataHash: bytes32(0), + allowDuplicates: true + }); + address expectedIpId = ipAssetRegistry.ipId(block.chainid, address(spgNftContract), tokenId); + + uint256 deadline = block.timestamp + 1000; + + (bytes memory sigMetadataAndAttachAndConfig, bytes32 expectedState, ) = _getSetBatchPermissionSigForPeriphery({ + ipId: expectedIpId, + permissionList: _getMetadataAndAttachTermsAndConfigPermissionList( + expectedIpId, + licenseAttachmentWorkflowsAddr + ), + deadline: deadline, + state: bytes32(0), + signerSk: testSenderSk + }); + + address ipId = licenseAttachmentWorkflows.registerIpAndAttachDefaultTerms({ + nftContract: address(spgNftContract), + tokenId: tokenId, + ipMetadata: testIpMetadata, + sigMetadataAndDefaultTerms: WorkflowStructs.SignatureData({ + signer: testSender, + deadline: deadline, + signature: sigMetadataAndAttachAndConfig + }) + }); + + assertEq(ipId, expectedIpId); + assertTrue(ipAssetRegistry.isRegistered(ipId)); + assertEq(IIPAccount(payable(ipId)).state(), expectedState); + (address defaultLicenseTemplate, uint256 defaultLicenseTermsId) = licenseRegistry.getDefaultLicenseTerms(); + (address licenseTemplate, uint256 licenseTermsId) = licenseRegistry.getAttachedLicenseTerms(ipId, 0); + assertEq(defaultLicenseTemplate, defaultLicenseTemplate); + assertEq(defaultLicenseTermsId, defaultLicenseTermsId); + assertEq(licenseTermsId, defaultLicenseTermsId); + } + function _setUpTest() private { spgNftContract = ISPGNFT( registrationWorkflows.createCollection( diff --git a/test/integration/workflows/RoyaltyTokenDistributionIntegration.t.sol b/test/integration/workflows/RoyaltyTokenDistributionIntegration.t.sol index abc39a4..8fe6307 100644 --- a/test/integration/workflows/RoyaltyTokenDistributionIntegration.t.sol +++ b/test/integration/workflows/RoyaltyTokenDistributionIntegration.t.sol @@ -31,6 +31,12 @@ contract RoyaltyTokenDistributionIntegration is BaseIntegration { WorkflowStructs.LicenseTermsData[] private commRemixTermsData; WorkflowStructs.RoyaltyShare[] private royaltyShares; + // random addresses + address private shareRecipientA = 0xfD6BC5A922Df6Fa2034d97958C5401023B21641B; + address private shareRecipientB = 0x3D1f17203f8B6918D1B96CE195920e768AB7a9aB; + address private shareRecipientC = 0x021CBD607beeCA2ACecBD8533D822f5Ca70169f3; + address private shareRecipientD = 0x5Bf05b423a1D090522700a3D5609D1FBbD690e76; + /// @dev To use, run the following command: /// forge script test/integration/workflows/RoyaltyTokenDistributionIntegration.t.sol:RoyaltyTokenDistributionIntegration \ /// --rpc-url=$TESTNET_URL -vvvv --broadcast --priority-gas-price=1 --legacy @@ -246,10 +252,10 @@ contract RoyaltyTokenDistributionIntegration is BaseIntegration { function _setupTest() private { ipMetadata = WorkflowStructs.IPMetadata({ - ipMetadataURI: "", - ipMetadataHash: "", - nftMetadataURI: "", - nftMetadataHash: "" + ipMetadataURI: "test-ip-uri", + ipMetadataHash: "test-ip-hash", + nftMetadataURI: "test-nft-uri", + nftMetadataHash: "test-nft-hash" }); spgNftContract = ISPGNFT( @@ -364,28 +370,28 @@ contract RoyaltyTokenDistributionIntegration is BaseIntegration { royaltyShares.push( WorkflowStructs.RoyaltyShare({ - recipient: testSender, + recipient: shareRecipientA, percentage: 50_000_000 // 50% }) ); royaltyShares.push( WorkflowStructs.RoyaltyShare({ - recipient: testSender, + recipient: shareRecipientB, percentage: 20_000_000 // 20% }) ); royaltyShares.push( WorkflowStructs.RoyaltyShare({ - recipient: testSender, + recipient: shareRecipientC, percentage: 20_000_000 // 20% }) ); royaltyShares.push( WorkflowStructs.RoyaltyShare({ - recipient: testSender, + recipient: shareRecipientD, percentage: 5_000_000 // 5% }) ); @@ -393,33 +399,7 @@ contract RoyaltyTokenDistributionIntegration is BaseIntegration { 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 - ); + assertTrue(licenseRegistry.hasIpAttachedLicenseTerms(ipId, address(pilTemplate), licenseTermsIds[i])); } } diff --git a/test/utils/BaseTest.t.sol b/test/utils/BaseTest.t.sol index c22d371..13a93d3 100644 --- a/test/utils/BaseTest.t.sol +++ b/test/utils/BaseTest.t.sol @@ -318,6 +318,32 @@ contract BaseTest is Test, DeployHelper { } } + /// @dev Get the permission list for setting metadata and attaching default license terms for the IP. + /// @param ipId The ID of the IP that the permissions are for. + /// @param to The address of the periphery contract to receive the permission. + /// @return permissionList The list of permissions for setting metadata and attaching default license terms. + function _getMetadataAndDefaultTermsPermissionList( + address ipId, + address to + ) internal view returns (AccessPermission.Permission[] memory permissionList) { + address[] memory modules = new address[](2); + bytes4[] memory selectors = new bytes4[](2); + permissionList = new AccessPermission.Permission[](2); + modules[0] = coreMetadataModuleAddr; + modules[1] = licensingModuleAddr; + selectors[0] = ICoreMetadataModule.setAll.selector; + selectors[1] = ILicensingModule.attachDefaultLicenseTerms.selector; + for (uint256 i = 0; i < 2; i++) { + permissionList[i] = AccessPermission.Permission({ + ipAccount: ipId, + signer: to, + to: modules[i], + func: selectors[i], + permission: AccessPermission.ALLOW + }); + } + } + /// @dev Get the permission list for attaching license terms and setting licensing config for the IP. /// @param ipId The ID of the IP that the permissions are for. /// @param to The address of the periphery contract to receive the permission. diff --git a/test/workflows/DerivativeWorkflows.t.sol b/test/workflows/DerivativeWorkflows.t.sol index d202571..8128f7f 100644 --- a/test/workflows/DerivativeWorkflows.t.sol +++ b/test/workflows/DerivativeWorkflows.t.sol @@ -527,4 +527,366 @@ contract DerivativeWorkflowsTest is BaseTest { expectedParentIndex: 0 }); } + + //////////////////////////////////////////////////////////////////////////// + // DEPRECATED, WILL BE REMOVED IN V1.4 // + //////////////////////////////////////////////////////////////////////////// + + function test_DerivativeWorkflows_mintAndRegisterIpAndMakeDerivative_withNonCommercialLicense_DEPR() + public + withCollection + whenCallerHasMinterRole + withEnoughTokens(address(derivativeWorkflows)) + withNonCommercialParentIp + { + _mintAndRegisterIpAndMakeDerivativeBaseTest_DEPR(); + } + + function test_DerivativeWorkflows_registerIpAndMakeDerivative_withNonCommercialLicense_DEPR() + public + withCollection + whenCallerHasMinterRole + withEnoughTokens(address(derivativeWorkflows)) + withNonCommercialParentIp + { + _registerIpAndMakeDerivativeBaseTest_DEPR(); + } + + function test_DerivativeWorkflows_mintAndRegisterIpAndMakeDerivative_withCommercialLicense_DEPR() + public + withCollection + whenCallerHasMinterRole + withEnoughTokens(address(derivativeWorkflows)) + withCommercialParentIp + { + _mintAndRegisterIpAndMakeDerivativeBaseTest_DEPR(); + } + + function test_DerivativeWorkflows_registerIpAndMakeDerivative_withCommercialLicense_DEPR() + public + withCollection + whenCallerHasMinterRole + withEnoughTokens(address(derivativeWorkflows)) + withCommercialParentIp + { + _registerIpAndMakeDerivativeBaseTest_DEPR(); + } + + function test_DerivativeWorkflows_mintAndRegisterIpAndMakeDerivativeWithLicenseTokens_DEPR() + public + withCollection + whenCallerHasMinterRole + withEnoughTokens(address(derivativeWorkflows)) + withNonCommercialParentIp + { + (address licenseTemplateParent, uint256 licenseTermsIdParent) = licenseRegistry.getAttachedLicenseTerms( + ipIdParent, + 0 + ); + + uint256 startLicenseTokenId = licensingModule.mintLicenseTokens({ + licensorIpId: ipIdParent, + licenseTemplate: address(pilTemplate), + licenseTermsId: licenseTermsIdParent, + amount: 1, + receiver: caller, + royaltyContext: "", + maxMintingFee: 0, + maxRevenueShare: 0 + }); + + // Need so that derivative workflows can transfer the license tokens + licenseToken.setApprovalForAll(address(derivativeWorkflows), true); + + uint256[] memory licenseTokenIds = new uint256[](1); + licenseTokenIds[0] = startLicenseTokenId; + + (address ipIdChild, uint256 tokenIdChild) = derivativeWorkflows + .mintAndRegisterIpAndMakeDerivativeWithLicenseTokens_deprecated({ + spgNftContract: address(nftContract), + licenseTokenIds: licenseTokenIds, + royaltyContext: "", + ipMetadata: ipMetadataDefault, + recipient: caller + }); + assertTrue(ipAssetRegistry.isRegistered(ipIdChild)); + assertEq(tokenIdChild, 2); + assertEq(nftContract.tokenURI(tokenIdChild), string.concat(testBaseURI, ipMetadataDefault.nftMetadataURI)); + assertMetadata(ipIdChild, ipMetadataDefault); + (address licenseTemplateChild, uint256 licenseTermsIdChild) = licenseRegistry.getAttachedLicenseTerms( + ipIdChild, + 0 + ); + assertEq(licenseTemplateChild, licenseTemplateParent); + assertEq(licenseTermsIdChild, licenseTermsIdParent); + assertEq(IIPAccount(payable(ipIdChild)).owner(), caller); + + assertParentChild({ + parentIpId: ipIdParent, + childIpId: ipIdChild, + expectedParentCount: 1, + expectedParentIndex: 0 + }); + } + + function test_DerivativeWorkflows_registerIpAndMakeDerivativeWithLicenseTokens_DEPR() + public + withCollection + whenCallerHasMinterRole + withEnoughTokens(address(derivativeWorkflows)) + withNonCommercialParentIp + { + (address licenseTemplateParent, uint256 licenseTermsIdParent) = licenseRegistry.getAttachedLicenseTerms( + ipIdParent, + 0 + ); + + uint256 tokenIdChild = nftContract.mint( + caller, + ipMetadataDefault.nftMetadataURI, + ipMetadataDefault.nftMetadataHash, + true + ); + address ipIdChild = ipAssetRegistry.ipId(block.chainid, address(nftContract), tokenIdChild); + + uint256 deadline = block.timestamp + 1000; + + uint256 startLicenseTokenId = licensingModule.mintLicenseTokens({ + licensorIpId: ipIdParent, + licenseTemplate: address(pilTemplate), + licenseTermsId: licenseTermsIdParent, + amount: 1, + receiver: caller, + royaltyContext: "", + maxMintingFee: 0, + maxRevenueShare: 0 + }); + + uint256[] memory licenseTokenIds = new uint256[](1); + licenseTokenIds[0] = startLicenseTokenId; + licenseToken.approve(address(derivativeWorkflows), startLicenseTokenId); + + (bytes memory sigMetadata, bytes32 expectedState, ) = _getSetPermissionSigForPeriphery({ + ipId: ipIdChild, + to: address(derivativeWorkflows), + module: address(coreMetadataModule), + selector: ICoreMetadataModule.setAll.selector, + deadline: deadline, + state: bytes32(0), + signerSk: sk.alice + }); + (bytes memory sigRegister, , ) = _getSetPermissionSigForPeriphery({ + ipId: ipIdChild, + to: address(derivativeWorkflows), + module: address(licensingModule), + selector: ILicensingModule.registerDerivativeWithLicenseTokens.selector, + deadline: deadline, + state: expectedState, + signerSk: sk.alice + }); + + address ipIdChildActual = derivativeWorkflows.registerIpAndMakeDerivativeWithLicenseTokens_deprecated({ + nftContract: address(nftContract), + tokenId: tokenIdChild, + licenseTokenIds: licenseTokenIds, + royaltyContext: "", + ipMetadata: ipMetadataDefault, + sigMetadata: WorkflowStructs.SignatureData({ signer: u.alice, deadline: deadline, signature: sigMetadata }), + sigRegister: WorkflowStructs.SignatureData({ signer: u.alice, deadline: deadline, signature: sigRegister }) + }); + assertEq(ipIdChildActual, ipIdChild); + assertTrue(ipAssetRegistry.isRegistered(ipIdChild)); + assertMetadata(ipIdChild, ipMetadataDefault); + (address licenseTemplateChild, uint256 licenseTermsIdChild) = licenseRegistry.getAttachedLicenseTerms( + ipIdChild, + 0 + ); + assertEq(licenseTemplateChild, licenseTemplateParent); + assertEq(licenseTermsIdChild, licenseTermsIdParent); + + assertParentChild({ + parentIpId: ipIdParent, + childIpId: ipIdChild, + expectedParentCount: 1, + expectedParentIndex: 0 + }); + } + + function test_SPG_multicall_mintAndRegisterIpAndMakeDerivative_DEPR() + public + withCollection + whenCallerHasMinterRole + withEnoughTokens(address(derivativeWorkflows)) + withNonCommercialParentIp + { + (address licenseTemplateParent, uint256 licenseTermsIdParent) = licenseRegistry.getAttachedLicenseTerms( + ipIdParent, + 0 + ); + address[] memory parentIpIds = new address[](1); + parentIpIds[0] = ipIdParent; + + uint256[] memory licenseTermsIds = new uint256[](1); + licenseTermsIds[0] = licenseTermsIdParent; + + bytes[] memory data = new bytes[](10); + for (uint256 i = 0; i < 10; i++) { + data[i] = abi.encodeWithSelector( + bytes4( + keccak256( + "mintAndRegisterIpAndMakeDerivative_deprecated(address,(address[],address,uint256[],bytes),(string,bytes32,string,bytes32),address)" + ) + ), + address(nftContract), + WorkflowStructs.MakeDerivativeDEPR({ + parentIpIds: parentIpIds, + licenseTemplate: address(pilTemplate), + licenseTermsIds: licenseTermsIds, + royaltyContext: "" + }), + ipMetadataDefault, + caller + ); + } + + bytes[] memory results = derivativeWorkflows.multicall(data); + + for (uint256 i = 0; i < 10; i++) { + (address ipIdChild, uint256 tokenIdChild) = abi.decode(results[i], (address, uint256)); + assertTrue(ipAssetRegistry.isRegistered(ipIdChild)); + assertEq(tokenIdChild, i + 2); + assertEq(nftContract.tokenURI(tokenIdChild), string.concat(testBaseURI, ipMetadataDefault.nftMetadataURI)); + assertMetadata(ipIdChild, ipMetadataDefault); + (address licenseTemplateChild, uint256 licenseTermsIdChild) = licenseRegistry.getAttachedLicenseTerms( + ipIdChild, + 0 + ); + assertEq(licenseTemplateChild, licenseTemplateParent); + assertEq(licenseTermsIdChild, licenseTermsIdParent); + assertEq(IIPAccount(payable(ipIdChild)).owner(), caller); + assertParentChild({ + parentIpId: ipIdParent, + childIpId: ipIdChild, + expectedParentCount: 1, + expectedParentIndex: 0 + }); + } + } + + function _mintAndRegisterIpAndMakeDerivativeBaseTest_DEPR() internal { + (address licenseTemplateParent, uint256 licenseTermsIdParent) = licenseRegistry.getAttachedLicenseTerms( + ipIdParent, + 0 + ); + + address[] memory parentIpIds = new address[](1); + parentIpIds[0] = ipIdParent; + + uint256[] memory licenseTermsIds = new uint256[](1); + licenseTermsIds[0] = licenseTermsIdParent; + + (address ipIdChild, uint256 tokenIdChild) = derivativeWorkflows.mintAndRegisterIpAndMakeDerivative_deprecated({ + spgNftContract: address(nftContract), + derivData: WorkflowStructs.MakeDerivativeDEPR({ + parentIpIds: parentIpIds, + licenseTemplate: address(pilTemplate), + licenseTermsIds: licenseTermsIds, + royaltyContext: "" + }), + ipMetadata: ipMetadataDefault, + recipient: caller + }); + assertTrue(ipAssetRegistry.isRegistered(ipIdChild)); + assertEq(tokenIdChild, 2); + assertEq(nftContract.tokenURI(tokenIdChild), string.concat(testBaseURI, ipMetadataDefault.nftMetadataURI)); + assertMetadata(ipIdChild, ipMetadataDefault); + (address licenseTemplateChild, uint256 licenseTermsIdChild) = licenseRegistry.getAttachedLicenseTerms( + ipIdChild, + 0 + ); + assertEq(licenseTemplateChild, licenseTemplateParent); + assertEq(licenseTermsIdChild, licenseTermsIdParent); + assertEq(IIPAccount(payable(ipIdChild)).owner(), caller); + + assertParentChild({ + parentIpId: ipIdParent, + childIpId: ipIdChild, + expectedParentCount: 1, + expectedParentIndex: 0 + }); + } + + function _registerIpAndMakeDerivativeBaseTest_DEPR() internal { + (address licenseTemplateParent, uint256 licenseTermsIdParent) = licenseRegistry.getAttachedLicenseTerms( + ipIdParent, + 0 + ); + + uint256 tokenIdChild = nftContract.mint( + address(caller), + ipMetadataDefault.nftMetadataURI, + ipMetadataDefault.nftMetadataHash, + true + ); + address ipIdChild = ipAssetRegistry.ipId(block.chainid, address(nftContract), tokenIdChild); + + uint256 deadline = block.timestamp + 1000; + + (bytes memory sigMetadata, bytes32 expectedState, ) = _getSetPermissionSigForPeriphery({ + ipId: ipIdChild, + to: address(derivativeWorkflows), + module: address(coreMetadataModule), + selector: ICoreMetadataModule.setAll.selector, + deadline: deadline, + state: bytes32(0), + signerSk: sk.alice + }); + (bytes memory sigRegister, , ) = _getSetPermissionSigForPeriphery({ + ipId: ipIdChild, + to: address(derivativeWorkflows), + module: address(licensingModule), + selector: ILicensingModule.registerDerivative.selector, + deadline: deadline, + state: expectedState, + signerSk: sk.alice + }); + + address[] memory parentIpIds = new address[](1); + parentIpIds[0] = ipIdParent; + + uint256[] memory licenseTermsIds = new uint256[](1); + licenseTermsIds[0] = licenseTermsIdParent; + + address ipIdChildActual = derivativeWorkflows.registerIpAndMakeDerivative_deprecated({ + nftContract: address(nftContract), + tokenId: tokenIdChild, + derivData: WorkflowStructs.MakeDerivativeDEPR({ + parentIpIds: parentIpIds, + licenseTemplate: address(pilTemplate), + licenseTermsIds: licenseTermsIds, + royaltyContext: "" + }), + ipMetadata: ipMetadataDefault, + sigMetadata: WorkflowStructs.SignatureData({ signer: u.alice, deadline: deadline, signature: sigMetadata }), + sigRegister: WorkflowStructs.SignatureData({ signer: u.alice, deadline: deadline, signature: sigRegister }) + }); + assertEq(ipIdChildActual, ipIdChild); + assertTrue(ipAssetRegistry.isRegistered(ipIdChild)); + assertEq(nftContract.tokenURI(tokenIdChild), string.concat(testBaseURI, ipMetadataDefault.nftMetadataURI)); + assertMetadata(ipIdChild, ipMetadataDefault); + (address licenseTemplateChild, uint256 licenseTermsIdChild) = licenseRegistry.getAttachedLicenseTerms( + ipIdChild, + 0 + ); + assertEq(licenseTemplateChild, licenseTemplateParent); + assertEq(licenseTermsIdChild, licenseTermsIdParent); + assertEq(IIPAccount(payable(ipIdChild)).owner(), caller); + + assertParentChild({ + parentIpId: ipIdParent, + childIpId: ipIdChild, + expectedParentCount: 1, + expectedParentIndex: 0 + }); + } } diff --git a/test/workflows/GroupingWorkflows.t.sol b/test/workflows/GroupingWorkflows.t.sol index 9a387a2..bfa6f6d 100644 --- a/test/workflows/GroupingWorkflows.t.sol +++ b/test/workflows/GroupingWorkflows.t.sol @@ -53,7 +53,7 @@ contract GroupingWorkflowsTest is BaseTest, ERC721Holder { mintingFee: 0, commercialRevShare: revShare, currencyToken: address(mockToken), - royaltyPolicy: address(royaltyPolicyLAP) + royaltyPolicy: address(royaltyPolicyLRP) }) ), licensingConfig: Licensing.LicensingConfig({ @@ -106,7 +106,7 @@ contract GroupingWorkflowsTest is BaseTest, ERC721Holder { signerSk: groupOwnerSk }); - vm.startPrank(minter); + vm.startPrank(groupOwner); vm.expectRevert( abi.encodeWithSelector( Errors.SPGNFT__DuplicatedNFTMetadataHash.selector, @@ -118,7 +118,8 @@ contract GroupingWorkflowsTest is BaseTest, ERC721Holder { groupingWorkflows.mintAndRegisterIpAndAttachLicenseAndAddToGroup({ spgNftContract: address(spgNftPublic), groupId: groupId, - recipient: minter, + recipient: groupOwner, + maxAllowedRewardShare: 100e6, // 100% ipMetadata: ipMetadataDefault, licensesData: testLicensesData, sigAddToGroup: WorkflowStructs.SignatureData({ @@ -145,11 +146,12 @@ contract GroupingWorkflowsTest is BaseTest, ERC721Holder { state: IIPAccount(payable(groupId)).state(), signerSk: groupOwnerSk }); - vm.startPrank(minter); + vm.startPrank(groupOwner); (address ipId, uint256 tokenId) = groupingWorkflows.mintAndRegisterIpAndAttachLicenseAndAddToGroup({ spgNftContract: address(spgNftPublic), groupId: groupId, - recipient: minter, + recipient: groupOwner, + maxAllowedRewardShare: 100e6, // 100% ipMetadata: ipMetadataDefault, licensesData: testLicensesData, sigAddToGroup: WorkflowStructs.SignatureData({ @@ -180,8 +182,8 @@ contract GroupingWorkflowsTest is BaseTest, ERC721Holder { // Register IP → Attach license terms → Add new IP to group IPA function test_GroupingWorkflows_registerIpAndAttachLicenseAndAddToGroup() public { // mint a NFT from the mock ERC721 contract - vm.startPrank(minter); - uint256 tokenId = MockERC721(mockNft).mint(minter); + vm.startPrank(groupOwner); + uint256 tokenId = MockERC721(mockNft).mint(groupOwner); vm.stopPrank(); // get the expected IP ID address expectedIpId = IIPAssetRegistry(ipAssetRegistry).ipId(block.chainid, address(mockNft), tokenId); @@ -193,7 +195,7 @@ contract GroupingWorkflowsTest is BaseTest, ERC721Holder { permissionList: _getMetadataAndAttachTermsAndConfigPermissionList(expectedIpId, address(groupingWorkflows)), deadline: deadline, state: bytes32(0), - signerSk: minterSk + signerSk: groupOwnerSk }); // Get the signature for setting the permission for calling `addIp` function in `GroupingModule` // from the Group IP owner @@ -206,14 +208,16 @@ contract GroupingWorkflowsTest is BaseTest, ERC721Holder { state: IIPAccount(payable(groupId)).state(), signerSk: groupOwnerSk }); + vm.startPrank(groupOwner); address ipId = groupingWorkflows.registerIpAndAttachLicenseAndAddToGroup({ nftContract: address(mockNft), tokenId: tokenId, groupId: groupId, + maxAllowedRewardShare: 100e6, // 100% licensesData: testLicensesData, ipMetadata: ipMetadataDefault, sigMetadataAndAttachAndConfig: WorkflowStructs.SignatureData({ - signer: minter, + signer: groupOwner, deadline: deadline, signature: sigMetadataAndAttach }), @@ -223,6 +227,8 @@ contract GroupingWorkflowsTest is BaseTest, ERC721Holder { signature: sigAddToGroup }) }); + vm.stopPrank(); + // check the IP id matches the expected IP id assertEq(ipId, expectedIpId); // check the IP is registered @@ -267,6 +273,7 @@ contract GroupingWorkflowsTest is BaseTest, ERC721Holder { address newGroupId = groupingWorkflows.registerGroupAndAttachLicenseAndAddIps({ groupPool: address(evenSplitGroupPool), ipIds: ipIds, + maxAllowedRewardShare: 100e6, // 100% licenseData: testGroupLicenseData[0] }); vm.stopPrank(); @@ -298,6 +305,7 @@ contract GroupingWorkflowsTest is BaseTest, ERC721Holder { address newGroupId = groupingWorkflows.registerGroupAndAttachLicenseAndAddIps({ groupPool: address(evenSplitGroupPool), ipIds: ipIds, + maxAllowedRewardShare: 100e6, // 100% licenseData: testGroupLicenseData[0] }); vm.stopPrank(); @@ -359,7 +367,7 @@ contract GroupingWorkflowsTest is BaseTest, ERC721Holder { vm.startPrank(ipOwner1); mockToken.approve(address(royaltyModule), amount1); royaltyModule.payRoyaltyOnBehalf(ipId1, ipOwner1, address(mockToken), amount1); - royaltyPolicyLAP.transferToVault(ipId1, newGroupId, address(mockToken)); + royaltyPolicyLRP.transferToVault(ipId1, newGroupId, address(mockToken)); vm.stopPrank(); uint256 amount2 = 10_000 * 10 ** mockToken.decimals(); // 10,000 tokens @@ -367,7 +375,7 @@ contract GroupingWorkflowsTest is BaseTest, ERC721Holder { vm.startPrank(ipOwner2); mockToken.approve(address(royaltyModule), amount2); royaltyModule.payRoyaltyOnBehalf(ipId2, ipOwner2, address(mockToken), amount2); - royaltyPolicyLAP.transferToVault(ipId2, newGroupId, address(mockToken)); + royaltyPolicyLRP.transferToVault(ipId2, newGroupId, address(mockToken)); vm.stopPrank(); address[] memory royaltyTokens = new address[](1); @@ -430,12 +438,13 @@ contract GroupingWorkflowsTest is BaseTest, ERC721Holder { data[i] = abi.encodeWithSelector( bytes4( keccak256( - "mintAndRegisterIpAndAttachLicenseAndAddToGroup(address,address,address,(address,uint256,(bool,uint256,address,bytes,uint32,bool,uint32,address))[],(string,bytes32,string,bytes32),(address,uint256,bytes),bool)" + "mintAndRegisterIpAndAttachLicenseAndAddToGroup(address,address,address,uint256,(address,uint256,(bool,uint256,address,bytes,uint32,bool,uint32,address))[],(string,bytes32,string,bytes32),(address,uint256,bytes),bool)" ) ), address(spgNftPublic), groupId, - minter, + groupOwner, + 100e6, // 100% testLicensesData, ipMetadataDefault, WorkflowStructs.SignatureData({ signer: groupOwner, deadline: deadline, signature: sigsAddToGroup[i] }), @@ -443,7 +452,7 @@ contract GroupingWorkflowsTest is BaseTest, ERC721Holder { ); } // batch call `mintAndRegisterIpAndAttachLicenseAndAddToGroup` - vm.startPrank(minter); + vm.startPrank(groupOwner); 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 @@ -466,9 +475,9 @@ contract GroupingWorkflowsTest is BaseTest, ERC721Holder { function test_GroupingWorkflows_multicall_registerIpAndAttachLicenseAndAddToGroup() public { // mint a NFT from the mock ERC721 contract uint256[] memory tokenIds = new uint256[](10); - vm.startPrank(minter); + vm.startPrank(groupOwner); for (uint256 i = 0; i < 10; i++) { - tokenIds[i] = MockERC721(mockNft).mint(minter); + tokenIds[i] = MockERC721(mockNft).mint(groupOwner); } vm.stopPrank(); // get the expected IP ID @@ -489,7 +498,7 @@ contract GroupingWorkflowsTest is BaseTest, ERC721Holder { ), deadline: deadline, state: bytes32(0), - signerSk: minterSk + signerSk: groupOwnerSk }); } // Get the signatures for setting the permission for calling `addIp` function in `GroupingModule` @@ -513,16 +522,17 @@ contract GroupingWorkflowsTest is BaseTest, ERC721Holder { data[i] = abi.encodeWithSelector( 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))" + "registerIpAndAttachLicenseAndAddToGroup(address,uint256,address,uint256,(address,uint256,(bool,uint256,address,bytes,uint32,bool,uint32,address))[],(string,bytes32,string,bytes32),(address,uint256,bytes),(address,uint256,bytes))" ) ), mockNft, tokenIds[i], groupId, + 100e6, // 100% testLicensesData, ipMetadataDefault, WorkflowStructs.SignatureData({ - signer: minter, + signer: groupOwner, deadline: deadline, signature: sigsMetadataAndAttach[i] }), @@ -530,7 +540,7 @@ contract GroupingWorkflowsTest is BaseTest, ERC721Holder { ); } // batch call `registerIpAndAttachLicenseAndAddToGroup` - vm.startPrank(minter); + vm.startPrank(groupOwner); 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 @@ -553,7 +563,8 @@ contract GroupingWorkflowsTest is BaseTest, ERC721Holder { groupingWorkflows.mintAndRegisterIpAndAttachLicenseAndAddToGroup({ spgNftContract: address(spgNftPublic), groupId: groupId, - recipient: minter, + recipient: groupOwner, + maxAllowedRewardShare: 100e6, // 100% ipMetadata: ipMetadataDefault, licensesData: new WorkflowStructs.LicenseData[](0), sigAddToGroup: WorkflowStructs.SignatureData({ @@ -569,10 +580,11 @@ contract GroupingWorkflowsTest is BaseTest, ERC721Holder { nftContract: address(mockNft), tokenId: 0, groupId: groupId, + maxAllowedRewardShare: 100e6, // 100% licensesData: new WorkflowStructs.LicenseData[](0), ipMetadata: ipMetadataDefault, sigMetadataAndAttachAndConfig: WorkflowStructs.SignatureData({ - signer: minter, + signer: groupOwner, deadline: block.timestamp + 1000, signature: new bytes(0) }), @@ -603,8 +615,8 @@ contract GroupingWorkflowsTest is BaseTest, ERC721Holder { // setup individual IPs for testing function _setupIPs() internal { // mint and approve tokens for minting - vm.startPrank(minter); - MockERC20(mockToken).mint(minter, 1000 * 10 * 10 ** MockERC20(mockToken).decimals()); + vm.startPrank(groupOwner); + MockERC20(mockToken).mint(groupOwner, 1000 * 10 * 10 ** MockERC20(mockToken).decimals()); MockERC20(mockToken).approve(address(spgNftPublic), 1000 * 10 * 10 ** MockERC20(mockToken).decimals()); vm.stopPrank(); @@ -614,14 +626,14 @@ contract GroupingWorkflowsTest is BaseTest, ERC721Holder { data[i] = abi.encodeWithSelector( bytes4(keccak256("mintAndRegisterIp(address,address,(string,bytes32,string,bytes32),bool)")), address(spgNftPublic), - minter, + groupOwner, ipMetadataDefault, true ); } // batch call `mintAndRegisterIp` - vm.startPrank(minter); + vm.startPrank(groupOwner); bytes[] memory results = registrationWorkflows.multicall(data); vm.stopPrank(); @@ -632,7 +644,7 @@ contract GroupingWorkflowsTest is BaseTest, ERC721Holder { } // attach license terms to the IPs - vm.startPrank(minter); + vm.startPrank(groupOwner); for (uint256 i = 0; i < 10; i++) { LicensingHelper.attachLicenseTermsAndSetConfigs( ipIds[i], @@ -644,4 +656,453 @@ 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(groupOwner); + (address ipId, uint256 tokenId) = groupingWorkflows.mintAndRegisterIpAndAttachLicenseAndAddToGroup_deprecated({ + spgNftContract: address(spgNftPublic), + groupId: groupId, + recipient: groupOwner, + 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(groupOwner); + uint256 tokenId = MockERC721(mockNft).mint(groupOwner); + 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: groupOwnerSk + }); + + // 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 + }); + + vm.startPrank(groupOwner); + address ipId = groupingWorkflows.registerIpAndAttachLicenseAndAddToGroup_deprecated({ + nftContract: address(mockNft), + tokenId: tokenId, + groupId: groupId, + licenseTemplate: address(pilTemplate), + licenseTermsId: testLicensesData[0].licenseTermsId, + ipMetadata: ipMetadataDefault, + sigMetadataAndAttachAndConfig: WorkflowStructs.SignatureData({ + signer: groupOwner, + deadline: deadline, + signature: sigMetadataAndAttach + }), + sigAddToGroup: WorkflowStructs.SignatureData({ + signer: groupOwner, + deadline: deadline, + signature: sigAddToGroup + }) + }); + vm.stopPrank(); + + // 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_deprecated({ + 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_deprecated({ + 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_deprecated({ + 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_deprecated({ + 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_deprecated({ + 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); + royaltyPolicyLRP.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); + royaltyPolicyLRP.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_deprecated( + 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_deprecated(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_deprecated(address,address,address,address,uint256,(string,bytes32,string,bytes32),(address,uint256,bytes))" + ) + ), + address(spgNftPublic), + groupId, + groupOwner, + pilTemplate, + testLicensesData[0].licenseTermsId, + ipMetadataDefault, + WorkflowStructs.SignatureData({ signer: groupOwner, deadline: deadline, signature: sigsAddToGroup[i] }) + ); + } + + // batch call `mintAndRegisterIpAndAttachLicenseAndAddToGroup` + vm.startPrank(groupOwner); + 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(groupOwner); + for (uint256 i = 0; i < 10; i++) { + tokenIds[i] = MockERC721(mockNft).mint(groupOwner); + } + 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: groupOwnerSk + }); + } + + // 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_deprecated(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: groupOwner, + deadline: deadline, + signature: sigsMetadataAndAttach[i] + }), + WorkflowStructs.SignatureData({ signer: groupOwner, deadline: deadline, signature: sigsAddToGroup[i] }) + ); + } + + // batch call `registerIpAndAttachLicenseAndAddToGroup` + vm.startPrank(groupOwner); + 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); + } + } } diff --git a/test/workflows/LicenseAttachmentWorkflows.t.sol b/test/workflows/LicenseAttachmentWorkflows.t.sol index 3cb434b..66873b1 100644 --- a/test/workflows/LicenseAttachmentWorkflows.t.sol +++ b/test/workflows/LicenseAttachmentWorkflows.t.sol @@ -35,6 +35,7 @@ contract LicenseAttachmentWorkflowsTest is BaseTest { function setUp() public override { super.setUp(); _setUpLicenseTermsData(); + _setUpTerms(); } modifier withIp(address owner) { @@ -97,6 +98,7 @@ contract LicenseAttachmentWorkflowsTest is BaseTest { uint256 ltAmt = pilTemplate.totalRegisteredLicenseTerms(); + vm.startPrank(u.alice); uint256[] memory licenseTermsIds = licenseAttachmentWorkflows.registerPILTermsAndAttach({ ipId: ipId, licenseTermsData: commTermsData, @@ -106,6 +108,7 @@ contract LicenseAttachmentWorkflowsTest is BaseTest { signature: signature }) }); + vm.stopPrank(); _assertAttachedLicenseTerms(ipId, licenseTermsIds); } @@ -204,6 +207,7 @@ contract LicenseAttachmentWorkflowsTest is BaseTest { signerSk: sk.alice }); + vm.startPrank(u.alice); uint256[] memory licenseTermsIds1 = licenseAttachmentWorkflows.registerPILTermsAndAttach({ ipId: ipId, licenseTermsData: commTermsData, @@ -213,6 +217,7 @@ contract LicenseAttachmentWorkflowsTest is BaseTest { signature: signature1 }) }); + vm.stopPrank(); (bytes memory signature2, , ) = _getSetBatchPermissionSigForPeriphery({ ipId: ipId, @@ -223,6 +228,7 @@ contract LicenseAttachmentWorkflowsTest is BaseTest { }); // attach the same license terms to the IP again, but it shouldn't revert + vm.startPrank(u.alice); uint256[] memory licenseTermsIds2 = licenseAttachmentWorkflows.registerPILTermsAndAttach({ ipId: ipId, licenseTermsData: commTermsData, @@ -232,6 +238,7 @@ contract LicenseAttachmentWorkflowsTest is BaseTest { signature: signature2 }) }); + vm.stopPrank(); for (uint256 i = 0; i < licenseTermsIds1.length; i++) { assertEq(licenseTermsIds1[i], licenseTermsIds2[i]); @@ -335,6 +342,71 @@ contract LicenseAttachmentWorkflowsTest is BaseTest { }); } + function test_LicenseAttachmentWorkflows_mintAndRegisterIpAndAttachDefaultTerms() + public + withCollection + whenCallerHasMinterRole + withEnoughTokens(address(licenseAttachmentWorkflows)) + { + (address ipId1, uint256 tokenId1) = licenseAttachmentWorkflows.mintAndRegisterIpAndAttachDefaultTerms({ + spgNftContract: address(nftContract), + recipient: caller, + ipMetadata: ipMetadataEmpty, + allowDuplicates: true + }); + assertTrue(ipAssetRegistry.isRegistered(ipId1)); + assertEq(tokenId1, 1); + assertEq(nftContract.tokenURI(tokenId1), string.concat(testBaseURI, tokenId1.toString())); + assertMetadata(ipId1, ipMetadataEmpty); + uint256[] memory licenseTemplates = new uint256[](1); + (, licenseTemplates[0]) = licenseRegistry.getDefaultLicenseTerms(); + (, uint256 licenseTermsId) = licenseRegistry.getAttachedLicenseTerms(ipId1, 0); + assertEq(licenseTermsId, licenseTemplates[0]); + } + + function test_LicenseAttachmentWorkflows_registerIpAndAttachDefaultTerms() + public + withCollection + whenCallerHasMinterRole + withEnoughTokens(address(licenseAttachmentWorkflows)) + { + uint256 tokenId = nftContract.mint({ + to: caller, + nftMetadataURI: ipMetadataEmpty.nftMetadataURI, + nftMetadataHash: ipMetadataEmpty.nftMetadataHash, + allowDuplicates: true + }); + address payable ipId = payable(ipAssetRegistry.ipId(block.chainid, address(nftContract), tokenId)); + + uint256 deadline = block.timestamp + 1000; + + (bytes memory sigMetadataAndDefaultTerms, , ) = _getSetBatchPermissionSigForPeriphery({ + ipId: ipId, + permissionList: _getMetadataAndDefaultTermsPermissionList(ipId, address(licenseAttachmentWorkflows)), + deadline: deadline, + state: bytes32(0), + signerSk: sk.alice + }); + + licenseAttachmentWorkflows.registerIpAndAttachDefaultTerms({ + nftContract: address(nftContract), + tokenId: tokenId, + ipMetadata: ipMetadataDefault, + sigMetadataAndDefaultTerms: WorkflowStructs.SignatureData({ + signer: u.alice, + deadline: deadline, + signature: sigMetadataAndDefaultTerms + }) + }); + + assertTrue(ipAssetRegistry.isRegistered(ipId)); + assertMetadata(ipId, ipMetadataDefault); + uint256[] memory licenseTemplates = new uint256[](1); + (, licenseTemplates[0]) = licenseRegistry.getDefaultLicenseTerms(); + (, uint256 licenseTermsId) = licenseRegistry.getAttachedLicenseTerms(ipId, 0); + assertEq(licenseTermsId, licenseTemplates[0]); + } + function _assertAttachedLicenseTerms(address ipId, uint256[] memory licenseTermsIds) internal { for (uint256 i = 0; i < commTermsData.length; i++) { (address licenseTemplate, uint256 licenseTermsId) = licenseRegistry.getAttachedLicenseTerms(ipId, i); @@ -445,4 +517,525 @@ contract LicenseAttachmentWorkflowsTest is BaseTest { }) ); } + + //////////////////////////////////////////////////////////////////////////// + // DEPRECATED // + //////////////////////////////////////////////////////////////////////////// + + function test_LicenseAttachmentWorkflows_registerPILTermsAndAttach_DEPR() public withCollection withIp(u.alice) { + address payable ipId = ipAsset[1].ipId; + uint256 deadline = block.timestamp + 1000; + + (bytes memory signature, , ) = _getSetPermissionSigForPeriphery({ + ipId: ipId, + to: address(licenseAttachmentWorkflows), + module: address(licensingModule), + selector: ILicensingModule.attachLicenseTerms.selector, + deadline: deadline, + state: IIPAccount(ipId).state(), + signerSk: sk.alice + }); + + uint256 ltAmt = pilTemplate.totalRegisteredLicenseTerms(); + + vm.startPrank(u.alice); + uint256[] memory licenseTermsIds = licenseAttachmentWorkflows.registerPILTermsAndAttach_deprecated({ + ipId: ipId, + terms: terms, + sigAttach: WorkflowStructs.SignatureData({ signer: u.alice, deadline: deadline, signature: signature }) + }); + vm.stopPrank(); + + assertEq(licenseTermsIds[0], ltAmt + 1); + assertEq(licenseTermsIds[1], ltAmt + 2); + assertEq(licenseTermsIds[2], ltAmt + 3); + assertEq(licenseTermsIds[3], ltAmt + 4); + assertEq(licenseTermsIds[4], ltAmt + 5); + } + + function test_LicenseAttachmentWorkflows_mintAndRegisterIpAndAttachPILTerms_DEPR() + public + withCollection + whenCallerHasMinterRole + withEnoughTokens(address(licenseAttachmentWorkflows)) + { + (address ipId1, uint256 tokenId1, uint256[] memory licenseTermsIds1) = licenseAttachmentWorkflows + .mintAndRegisterIpAndAttachPILTerms_deprecated({ + spgNftContract: address(nftContract), + recipient: caller, + ipMetadata: ipMetadataEmpty, + terms: terms + }); + assertTrue(ipAssetRegistry.isRegistered(ipId1)); + assertEq(tokenId1, 1); + assertEq(licenseTermsIds1[0], 2); + assertEq(licenseTermsIds1[1], 3); + assertEq(licenseTermsIds1[2], 4); + assertEq(licenseTermsIds1[3], 5); + assertEq(licenseTermsIds1[4], 6); + assertEq(nftContract.tokenURI(tokenId1), string.concat(testBaseURI, tokenId1.toString())); + assertMetadata(ipId1, ipMetadataEmpty); + (address licenseTemplate, uint256 licenseTermsId) = licenseRegistry.getAttachedLicenseTerms(ipId1, 0); + assertEq(licenseTemplate, address(pilTemplate)); + assertEq(licenseTermsId, licenseTermsIds1[0]); + (licenseTemplate, licenseTermsId) = licenseRegistry.getAttachedLicenseTerms(ipId1, 1); + assertEq(licenseTemplate, address(pilTemplate)); + assertEq(licenseTermsId, licenseTermsIds1[1]); + (licenseTemplate, licenseTermsId) = licenseRegistry.getAttachedLicenseTerms(ipId1, 2); + assertEq(licenseTemplate, address(pilTemplate)); + assertEq(licenseTermsId, licenseTermsIds1[2]); + (licenseTemplate, licenseTermsId) = licenseRegistry.getAttachedLicenseTerms(ipId1, 3); + assertEq(licenseTemplate, address(pilTemplate)); + assertEq(licenseTermsId, licenseTermsIds1[3]); + (licenseTemplate, licenseTermsId) = licenseRegistry.getAttachedLicenseTerms(ipId1, 4); + assertEq(licenseTemplate, address(pilTemplate)); + assertEq(licenseTermsId, licenseTermsIds1[4]); + + (address ipId2, uint256 tokenId2, uint256[] memory licenseTermsIds2) = licenseAttachmentWorkflows + .mintAndRegisterIpAndAttachPILTerms_deprecated({ + spgNftContract: address(nftContract), + recipient: caller, + ipMetadata: ipMetadataDefault, + terms: terms + }); + assertTrue(ipAssetRegistry.isRegistered(ipId2)); + assertEq(tokenId2, 2); + assertEq(licenseTermsIds1[0], licenseTermsIds2[0]); + assertEq(licenseTermsIds1[1], licenseTermsIds2[1]); + assertEq(licenseTermsIds1[2], licenseTermsIds2[2]); + assertEq(licenseTermsIds1[3], licenseTermsIds2[3]); + assertEq(licenseTermsIds1[4], licenseTermsIds2[4]); + assertEq(nftContract.tokenURI(tokenId2), string.concat(testBaseURI, ipMetadataDefault.nftMetadataURI)); + assertMetadata(ipId2, ipMetadataDefault); + } + + function test_LicenseAttachmentWorkflows_registerIpAndAttachPILTerms_DEPR() + public + withCollection + whenCallerHasMinterRole + withEnoughTokens(address(licenseAttachmentWorkflows)) + { + uint256 tokenId = nftContract.mint(address(caller), ipMetadataEmpty.nftMetadataURI, "", true); + address payable ipId = payable(ipAssetRegistry.ipId(block.chainid, address(nftContract), tokenId)); + + uint256 deadline = block.timestamp + 1000; + + (bytes memory sigMetadata, bytes32 expectedState, ) = _getSetPermissionSigForPeriphery({ + ipId: ipId, + to: address(licenseAttachmentWorkflows), + module: address(coreMetadataModule), + selector: ICoreMetadataModule.setAll.selector, + deadline: deadline, + state: bytes32(0), + signerSk: sk.alice + }); + + (bytes memory sigAttach, , ) = _getSetPermissionSigForPeriphery({ + ipId: ipId, + to: address(licenseAttachmentWorkflows), + module: address(licensingModule), + selector: ILicensingModule.attachLicenseTerms.selector, + deadline: deadline, + state: expectedState, + signerSk: sk.alice + }); + + licenseAttachmentWorkflows.registerIpAndAttachPILTerms_deprecated({ + nftContract: address(nftContract), + tokenId: tokenId, + ipMetadata: ipMetadataDefault, + terms: terms, + sigMetadata: WorkflowStructs.SignatureData({ signer: u.alice, deadline: deadline, signature: sigMetadata }), + sigAttach: WorkflowStructs.SignatureData({ signer: u.alice, deadline: deadline, signature: sigAttach }) + }); + + assertTrue(ipAssetRegistry.isRegistered(ipId)); + assertMetadata(ipId, ipMetadataDefault); + (address licenseTemplate, uint256 licenseTermsId) = licenseRegistry.getAttachedLicenseTerms(ipId, 0); + assertEq(licenseTemplate, address(pilTemplate)); + assertEq(licenseTermsId, pilTemplate.getLicenseTermsId(terms[0])); + (licenseTemplate, licenseTermsId) = licenseRegistry.getAttachedLicenseTerms(ipId, 1); + assertEq(licenseTemplate, address(pilTemplate)); + assertEq(licenseTermsId, pilTemplate.getLicenseTermsId(terms[1])); + (licenseTemplate, licenseTermsId) = licenseRegistry.getAttachedLicenseTerms(ipId, 2); + assertEq(licenseTemplate, address(pilTemplate)); + assertEq(licenseTermsId, pilTemplate.getLicenseTermsId(terms[2])); + (licenseTemplate, licenseTermsId) = licenseRegistry.getAttachedLicenseTerms(ipId, 3); + assertEq(licenseTemplate, address(pilTemplate)); + assertEq(licenseTermsId, pilTemplate.getLicenseTermsId(terms[3])); + (licenseTemplate, licenseTermsId) = licenseRegistry.getAttachedLicenseTerms(ipId, 4); + assertEq(licenseTemplate, address(pilTemplate)); + assertEq(licenseTermsId, pilTemplate.getLicenseTermsId(terms[4])); + } + + function test_LicenseAttachmentWorkflows_registerPILTermsAndAttach_idempotency_DEPR() + public + withCollection + withIp(u.alice) + { + address payable ipId = ipAsset[1].ipId; + uint256 deadline = block.timestamp + 1000; + + (bytes memory signature1, , ) = _getSetPermissionSigForPeriphery({ + ipId: ipId, + to: address(licenseAttachmentWorkflows), + module: address(licensingModule), + selector: ILicensingModule.attachLicenseTerms.selector, + deadline: deadline, + state: IIPAccount(ipId).state(), + signerSk: sk.alice + }); + + vm.startPrank(u.alice); + uint256[] memory licenseTermsIds1 = licenseAttachmentWorkflows.registerPILTermsAndAttach_deprecated({ + ipId: ipId, + terms: terms, + sigAttach: WorkflowStructs.SignatureData({ signer: u.alice, deadline: deadline, signature: signature1 }) + }); + vm.stopPrank(); + + (bytes memory signature2, , ) = _getSetPermissionSigForPeriphery({ + ipId: ipId, + to: address(licenseAttachmentWorkflows), + module: address(licensingModule), + selector: ILicensingModule.attachLicenseTerms.selector, + deadline: deadline, + state: IIPAccount(ipId).state(), + signerSk: sk.alice + }); + + /// attach the same license terms to the IP again, but it shouldn't revert + vm.startPrank(u.alice); + uint256[] memory licenseTermsIds2 = licenseAttachmentWorkflows.registerPILTermsAndAttach_deprecated({ + ipId: ipId, + terms: terms, + sigAttach: WorkflowStructs.SignatureData({ signer: u.alice, deadline: deadline, signature: signature2 }) + }); + vm.stopPrank(); + + assertEq(licenseTermsIds1[0], licenseTermsIds2[0]); + assertEq(licenseTermsIds1[1], licenseTermsIds2[1]); + assertEq(licenseTermsIds1[2], licenseTermsIds2[2]); + assertEq(licenseTermsIds1[3], licenseTermsIds2[3]); + assertEq(licenseTermsIds1[4], licenseTermsIds2[4]); + } + + function test_revert_registerPILTermsAndAttach_DerivativesCannotAddLicenseTerms_DEPR() + public + withCollection + whenCallerHasMinterRole + withEnoughTokens(address(licenseAttachmentWorkflows)) + { + (address ipIdParent, , uint256[] memory licenseTermsIdsParent) = licenseAttachmentWorkflows + .mintAndRegisterIpAndAttachPILTerms_deprecated({ + spgNftContract: address(nftContract), + recipient: caller, + ipMetadata: ipMetadataDefault, + terms: terms + }); + + address[] memory parentIpIds = new address[](1); + parentIpIds[0] = ipIdParent; + + uint256[] memory licenseTermsIds = new uint256[](1); + licenseTermsIds[0] = licenseTermsIdsParent[0]; + + (address ipIdChild, ) = derivativeWorkflows.mintAndRegisterIpAndMakeDerivative_deprecated({ + spgNftContract: address(nftContract), + derivData: WorkflowStructs.MakeDerivativeDEPR({ + parentIpIds: parentIpIds, + licenseTemplate: address(pilTemplate), + licenseTermsIds: licenseTermsIds, + royaltyContext: "" + }), + ipMetadata: ipMetadataDefault, + recipient: caller + }); + + uint256 deadline = block.timestamp + 1000; + + (bytes memory signature, , ) = _getSetPermissionSigForPeriphery({ + ipId: ipIdChild, + to: address(licenseAttachmentWorkflows), + module: address(licensingModule), + selector: ILicensingModule.attachLicenseTerms.selector, + deadline: deadline, + state: IIPAccount(payable(ipIdChild)).state(), + signerSk: sk.alice + }); + + /// attach license terms to the child ip, should revert with the correct error + vm.expectRevert(CoreErrors.LicensingModule__DerivativesCannotAddLicenseTerms.selector); + licenseAttachmentWorkflows.registerPILTermsAndAttach_deprecated({ + ipId: ipIdChild, + terms: terms, + sigAttach: WorkflowStructs.SignatureData({ signer: u.alice, deadline: deadline, signature: signature }) + }); + } + + function test_LicenseAttachmentWorkflows_registerPILTermsAndAttach_SingleTerms_DEPR() + public + withCollection + withIp(u.alice) + { + address payable ipId = ipAsset[1].ipId; + uint256 deadline = block.timestamp + 1000; + + (bytes memory signature, , ) = _getSetPermissionSigForPeriphery({ + ipId: ipId, + to: address(licenseAttachmentWorkflows), + module: address(licensingModule), + selector: ILicensingModule.attachLicenseTerms.selector, + deadline: deadline, + state: IIPAccount(ipId).state(), + signerSk: sk.alice + }); + + uint256 ltAmt = pilTemplate.totalRegisteredLicenseTerms(); + + vm.startPrank(u.alice); + uint256 licenseTermsId = licenseAttachmentWorkflows.registerPILTermsAndAttach_deprecated({ + ipId: ipId, + terms: PILFlavors.commercialUse({ + mintingFee: 100, + currencyToken: address(mockToken), + royaltyPolicy: address(royaltyPolicyLAP) + }), + sigAttach: WorkflowStructs.SignatureData({ signer: u.alice, deadline: deadline, signature: signature }) + }); + vm.stopPrank(); + + assertEq(licenseTermsId, ltAmt + 1); + } + + function test_LicenseAttachmentWorkflows_mintAndRegisterIpAndAttachPILTerms_SingleTerms_DEPR() + public + withCollection + whenCallerHasMinterRole + withEnoughTokens(address(licenseAttachmentWorkflows)) + { + (address ipId1, uint256 tokenId1, uint256 licenseTermsId1) = licenseAttachmentWorkflows + .mintAndRegisterIpAndAttachPILTerms_deprecated({ + spgNftContract: address(nftContract), + recipient: caller, + ipMetadata: ipMetadataEmpty, + terms: PILFlavors.nonCommercialSocialRemixing() + }); + assertTrue(ipAssetRegistry.isRegistered(ipId1)); + assertEq(tokenId1, 1); + assertEq(licenseTermsId1, 1); + assertEq(nftContract.tokenURI(tokenId1), string.concat(testBaseURI, tokenId1.toString())); + assertMetadata(ipId1, ipMetadataEmpty); + (address licenseTemplate, uint256 licenseTermsId) = licenseRegistry.getAttachedLicenseTerms(ipId1, 0); + assertEq(licenseTemplate, address(pilTemplate)); + assertEq(licenseTermsId, licenseTermsId1); + + (address ipId2, uint256 tokenId2, uint256 licenseTermsId2) = licenseAttachmentWorkflows + .mintAndRegisterIpAndAttachPILTerms_deprecated({ + spgNftContract: address(nftContract), + recipient: caller, + ipMetadata: ipMetadataDefault, + terms: PILFlavors.nonCommercialSocialRemixing() + }); + assertTrue(ipAssetRegistry.isRegistered(ipId2)); + assertEq(tokenId2, 2); + assertEq(licenseTermsId1, licenseTermsId2); + assertEq(nftContract.tokenURI(tokenId2), string.concat(testBaseURI, ipMetadataDefault.nftMetadataURI)); + assertMetadata(ipId2, ipMetadataDefault); + } + + function test_LicenseAttachmentWorkflows_registerIpAndAttachPILTerms_SingleTerms_DEPR() + public + withCollection + whenCallerHasMinterRole + withEnoughTokens(address(licenseAttachmentWorkflows)) + { + uint256 tokenId = nftContract.mint(address(caller), ipMetadataEmpty.nftMetadataURI, "", true); + address payable ipId = payable(ipAssetRegistry.ipId(block.chainid, address(nftContract), tokenId)); + + uint256 deadline = block.timestamp + 1000; + + (bytes memory sigMetadata, bytes32 expectedState, ) = _getSetPermissionSigForPeriphery({ + ipId: ipId, + to: address(licenseAttachmentWorkflows), + module: address(coreMetadataModule), + selector: ICoreMetadataModule.setAll.selector, + deadline: deadline, + state: bytes32(0), + signerSk: sk.alice + }); + + (bytes memory sigAttach, , ) = _getSetPermissionSigForPeriphery({ + ipId: ipId, + to: address(licenseAttachmentWorkflows), + module: address(licensingModule), + selector: ILicensingModule.attachLicenseTerms.selector, + deadline: deadline, + state: expectedState, + signerSk: sk.alice + }); + + licenseAttachmentWorkflows.registerIpAndAttachPILTerms_deprecated({ + nftContract: address(nftContract), + tokenId: tokenId, + ipMetadata: ipMetadataDefault, + terms: PILFlavors.nonCommercialSocialRemixing(), + sigMetadata: WorkflowStructs.SignatureData({ signer: u.alice, deadline: deadline, signature: sigMetadata }), + sigAttach: WorkflowStructs.SignatureData({ signer: u.alice, deadline: deadline, signature: sigAttach }) + }); + } + + function test_LicenseAttachmentWorkflows_registerPILTermsAndAttach_idempotency_SingleTerms_DEPR() + public + withCollection + withIp(u.alice) + { + address payable ipId = ipAsset[1].ipId; + uint256 deadline = block.timestamp + 1000; + + (bytes memory signature1, , ) = _getSetPermissionSigForPeriphery({ + ipId: ipId, + to: address(licenseAttachmentWorkflows), + module: address(licensingModule), + selector: ILicensingModule.attachLicenseTerms.selector, + deadline: deadline, + state: IIPAccount(ipId).state(), + signerSk: sk.alice + }); + + vm.startPrank(u.alice); + uint256 licenseTermsId1 = licenseAttachmentWorkflows.registerPILTermsAndAttach_deprecated({ + ipId: ipId, + terms: PILFlavors.commercialUse({ + mintingFee: 100, + currencyToken: address(mockToken), + royaltyPolicy: address(royaltyPolicyLAP) + }), + sigAttach: WorkflowStructs.SignatureData({ signer: u.alice, deadline: deadline, signature: signature1 }) + }); + vm.stopPrank(); + + (bytes memory signature2, , ) = _getSetPermissionSigForPeriphery({ + ipId: ipId, + to: address(licenseAttachmentWorkflows), + module: address(licensingModule), + selector: ILicensingModule.attachLicenseTerms.selector, + deadline: deadline, + state: IIPAccount(ipId).state(), + signerSk: sk.alice + }); + + // attach the same license terms to the IP again, but it shouldn't revert + vm.startPrank(u.alice); + uint256 licenseTermsId2 = licenseAttachmentWorkflows.registerPILTermsAndAttach_deprecated({ + ipId: ipId, + terms: PILFlavors.commercialUse({ + mintingFee: 100, + currencyToken: address(mockToken), + royaltyPolicy: address(royaltyPolicyLAP) + }), + sigAttach: WorkflowStructs.SignatureData({ signer: u.alice, deadline: deadline, signature: signature2 }) + }); + vm.stopPrank(); + + assertEq(licenseTermsId1, licenseTermsId2); + } + + function test_revert_registerPILTermsAndAttach_DerivativesCannotAddLicenseTerms_SingleTerms_DEPR() + public + withCollection + whenCallerHasMinterRole + withEnoughTokens(address(licenseAttachmentWorkflows)) + { + (address ipIdParent, , uint256 licenseTermsIdParent) = licenseAttachmentWorkflows + .mintAndRegisterIpAndAttachPILTerms_deprecated({ + spgNftContract: address(nftContract), + recipient: caller, + ipMetadata: ipMetadataDefault, + terms: PILFlavors.nonCommercialSocialRemixing() + }); + + address[] memory parentIpIds = new address[](1); + parentIpIds[0] = ipIdParent; + + uint256[] memory licenseTermsIds = new uint256[](1); + licenseTermsIds[0] = licenseTermsIdParent; + + (address ipIdChild, ) = derivativeWorkflows.mintAndRegisterIpAndMakeDerivative_deprecated({ + spgNftContract: address(nftContract), + derivData: WorkflowStructs.MakeDerivativeDEPR({ + parentIpIds: parentIpIds, + licenseTemplate: address(pilTemplate), + licenseTermsIds: licenseTermsIds, + royaltyContext: "" + }), + ipMetadata: ipMetadataDefault, + recipient: caller + }); + + uint256 deadline = block.timestamp + 1000; + + (bytes memory signature, , ) = _getSetPermissionSigForPeriphery({ + ipId: ipIdChild, + to: address(licenseAttachmentWorkflows), + module: address(licensingModule), + selector: ILicensingModule.attachLicenseTerms.selector, + deadline: deadline, + state: IIPAccount(payable(ipIdChild)).state(), + signerSk: sk.alice + }); + + // attach a different license terms to the child ip, should revert with the correct error + vm.expectRevert(CoreErrors.LicensingModule__DerivativesCannotAddLicenseTerms.selector); + licenseAttachmentWorkflows.registerPILTermsAndAttach_deprecated({ + ipId: ipIdChild, + terms: PILFlavors.commercialUse({ + mintingFee: 100, + currencyToken: address(mockToken), + royaltyPolicy: address(royaltyPolicyLAP) + }), + sigAttach: WorkflowStructs.SignatureData({ signer: u.alice, deadline: deadline, signature: signature }) + }); + } + + function _setUpTerms() private { + terms.push( + PILFlavors.commercialRemix({ + mintingFee: 0, + commercialRevShare: 5 * 10 ** 6, // 5% + royaltyPolicy: address(royaltyPolicyLAP), + currencyToken: address(mockToken) + }) + ); + terms.push( + PILFlavors.commercialUse({ + mintingFee: 0, + currencyToken: address(mockToken), + royaltyPolicy: address(royaltyPolicyLAP) + }) + ); + terms.push( + PILFlavors.commercialUse({ + mintingFee: 0, + currencyToken: address(mockToken), + royaltyPolicy: address(royaltyPolicyLRP) + }) + ); + terms.push( + PILFlavors.commercialRemix({ + mintingFee: 0, + commercialRevShare: 5 * 10 ** 6, // 5% + royaltyPolicy: address(royaltyPolicyLRP), + currencyToken: address(mockToken) + }) + ); + terms.push( + PILFlavors.commercialRemix({ + mintingFee: 100 * 10 ** mockToken.decimals(), + commercialRevShare: 10 * 10 ** 6, // 10% + royaltyPolicy: address(royaltyPolicyLRP), + currencyToken: address(mockToken) + }) + ); + } } diff --git a/test/workflows/RegistrationWorkflows.t.sol b/test/workflows/RegistrationWorkflows.t.sol index a6ecb3b..387d9f4 100644 --- a/test/workflows/RegistrationWorkflows.t.sol +++ b/test/workflows/RegistrationWorkflows.t.sol @@ -184,12 +184,14 @@ contract RegistrationWorkflowsTest is BaseTest { signerSk: sk.alice }); + vm.startPrank(u.alice); address actualIpId = registrationWorkflows.registerIp({ nftContract: address(mockNft), tokenId: tokenId, ipMetadata: ipMetadataDefault, sigMetadata: WorkflowStructs.SignatureData({ signer: u.alice, deadline: deadline, signature: sigMetadata }) }); + vm.stopPrank(); assertEq(IIPAccount(payable(actualIpId)).state(), expectedState); assertEq(actualIpId, expectedIpId); @@ -263,4 +265,128 @@ contract RegistrationWorkflowsTest is BaseTest { assertMetadata(ipIds[i], ipMetadataDefault); } } + + //////////////////////////////////////////////////////////////////////////// + // DEPRECATED, WILL BE REMOVED IN V1.4 // + //////////////////////////////////////////////////////////////////////////// + function test_RegistrationWorkflows_revert_mintAndRegisterIp_callerNotAuthorizedToMint_DEPR() public { + vm.prank(u.alice); // minter and admin of nftContract + nftContract = ISPGNFT( + registrationWorkflows.createCollection( + ISPGNFT.InitParams({ + name: "Test Private Collection", + symbol: "TESTPRIV", + baseURI: testBaseURI, + contractURI: testContractURI, + maxSupply: 100, + mintFee: 100 * 10 ** mockToken.decimals(), + mintFeeToken: address(mockToken), + mintFeeRecipient: feeRecipient, + owner: minter, + mintOpen: true, + isPublicMinting: false // not public minting + }) + ) + ); + + vm.expectRevert(Errors.Workflow__CallerNotAuthorizedToMint.selector); + vm.prank(u.bob); // caller does not have minter role + registrationWorkflows.mintAndRegisterIp_deprecated({ + spgNftContract: address(nftContract), + recipient: u.bob, + ipMetadata: ipMetadataEmpty + }); + } + + function test_RegistrationWorkflows_mintAndRegisterIp_publicMint_DEPR() public { + vm.prank(u.alice); // minter and admin of nftContract + nftContract = ISPGNFT( + registrationWorkflows.createCollection( + ISPGNFT.InitParams({ + name: "Test Public Collection", + symbol: "TESTPUB", + baseURI: testBaseURI, + contractURI: testContractURI, + maxSupply: 100, + mintFee: 1 * 10 ** mockToken.decimals(), + mintFeeToken: address(mockToken), + mintFeeRecipient: feeRecipient, + owner: minter, + mintOpen: true, + isPublicMinting: true // public minting is enabled + }) + ) + ); + + vm.startPrank(u.bob); // caller does not have minter role + mockToken.mint(address(u.bob), 1000 * 10 ** mockToken.decimals()); + mockToken.approve(address(nftContract), 1000 * 10 ** mockToken.decimals()); + + // caller has minter role and public minting is enabled + (address ipId, uint256 tokenId) = registrationWorkflows.mintAndRegisterIp_deprecated({ + spgNftContract: address(nftContract), + recipient: u.bob, + ipMetadata: ipMetadataEmpty + }); + vm.stopPrank(); + + assertTrue(ipAssetRegistry.isRegistered(ipId)); + assertEq(tokenId, 1); + assertEq(nftContract.tokenURI(tokenId), string.concat(testBaseURI, tokenId.toString())); + assertMetadata(ipId, ipMetadataEmpty); + } + + function test_RegistrationWorkflows_mintAndRegisterIp_DEPR() public withCollection whenCallerHasMinterRole { + mockToken.mint(address(caller), 1000 * 10 ** mockToken.decimals()); + mockToken.approve(address(nftContract), 1000 * 10 ** mockToken.decimals()); + + (address ipId1, uint256 tokenId1) = registrationWorkflows.mintAndRegisterIp_deprecated({ + spgNftContract: address(nftContract), + recipient: u.bob, + ipMetadata: ipMetadataEmpty + }); + assertEq(tokenId1, 1); + assertTrue(ipAssetRegistry.isRegistered(ipId1)); + assertEq(nftContract.tokenURI(tokenId1), string.concat(testBaseURI, tokenId1.toString())); + assertMetadata(ipId1, ipMetadataEmpty); + + (address ipId2, uint256 tokenId2) = registrationWorkflows.mintAndRegisterIp_deprecated({ + spgNftContract: address(nftContract), + recipient: u.bob, + ipMetadata: ipMetadataDefault + }); + assertEq(tokenId2, 2); + assertTrue(ipAssetRegistry.isRegistered(ipId2)); + assertEq(nftContract.tokenURI(tokenId2), string.concat(testBaseURI, ipMetadataDefault.nftMetadataURI)); + assertMetadata(ipId2, ipMetadataDefault); + } + + function test_RegistrationWorkflows_multicall_mintAndRegisterIp_DEPR() + public + withCollection + whenCallerHasMinterRole + { + mockToken.mint(address(caller), 1000 * 10 * 10 ** mockToken.decimals()); + mockToken.approve(address(nftContract), 1000 * 10 * 10 ** mockToken.decimals()); + + bytes[] memory data = new bytes[](10); + for (uint256 i = 0; i < 10; i++) { + data[i] = abi.encodeWithSelector( + bytes4(keccak256("mintAndRegisterIp_deprecated(address,address,(string,bytes32,string,bytes32))")), + address(nftContract), + u.bob, + ipMetadataDefault + ); + } + bytes[] memory results = registrationWorkflows.multicall(data); + address[] memory ipIds = new address[](10); + uint256[] memory tokenIds = new uint256[](10); + + for (uint256 i = 0; i < 10; i++) { + (ipIds[i], tokenIds[i]) = abi.decode(results[i], (address, uint256)); + assertTrue(ipAssetRegistry.isRegistered(ipIds[i])); + assertEq(nftContract.tokenURI(tokenIds[i]), string.concat(testBaseURI, ipMetadataDefault.nftMetadataURI)); + assertMetadata(ipIds[i], ipMetadataDefault); + } + } } diff --git a/test/workflows/RoyaltyTokenDistributionWorkflows.t.sol b/test/workflows/RoyaltyTokenDistributionWorkflows.t.sol index b9fd737..07b475d 100644 --- a/test/workflows/RoyaltyTokenDistributionWorkflows.t.sol +++ b/test/workflows/RoyaltyTokenDistributionWorkflows.t.sol @@ -29,9 +29,15 @@ contract RoyaltyTokenDistributionWorkflowsTest is BaseTest { WorkflowStructs.RoyaltyShare[] private royaltyShares; WorkflowStructs.MakeDerivative private derivativeData; + /// DEPRECATED, WILL BE REMOVED IN V1.4---------------------------------------------------------------------------- + PILTerms[] private commRemixTerms; + WorkflowStructs.MakeDerivativeDEPR private derivativeDataDEPR; + ///---------------------------------------------------------------------------------------------------------------- + function setUp() public override { super.setUp(); _setUpTest(); + _setUpDEPR(); } function test_RoyaltyTokenDistributionWorkflows_mintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokens() @@ -53,7 +59,7 @@ contract RoyaltyTokenDistributionWorkflowsTest is BaseTest { vm.stopPrank(); assertTrue(ipAssetRegistry.isRegistered(ipId)); - assertEq(tokenId, 2); + assertEq(tokenId, 3); assertEq(spgNftPublic.tokenURI(tokenId), string.concat(testBaseURI, ipMetadataDefault.nftMetadataURI)); assertMetadata(ipId, ipMetadataDefault); _assertAttachedLicenseTerms(ipId, licenseTermsIds); @@ -79,7 +85,7 @@ contract RoyaltyTokenDistributionWorkflowsTest is BaseTest { }); vm.stopPrank(); - assertEq(tokenId, 2); + assertEq(tokenId, 3); assertEq(spgNftPublic.tokenURI(tokenId), string.concat(testBaseURI, ipMetadataDefault.nftMetadataURI)); assertEq(ipAssetRegistry.ipId(block.chainid, address(spgNftPublic), tokenId), ipId); assertMetadata(ipId, ipMetadataDefault); @@ -480,4 +486,112 @@ contract RoyaltyTokenDistributionWorkflowsTest is BaseTest { (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerSk, digest); signature = abi.encodePacked(r, s, v); } + + //////////////////////////////////////////////////////////////////////////// + // DEPRECATED, WILL BE REMOVED IN V1.4 // + //////////////////////////////////////////////////////////////////////////// + + function test_RoyaltyTokenDistributionWorkflows_mintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokens_DEPR() + public + { + vm.startPrank(u.alice); + mockToken.mint(u.alice, nftMintingFee + licenseMintingFee); + mockToken.approve(address(spgNftPublic), nftMintingFee); + mockToken.approve(address(royaltyTokenDistributionWorkflows), licenseMintingFee); + + (address ipId, uint256 tokenId, uint256[] memory licenseTermsIds) = royaltyTokenDistributionWorkflows + .mintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokens_deprecated({ + spgNftContract: address(spgNftPublic), + recipient: u.alice, + ipMetadata: ipMetadataDefault, + terms: commRemixTerms, + royaltyShares: royaltyShares + }); + vm.stopPrank(); + + assertTrue(ipAssetRegistry.isRegistered(ipId)); + assertEq(tokenId, 3); + assertEq(spgNftPublic.tokenURI(tokenId), string.concat(testBaseURI, ipMetadataDefault.nftMetadataURI)); + assertMetadata(ipId, ipMetadataDefault); + assertEq(licenseTermsIds[0], pilTemplate.getLicenseTermsId(commRemixTerms[0])); + (address licenseTemplateAttached, uint256 licenseTermsIdAttached) = licenseRegistry.getAttachedLicenseTerms( + ipId, + 0 + ); + assertEq(licenseTemplateAttached, address(pilTemplate)); + assertEq(licenseTermsIdAttached, pilTemplate.getLicenseTermsId(commRemixTerms[0])); + (licenseTemplateAttached, licenseTermsIdAttached) = licenseRegistry.getAttachedLicenseTerms(ipId, 1); + assertEq(licenseTemplateAttached, address(pilTemplate)); + assertEq(licenseTermsIdAttached, pilTemplate.getLicenseTermsId(commRemixTerms[1])); + _assertRoyaltyTokenDistribution(ipId); + } + + function test_RoyaltyTokenDistributionWorkflows_revert_RoyaltyVaultNotDeployed_DEPR() public { + vm.startPrank(u.alice); + mockToken.mint(u.alice, licenseMintingFee); + mockToken.approve(address(spgNftPublic), licenseMintingFee); + + PILTerms[] memory terms = new PILTerms[](1); + terms[0] = PILFlavors.nonCommercialSocialRemixing(); + vm.expectRevert(Errors.RoyaltyTokenDistributionWorkflows__RoyaltyVaultNotDeployed.selector); + royaltyTokenDistributionWorkflows.mintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokens_deprecated({ + spgNftContract: address(spgNftPublic), + recipient: u.alice, + ipMetadata: ipMetadataDefault, + terms: terms, + royaltyShares: royaltyShares + }); + vm.stopPrank(); + } + + function _setUpDEPR() private { + uint32 testCommRevShare = 5 * 10 ** 6; // 5% + + commRemixTerms.push( + PILFlavors.commercialRemix({ + mintingFee: licenseMintingFee, + commercialRevShare: testCommRevShare, + royaltyPolicy: address(royaltyPolicyLAP), + currencyToken: address(mockToken) + }) + ); + + commRemixTerms.push( + PILFlavors.commercialRemix({ + mintingFee: licenseMintingFee, + commercialRevShare: testCommRevShare, + royaltyPolicy: address(royaltyPolicyLRP), + currencyToken: address(mockToken) + }) + ); + + PILTerms[] memory commRemixTermsParent = new PILTerms[](1); + commRemixTermsParent[0] = PILFlavors.commercialRemix({ + mintingFee: licenseMintingFee, + commercialRevShare: testCommRevShare, + royaltyPolicy: address(royaltyPolicyLRP), + currencyToken: address(mockToken) + }); + + address[] memory ipIdParent = new address[](1); + uint256[] memory licenseTermsIdsParent; + vm.startPrank(u.alice); + mockToken.mint(u.alice, licenseMintingFee); + mockToken.approve(address(spgNftPublic), licenseMintingFee); + (ipIdParent[0], , licenseTermsIdsParent) = licenseAttachmentWorkflows + .mintAndRegisterIpAndAttachPILTerms_deprecated({ + spgNftContract: address(spgNftPublic), + recipient: u.alice, + ipMetadata: ipMetadataDefault, + terms: commRemixTermsParent + }); + vm.stopPrank(); + + derivativeDataDEPR = WorkflowStructs.MakeDerivativeDEPR({ + parentIpIds: ipIdParent, + licenseTemplate: address(pilTemplate), + licenseTermsIds: licenseTermsIdsParent, + royaltyContext: "" + }); + } } diff --git a/test/workflows/RoyaltyWorkflows.t.sol b/test/workflows/RoyaltyWorkflows.t.sol index 2ce573d..2743b51 100644 --- a/test/workflows/RoyaltyWorkflows.t.sol +++ b/test/workflows/RoyaltyWorkflows.t.sol @@ -92,7 +92,7 @@ contract RoyaltyWorkflowsTest is BaseTest { royaltyModule.maxPercent() + // 1000 * 10% = 100 royalty from childIpB (((defaultMintingFeeA * defaultCommRevShareA) / royaltyModule.maxPercent()) * defaultCommRevShareA) / royaltyModule.maxPercent() // 1000 * 10% * 10% = 10 royalty from grandChildIp - // TODO(SP-XXX): Value should be 20 but MockIPGraph in @storyprotocol/test currently only supports + // TODO(SP-XXX): Value should be 20 but MockIPGraph in @storyprotocol/test currently only supports // single-path calculation. This needs to be updated once MockIPGraph supports multi-path calculations. ); assertEq( diff --git a/yarn.lock b/yarn.lock index ca73d02..da1e3ef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -313,9 +313,9 @@ integrity sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg== "@noble/hashes@^1.4.0": - version "1.7.0" - resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.7.0.tgz#5d9e33af2c7d04fee35de1519b80c958b2e35e39" - integrity sha512-HXydb0DgzTpDPwbVeDGCG1gIu7X6+AuU6Zl6av/E/KG8LMsvPntvq+w17CHRpKBmN6Ybdrt1eP3k4cj8DJa78w== + version "1.7.1" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.7.1.tgz#5738f6d765710921e7a751e00c20ae091ed8db0f" + integrity sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ== "@nodelib/fs.scandir@2.1.5": version "2.1.5" @@ -338,25 +338,15 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@openzeppelin/contracts-upgradeable@5.0.2": - version "5.0.2" - resolved "https://registry.yarnpkg.com/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-5.0.2.tgz#3e5321a2ecdd0b206064356798c21225b6ec7105" - integrity sha512-0MmkHSHiW2NRFiT9/r5Lu4eJq5UJ4/tzlOgYXNAIj/ONkQTVnz22pLxDvp4C4uZ9he7ZFvGn3Driptn1/iU7tQ== - -"@openzeppelin/contracts-upgradeable@5.1.0": - version "5.1.0" - resolved "https://registry.yarnpkg.com/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-5.1.0.tgz#4d37648b7402929c53e2ff6e45749ecff91eb2b6" - integrity sha512-AIElwP5Ck+cslNE+Hkemf5SxjJoF4wBvvjxc27Rp+9jaPs/CLIaUBMYe1FNzhdiN0cYuwGRmYaRHmmntuiju4Q== - -"@openzeppelin/contracts@5.0.2": - version "5.0.2" - resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-5.0.2.tgz#b1d03075e49290d06570b2fd42154d76c2a5d210" - integrity sha512-ytPc6eLGcHHnapAZ9S+5qsdomhjo6QBHTDRRBFfTxXIpsicMhVPouPgmUPebZZZGX7vt9USA+Z+0M0dSVtSUEA== +"@openzeppelin/contracts-upgradeable@5.2.0": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-5.2.0.tgz#caf9a6eaf4f16d7f90f9b45a6db4e7b125f4b13b" + integrity sha512-mZIu9oa4tQTlGiOJHk6D3LdJlqFqF6oNOSn6S6UVJtzfs9UsY9/dhMEbAVTwElxUtJnjpf6yA062+oBp+eOyPg== -"@openzeppelin/contracts@5.1.0": - version "5.1.0" - resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-5.1.0.tgz#4e61162f2a2bf414c4e10c45eca98ce5f1aadbd4" - integrity sha512-p1ULhl7BXzjjbha5aqst+QMLY+4/LCWADXOCsmLHRM77AqiPjnd9vvUN9sosUfhL9JGKpZ0TjEGxgvnizmWGSA== +"@openzeppelin/contracts@5.2.0": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-5.2.0.tgz#bd020694218202b811b0ea3eec07277814c658da" + integrity sha512-bxjNie5z89W1Ea0NZLZluFh8PrFNn9DH8DQlujEok2yjsOlraUPKID5p1Wk3qdNbf6XkQ1Os2RvfiHrrXLHWKA== "@pkgr/core@^0.1.0": version "0.1.1" @@ -428,10 +418,10 @@ "@story-protocol/protocol-core@github:storyprotocol/protocol-core-v1#main": version "1.1.0" - resolved "https://codeload.github.com/storyprotocol/protocol-core-v1/tar.gz/5bb1f0f0d2962aa4ee38e24637be841c77b294e7" + resolved "https://codeload.github.com/storyprotocol/protocol-core-v1/tar.gz/108f44760d86e4e90d20a4c0e474e00df5d2c1ee" dependencies: - "@openzeppelin/contracts" "5.0.2" - "@openzeppelin/contracts-upgradeable" "5.0.2" + "@openzeppelin/contracts" "5.2.0" + "@openzeppelin/contracts-upgradeable" "5.2.0" erc6551 "^0.3.1" solady "^0.0.281" @@ -481,9 +471,9 @@ integrity sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA== "@types/node@*": - version "22.10.5" - resolved "https://registry.yarnpkg.com/@types/node/-/node-22.10.5.tgz#95af89a3fb74a2bb41ef9927f206e6472026e48b" - integrity sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ== + version "22.12.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.12.0.tgz#bf8af3b2af0837b5a62a368756ff2b705ae0048c" + integrity sha512-Fll2FZ1riMjNmlmJOdAyY5pUbkftXslB5DgEzlIuNaiWhXd00FhWxVC/r4yV/4wBb9JfImTu+jiSvXTkJ7F/gA== dependencies: undici-types "~6.20.0" @@ -500,9 +490,9 @@ integrity sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA== "@ungap/structured-clone@^1.2.0": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.1.tgz#28fa185f67daaf7b7a1a8c1d445132c5d979f8bd" - integrity sha512-fEzPV3hSkSMltkw152tJKNARhOupqbH96MZWyRjNaYZOMIzbrTeQDG+MTc6Mr2pgzFQzFxAfmhGDNP5QK++2ZA== + version "1.3.0" + resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz#d06bbb384ebcf6c505fde1c3d0ed4ddffe0aaff8" + integrity sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g== abbrev@1: version "1.1.1" @@ -1016,9 +1006,9 @@ escodegen@1.8.x: source-map "~0.2.0" eslint-plugin-prettier@^5.1.3: - version "5.2.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz#d1c8f972d8f60e414c25465c163d16f209411f95" - integrity sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw== + version "5.2.3" + resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.3.tgz#c4af01691a6fa9905207f0fbba0d7bea0902cce5" + integrity sha512-qJ+y0FfCp/mQYQ/vWQ3s7eUlFEL4PyKfAJxsnYTJ4YT73nsJBWqmEpFryxV9OeUiqmsTsYJ5Y+KDNaeP31wrRw== dependencies: prettier-linter-helpers "^1.0.0" synckit "^0.9.1" @@ -1198,14 +1188,14 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== fast-uri@^3.0.1: - version "3.0.5" - resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.5.tgz#19f5f9691d0dab9b85861a7bb5d98fca961da9cd" - integrity sha512-5JnBCWpFlMo0a3ciDy/JckMzzv1U9coZrIhedq+HXxxUfDTAiS0LA8OKVao4G9BxmCVck/jtA5r3KAtRWEyD8Q== + version "3.0.6" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.6.tgz#88f130b77cfaea2378d56bf970dea21257a68748" + integrity sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw== fastq@^1.6.0: - version "1.18.0" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.18.0.tgz#d631d7e25faffea81887fe5ea8c9010e1b36fee0" - integrity sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw== + version "1.19.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.19.0.tgz#a82c6b7c2bb4e44766d865f07997785fecfdcb89" + integrity sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA== dependencies: reusify "^1.0.4" @@ -1766,9 +1756,9 @@ log-symbols@^4.1.0: is-unicode-supported "^0.1.0" loupe@^3.1.0: - version "3.1.2" - resolved "https://registry.yarnpkg.com/loupe/-/loupe-3.1.2.tgz#c86e0696804a02218f2206124c45d8b15291a240" - integrity sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg== + version "3.1.3" + resolved "https://registry.yarnpkg.com/loupe/-/loupe-3.1.3.tgz#042a8f7986d77f3d0f98ef7990a2b2fef18b0fd2" + integrity sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug== lowercase-keys@^3.0.0: version "3.0.0" @@ -2247,9 +2237,9 @@ sc-istanbul@^0.4.5: wordwrap "^1.0.0" semver@^7.3.4, semver@^7.3.7, semver@^7.5.2, semver@^7.6.3: - version "7.6.3" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" - integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== + version "7.7.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.0.tgz#9c6fe61d0c6f9fa9e26575162ee5a9180361b09c" + integrity sha512-DrfFnPzblFmNrIZzg5RzHegbiRWg7KMR7btwi2yjHwx06zsUbO5g613sVwEV7FTwmzJu+Io0lJe2GJ3LxqpvBQ== serialize-javascript@^6.0.2: version "6.0.2"