From f559e0bb4069ea5d213137fe3ed832fa9a858b81 Mon Sep 17 00:00:00 2001 From: Kingter <83567446+kingster-will@users.noreply.github.com> Date: Mon, 22 Apr 2024 23:13:26 -0700 Subject: [PATCH] Refactor Minting Fee and Receiver Check Hooks into Unified LicensingHook (#115) * Add Unified Licensing Hook * Refactor and add more tests --- .../modules/licensing/ILicensingHook.sol | 52 ++++ .../modules/licensing/ILicensingModule.sol | 17 ++ .../modules/licensing/IMintingFeeModule.sol | 29 -- .../registries/ILicenseRegistry.sol | 23 +- contracts/lib/Errors.sol | 9 + contracts/lib/Licensing.sol | 14 +- .../modules/licensing/LicensingModule.sol | 159 +++++++---- contracts/registries/LicenseRegistry.sol | 69 +++-- script/foundry/utils/DeployHelper.sol | 93 +++--- .../mocks/module/MockLicensingHook.sol | 43 +++ .../modules/licensing/LicensingModule.t.sol | 267 +++++++++++++++++- test/foundry/registries/LicenseRegistry.t.sol | 56 ++-- 12 files changed, 617 insertions(+), 214 deletions(-) create mode 100644 contracts/interfaces/modules/licensing/ILicensingHook.sol delete mode 100644 contracts/interfaces/modules/licensing/IMintingFeeModule.sol create mode 100644 test/foundry/mocks/module/MockLicensingHook.sol diff --git a/contracts/interfaces/modules/licensing/ILicensingHook.sol b/contracts/interfaces/modules/licensing/ILicensingHook.sol new file mode 100644 index 000000000..e671a119a --- /dev/null +++ b/contracts/interfaces/modules/licensing/ILicensingHook.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.23; + +import { IModule } from "../base/IModule.sol"; + +/// @title ILicensingHook +/// @notice This interface defines the hook functions that are called by the LicensingModule when +/// executing licensing functions. +/// IP owners can configure the hook to a specific license terms or all licenses of an IP Asset. +/// @dev Developers can create a contract that implements this interface to implement various checks +/// and determine the minting price. +interface ILicensingHook is IModule { + /// @notice This function is called when the LicensingModule mints license tokens. + /// @dev The hook can be used to implement various checks and determine the minting price. + /// The hook should revert if the minting is not allowed. + /// @param caller The address of the caller who calling the mintLicenseTokens() function. + /// @param licensorIpId The ID of licensor IP from which issue the license tokens. + /// @param licenseTemplate The address of the license template. + /// @param licenseTermsId The ID of the license terms within the license template, + /// which is used to mint license tokens. + /// @param amount The amount of license tokens to mint. + /// @param receiver The address of the receiver who receive the license tokens. + /// @param hookData The data to be used by the licensing hook. + /// @return totalMintingFee The total minting fee to be paid when minting amount of license tokens. + function beforeMintLicenseTokens( + address caller, + address licensorIpId, + address licenseTemplate, + uint256 licenseTermsId, + uint256 amount, + address receiver, + bytes calldata hookData + ) external returns (uint256 totalMintingFee); + + /// @notice This function is called when the LicensingModule mints license tokens. + /// @dev The hook can be used to implement various checks and determine the minting price. + /// The hook should revert if the registering of derivative is not allowed. + /// @param childIpId The derivative IP ID. + /// @param parentIpId The parent IP ID. + /// @param licenseTemplate The address of the license template. + /// @param licenseTermsId The ID of the license terms within the license template. + /// @param hookData The data to be used by the licensing hook. + /// @return mintingFee The minting fee to be paid when register child IP to the parent IP as derivative. + function beforeRegisterDerivative( + address caller, + address childIpId, + address parentIpId, + address licenseTemplate, + uint256 licenseTermsId, + bytes calldata hookData + ) external returns (uint256 mintingFee); +} diff --git a/contracts/interfaces/modules/licensing/ILicensingModule.sol b/contracts/interfaces/modules/licensing/ILicensingModule.sol index 4a803fb15..691748fec 100644 --- a/contracts/interfaces/modules/licensing/ILicensingModule.sol +++ b/contracts/interfaces/modules/licensing/ILicensingModule.sol @@ -2,6 +2,7 @@ pragma solidity 0.8.23; import { IModule } from "../base/IModule.sol"; +import { Licensing } from "../../../lib/Licensing.sol"; /// @title ILicensingModule /// @notice This interface defines the entry point for users to manage licenses in the Story Protocol. @@ -121,4 +122,20 @@ interface ILicensingModule is IModule { uint256[] calldata licenseTokenIds, bytes calldata royaltyContext ) external; + + /// @notice Sets the licensing configuration for a specific license terms of an IP. + /// If both licenseTemplate and licenseTermsId are not specified then the licensing config apply + /// to all licenses of given IP. + /// @param ipId The address of the IP for which the configuration is being set. + /// @param licenseTemplate The address of the license template used. + /// If not specified, the configuration applies to all licenses. + /// @param licenseTermsId The ID of the license terms within the license template. + /// If not specified, the configuration applies to all licenses. + /// @param licensingConfig The licensing configuration for the license. + function setLicensingConfig( + address ipId, + address licenseTemplate, + uint256 licenseTermsId, + Licensing.LicensingConfig memory licensingConfig + ) external; } diff --git a/contracts/interfaces/modules/licensing/IMintingFeeModule.sol b/contracts/interfaces/modules/licensing/IMintingFeeModule.sol deleted file mode 100644 index 04f30329e..000000000 --- a/contracts/interfaces/modules/licensing/IMintingFeeModule.sol +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.23; - -import { IModule } from "../base/IModule.sol"; - -/// @title IMintingFeeModule -/// @notice This interface is used to determine the minting fee of a license token. -/// IP owners can configure the MintingFeeModule to a specific license terms or all licenses of an IP Asset. -/// When someone calls the `mintLicenseTokens` function of LicensingModule, the LicensingModule will check whether -/// the license term or IP Asset has been configured with this module. If so, LicensingModule will call this module -/// to determine the minting fee of the license token. -/// @dev Developers can create a contract that implements this interface to implement various algorithms to determine -/// the minting price, -/// for example, a bonding curve formula. This allows IP owners to configure the module to hook into the LicensingModule -/// when minting a license token. -interface IMintingFeeModule is IModule { - /// @notice Calculates the total minting fee for a given amount of license tokens. - /// @param ipId The IP ID. - /// @param licenseTemplate The address of the license template. - /// @param licenseTermsId The ID of the license terms within the license template. - /// @param amount The amount of license tokens to mint. - /// @return The total minting fee. - function getMintingFee( - address ipId, - address licenseTemplate, - uint256 licenseTermsId, - uint256 amount - ) external view returns (uint256); -} diff --git a/contracts/interfaces/registries/ILicenseRegistry.sol b/contracts/interfaces/registries/ILicenseRegistry.sol index 4ed4278c2..d089155c6 100644 --- a/contracts/interfaces/registries/ILicenseRegistry.sol +++ b/contracts/interfaces/registries/ILicenseRegistry.sol @@ -13,14 +13,14 @@ interface ILicenseRegistry { event LicenseTemplateRegistered(address indexed licenseTemplate); /// @notice Emitted when a minting license configuration is set. - event MintingLicenseConfigSetLicense( + event LicensingConfigSetForLicense( address indexed ipId, address indexed licenseTemplate, uint256 indexed licenseTermsId ); /// @notice Emitted when a minting license configuration is set for all licenses of an IP. - event MintingLicenseConfigSetForIP(address indexed ipId, Licensing.MintingLicenseConfig mintingLicenseConfig); + event LicensingConfigSetForIP(address indexed ipId, Licensing.LicensingConfig licensingConfig); /// @notice Emitted when an expiration time is set for an IP. event ExpirationTimeSet(address indexed ipId, uint256 expireTime); @@ -75,7 +75,7 @@ interface ILicenseRegistry { address licenseTemplate, uint256 licenseTermsId, bool isMintedByIpOwner - ) external view returns (Licensing.MintingLicenseConfig memory); + ) external view returns (Licensing.LicensingConfig memory); /// @notice Attaches license terms to an IP. /// @param ipId The address of the IP to which the license terms are attached. @@ -149,34 +149,31 @@ interface ILicenseRegistry { /// @param licenseTemplate The address of the license template where the license terms are defined. /// @param licenseTermsId The ID of the license terms. /// @return The configuration for minting the license. - function getMintingLicenseConfig( + function getLicensingConfig( address ipId, address licenseTemplate, uint256 licenseTermsId - ) external view returns (Licensing.MintingLicenseConfig memory); + ) external view returns (Licensing.LicensingConfig memory); /// @notice Sets the minting license configuration for a specific license attached to a specific IP. /// @dev This function can only be called by the LicensingModule. /// @param ipId The address of the IP for which the configuration is being set. /// @param licenseTemplate The address of the license template used. /// @param licenseTermsId The ID of the license terms within the license template. - /// @param mintingLicenseConfig The configuration for minting the license. - function setMintingLicenseConfigForLicense( + /// @param licensingConfig The configuration for minting the license. + function setLicensingConfigForLicense( address ipId, address licenseTemplate, uint256 licenseTermsId, - Licensing.MintingLicenseConfig calldata mintingLicenseConfig + Licensing.LicensingConfig calldata licensingConfig ) external; /// @notice Sets the MintingLicenseConfig for an IP and applies it to all licenses attached to the IP. /// @dev This function will set a global configuration for all licenses under a specific IP. /// However, this global configuration can be overridden by a configuration set at a specific license level. /// @param ipId The IP ID for which the configuration is being set. - /// @param mintingLicenseConfig The MintingLicenseConfig to be set for all licenses under the given IP. - function setMintingLicenseConfigForIp( - address ipId, - Licensing.MintingLicenseConfig calldata mintingLicenseConfig - ) external; + /// @param licensingConfig The MintingLicenseConfig to be set for all licenses under the given IP. + function setLicensingConfigForIp(address ipId, Licensing.LicensingConfig calldata licensingConfig) external; /// @notice Sets the expiration time for an IP. /// @param ipId The address of the IP. diff --git a/contracts/lib/Errors.sol b/contracts/lib/Errors.sol index 35f2a55a5..11d87bd32 100644 --- a/contracts/lib/Errors.sol +++ b/contracts/lib/Errors.sol @@ -75,6 +75,9 @@ library Errors { /// @notice Zero address provided for License Token. error LicensingModule__ZeroLicenseToken(); + /// @notice Zero address provided for Module Registry. + error LicensingModule__ZeroModuleRegistry(); + /// @notice Zero address provided for Licensing Module. error LicenseRegistry__ZeroLicensingModule(); @@ -219,6 +222,12 @@ library Errors { address licensorIpId ); + /// @notice Licensing hook is invalid either not support ILicensingHook interface or not registered as module + error LicensingModule__InvalidLicensingHook(address hook); + + /// @notice The license terms ID is invalid or license template doesn't exist. + error LicensingModule__InvalidLicenseTermsId(address licenseTemplate, uint256 licenseTermsId); + //////////////////////////////////////////////////////////////////////////// // Dispute Module // //////////////////////////////////////////////////////////////////////////// diff --git a/contracts/lib/Licensing.sol b/contracts/lib/Licensing.sol index 25c7e3226..4c5d85172 100644 --- a/contracts/lib/Licensing.sol +++ b/contracts/lib/Licensing.sol @@ -7,20 +7,18 @@ library Licensing { /// @notice This struct is used by IP owners to define the configuration /// when others are minting license tokens of their IP through the LicensingModule. /// When the `mintLicenseTokens` function of LicensingModule is called, the LicensingModule will read - /// this configuration to determine the minting fee and who can receive the license tokens. + /// this configuration to determine the minting fee and execute the licensing hook if set. /// IP owners can set these configurations for each License or set the configuration for the IP /// so that the configuration applies to all licenses of the IP. /// If both the license and IP have the configuration, then the license configuration takes precedence. /// @param isSet Whether the configuration is set or not. /// @param mintingFee The minting fee to be paid when minting license tokens. - /// @param mintingFeeModule The module that determines the minting fee. - /// @param receiverCheckModule The module that determines who can receive the license tokens. - /// @param receiverCheckData The data to be used by the receiver check module. - struct MintingLicenseConfig { + /// @param licensingHook The hook contract address for the licensing module, or address(0) if none + /// @param hookData The data to be used by the licensing hook. + struct LicensingConfig { bool isSet; uint256 mintingFee; - address mintingFeeModule; - address receiverCheckModule; - bytes receiverCheckData; + address licensingHook; + bytes hookData; } } diff --git a/contracts/modules/licensing/LicensingModule.sol b/contracts/modules/licensing/LicensingModule.sol index 76b66498e..6815b53d2 100644 --- a/contracts/modules/licensing/LicensingModule.sol +++ b/contracts/modules/licensing/LicensingModule.sol @@ -21,12 +21,12 @@ import { RoyaltyModule } from "../../modules/royalty/RoyaltyModule.sol"; import { AccessControlled } from "../../access/AccessControlled.sol"; import { LICENSING_MODULE_KEY } from "../../lib/modules/Module.sol"; import { BaseModule } from "../BaseModule.sol"; -import { ILicenseTemplate } from "contracts/interfaces/modules/licensing/ILicenseTemplate.sol"; -import { IMintingFeeModule } from "contracts/interfaces/modules/licensing/IMintingFeeModule.sol"; +import { ILicenseTemplate } from "../../interfaces/modules/licensing/ILicenseTemplate.sol"; import { IPAccountStorageOps } from "../../lib/IPAccountStorageOps.sol"; -import { IHookModule } from "../../interfaces/modules/base/IHookModule.sol"; import { ILicenseToken } from "../../interfaces/ILicenseToken.sol"; import { ProtocolPausableUpgradeable } from "../../pause/ProtocolPausableUpgradeable.sol"; +import { ILicensingHook } from "../..//interfaces/modules/licensing/ILicensingHook.sol"; +import { IModuleRegistry } from "../../interfaces/registries/IModuleRegistry.sol"; /// @title Licensing Module /// @notice Licensing module is the main entry point for the licensing system. It is responsible for: @@ -67,6 +67,8 @@ contract LicensingModule is /// @custom:oz-upgrades-unsafe-allow state-variable-immutable ILicenseToken public immutable LICENSE_NFT; + IModuleRegistry public immutable MODULE_REGISTRY; + // keccak256(abi.encode(uint256(keccak256("story-protocol.LicensingModule")) - 1)) & ~bytes32(uint256(0xff)); bytes32 private constant LicensingModuleStorageLocation = 0x0f7178cb62e4803c52d40f70c08a6f88d6ee1af1838d58e0c83a222a6c3d3100; @@ -81,6 +83,7 @@ contract LicensingModule is constructor( address accessController, address ipAccountRegistry, + address moduleRegistry, address royaltyModule, address licenseRegistry, address disputeModule, @@ -90,6 +93,8 @@ contract LicensingModule is if (licenseRegistry == address(0)) revert Errors.LicensingModule__ZeroLicenseRegistry(); if (disputeModule == address(0)) revert Errors.LicensingModule__ZeroDisputeModule(); if (licenseToken == address(0)) revert Errors.LicensingModule__ZeroLicenseToken(); + if (moduleRegistry == address(0)) revert Errors.LicensingModule__ZeroModuleRegistry(); + MODULE_REGISTRY = IModuleRegistry(moduleRegistry); ROYALTY_MODULE = RoyaltyModule(royaltyModule); LICENSE_REGISTRY = ILicenseRegistry(licenseRegistry); DISPUTE_MODULE = IDisputeModule(disputeModule); @@ -149,7 +154,7 @@ contract LicensingModule is uint256 amount, address receiver, bytes calldata royaltyContext - ) external whenNotPaused returns (uint256 startLicenseTokenId) { + ) external whenNotPaused nonReentrant returns (uint256 startLicenseTokenId) { if (amount == 0) { revert Errors.LicensingModule__MintAmountZero(); } @@ -158,20 +163,26 @@ contract LicensingModule is } _verifyIpNotDisputed(licensorIpId); - - Licensing.MintingLicenseConfig memory mlc = LICENSE_REGISTRY.verifyMintLicenseToken( + Licensing.LicensingConfig memory lsc = LICENSE_REGISTRY.verifyMintLicenseToken( licensorIpId, licenseTemplate, licenseTermsId, _hasPermission(licensorIpId) ); - if (mlc.receiverCheckModule != address(0)) { - if (!IHookModule(mlc.receiverCheckModule).verify(receiver, mlc.receiverCheckData)) { - revert Errors.LicensingModule__ReceiverCheckFailed(receiver); - } + uint256 mintingFeeByHook = 0; + if (lsc.licensingHook != address(0)) { + mintingFeeByHook = ILicensingHook(lsc.licensingHook).beforeMintLicenseTokens( + msg.sender, + licensorIpId, + licenseTemplate, + licenseTermsId, + amount, + receiver, + lsc.hookData + ); } - _payMintingFee(licensorIpId, licenseTemplate, licenseTermsId, amount, royaltyContext, mlc); + _payMintingFee(licensorIpId, licenseTemplate, licenseTermsId, amount, royaltyContext, lsc, mintingFeeByHook); if (!ILicenseTemplate(licenseTemplate).verifyMintLicenseToken(licenseTermsId, receiver, licensorIpId, amount)) { revert Errors.LicensingModule__LicenseDenyMintLicenseToken(licenseTemplate, licenseTermsId, licensorIpId); @@ -246,6 +257,7 @@ contract LicensingModule is LICENSE_REGISTRY.registerDerivativeIp(childIpId, parentIpIds, licenseTemplate, licenseTermsIds); // Process the payment for the minting fee. (address commonRoyaltyPolicy, bytes[] memory royaltyDatas) = _payMintingFeeForAllParentIps( + childIpId, parentIpIds, licenseTermsIds, licenseTemplate, @@ -340,11 +352,43 @@ contract LicensingModule is ); } + /// @notice Sets the licensing configuration for a specific license terms of an IP. + /// If both licenseTemplate and licenseTermsId are not specified then the licensing config apply + /// to all licenses of given IP. + /// @param ipId The address of the IP for which the configuration is being set. + /// @param licenseTemplate The address of the license template used. + /// If not specified, the configuration applies to all licenses. + /// @param licenseTermsId The ID of the license terms within the license template. + /// If not specified, the configuration applies to all licenses. + /// @param licensingConfig The licensing configuration for the license. + function setLicensingConfig( + address ipId, + address licenseTemplate, + uint256 licenseTermsId, + Licensing.LicensingConfig memory licensingConfig + ) external verifyPermission(ipId) { + if ( + licensingConfig.licensingHook != address(0) && + (!licensingConfig.licensingHook.supportsInterface(type(ILicensingHook).interfaceId) || + !MODULE_REGISTRY.isRegistered(licensingConfig.licensingHook)) + ) { + revert Errors.LicensingModule__InvalidLicensingHook(licensingConfig.licensingHook); + } + if (licenseTemplate == address(0) && licenseTermsId == 0) { + LICENSE_REGISTRY.setLicensingConfigForIp(ipId, licensingConfig); + } else if (licenseTemplate != address(0) && licenseTermsId != 0) { + LICENSE_REGISTRY.setLicensingConfigForLicense(ipId, licenseTemplate, licenseTermsId, licensingConfig); + } else { + revert Errors.LicensingModule__InvalidLicenseTermsId(licenseTemplate, licenseTermsId); + } + } + /// @dev pay minting fee for all parent IPs /// This function is called by registerDerivative /// It pays the minting fee for all parent IPs through the royalty module /// finally returns the common royalty policy and data for the parent IPs function _payMintingFeeForAllParentIps( + address childIpId, address[] calldata parentIpIds, uint256[] calldata licenseTermsIds, address licenseTemplate, @@ -356,25 +400,13 @@ contract LicensingModule is // pay minting fee for all parent IPs for (uint256 i = 0; i < parentIpIds.length; i++) { - uint256 lcId = licenseTermsIds[i]; - Licensing.MintingLicenseConfig memory mlc = LICENSE_REGISTRY.getMintingLicenseConfig( - parentIpIds[i], - licenseTemplate, - lcId - ); - // check childIpOwner is qualified with check receiver module - if (mlc.receiverCheckModule != address(0)) { - if (!IHookModule(mlc.receiverCheckModule).verify(childIpOwner, mlc.receiverCheckData)) { - revert Errors.LicensingModule__ReceiverCheckFailed(childIpOwner); - } - } - (address royaltyPolicy, bytes memory royaltyData) = _payMintingFee( + (address royaltyPolicy, bytes memory royaltyData) = _executeLicensingHookAndPayMintingFee( + childIpId, parentIpIds[i], licenseTemplate, - lcId, - 1, - royaltyContext, - mlc + licenseTermsIds[i], + childIpOwner, + royaltyContext ); royaltyDatas[i] = royaltyData; // royaltyPolicy must be the same for all parent IPs and royaltyPolicy could be 0 @@ -387,6 +419,42 @@ contract LicensingModule is } } + function _executeLicensingHookAndPayMintingFee( + address childIpId, + address parentIpId, + address licenseTemplate, + uint256 licenseTermsId, + address childIpOwner, + bytes calldata royaltyContext + ) private returns (address royaltyPolicy, bytes memory royaltyData) { + Licensing.LicensingConfig memory lsc = LICENSE_REGISTRY.getLicensingConfig( + parentIpId, + licenseTemplate, + licenseTermsId + ); + // check childIpOwner is qualified with check receiver module + uint256 mintingFeeByHook = 0; + if (lsc.licensingHook != address(0)) { + mintingFeeByHook = ILicensingHook(lsc.licensingHook).beforeRegisterDerivative( + msg.sender, + childIpId, + parentIpId, + licenseTemplate, + licenseTermsId, + lsc.hookData + ); + } + (royaltyPolicy, royaltyData) = _payMintingFee( + parentIpId, + licenseTemplate, + licenseTermsId, + 1, + royaltyContext, + lsc, + mintingFeeByHook + ); + } + /// @dev pay minting fee for an parent IP /// This function is called by mintLicenseTokens and registerDerivative /// It initialize royalty module and pays the minting fee for the parent IP through the royalty module @@ -396,7 +464,7 @@ contract LicensingModule is /// @param licenseTermsId The ID of the license terms. /// @param amount The amount of license tokens to mint. /// @param royaltyContext The context of the royalty. - /// @param mlc The minting license config + /// @param licensingConfig The minting license config /// @return royaltyPolicy The address of the royalty policy. /// @return royaltyData The data of the royalty policy. function _payMintingFee( @@ -405,16 +473,17 @@ contract LicensingModule is uint256 licenseTermsId, uint256 amount, bytes calldata royaltyContext, - Licensing.MintingLicenseConfig memory mlc + Licensing.LicensingConfig memory licensingConfig, + uint256 mintingFeeByHook ) private returns (address royaltyPolicy, bytes memory royaltyData) { ILicenseTemplate lct = ILicenseTemplate(licenseTemplate); - uint256 mintingFee = 0; + uint256 mintingFeeByLicense = 0; address currencyToken = address(0); - (royaltyPolicy, royaltyData, mintingFee, currencyToken) = lct.getRoyaltyPolicy(licenseTermsId); + (royaltyPolicy, royaltyData, mintingFeeByLicense, currencyToken) = lct.getRoyaltyPolicy(licenseTermsId); if (royaltyPolicy != address(0)) { ROYALTY_MODULE.onLicenseMinting(parentIpId, royaltyPolicy, royaltyData, royaltyContext); - uint256 tmf = _getTotalMintingFee(mlc, parentIpId, licenseTemplate, licenseTermsId, mintingFee, amount); + uint256 tmf = _getTotalMintingFee(licensingConfig, mintingFeeByHook, mintingFeeByLicense, amount); // pay minting fee if (tmf > 0) { ROYALTY_MODULE.payLicenseMintingFee(parentIpId, msg.sender, royaltyPolicy, currencyToken, tmf); @@ -425,29 +494,19 @@ contract LicensingModule is /// @dev get total minting fee /// There are 3 places to get the minting fee: license terms, MintingLicenseConfig, MintingFeeModule /// The order of priority is MintingFeeModule > MintingLicenseConfig > > license terms - /// @param mintingLicenseConfig The minting license config - /// @param licensorIpId The licensor IP ID. - /// @param licenseTemplate The address of the license template. - /// @param licenseTermsId The ID of the license terms. + /// @param licensingConfig The minting license config + /// @param mintingFeeSetByHook The minting fee set by the hook. /// @param mintingFeeSetByLicenseTerms The minting fee set by the license terms. /// @param amount The amount of license tokens to mint. function _getTotalMintingFee( - Licensing.MintingLicenseConfig memory mintingLicenseConfig, - address licensorIpId, - address licenseTemplate, - uint256 licenseTermsId, + Licensing.LicensingConfig memory licensingConfig, + uint256 mintingFeeSetByHook, uint256 mintingFeeSetByLicenseTerms, uint256 amount ) private view returns (uint256) { - if (!mintingLicenseConfig.isSet) return mintingFeeSetByLicenseTerms * amount; - if (mintingLicenseConfig.mintingFeeModule == address(0)) return mintingLicenseConfig.mintingFee * amount; - return - IMintingFeeModule(mintingLicenseConfig.mintingFeeModule).getMintingFee( - licensorIpId, - licenseTemplate, - licenseTermsId, - amount - ); + if (!licensingConfig.isSet) return mintingFeeSetByLicenseTerms * amount; + if (licensingConfig.licensingHook == address(0)) return licensingConfig.mintingFee * amount; + return mintingFeeSetByHook; } /// @dev Verifies if the IP is disputed diff --git a/contracts/registries/LicenseRegistry.sol b/contracts/registries/LicenseRegistry.sol index c0b11b4bf..e8f74046b 100644 --- a/contracts/registries/LicenseRegistry.sol +++ b/contracts/registries/LicenseRegistry.sol @@ -42,8 +42,8 @@ contract LicenseRegistry is ILicenseRegistry, AccessManagedUpgradeable, UUPSUpgr /// @param attachedLicenseTerms Mapping of attached license terms to IP IDs /// @param licenseTemplates Mapping of license templates to IP IDs /// @param expireTimes Mapping of IP IDs to expire times - /// @param mintingLicenseConfigs Mapping of minting license configs to a licenseTerms of an IP - /// @param mintingLicenseConfigsForIp Mapping of minting license configs to an IP, + /// @param licensingConfigs Mapping of minting license configs to a licenseTerms of an IP + /// @param licensingConfigsForIp Mapping of minting license configs to an IP, /// the config will apply to all licenses under the IP /// @dev Storage structure for the LicenseRegistry /// @custom:storage-location erc7201:story-protocol.LicenseRegistry @@ -55,8 +55,8 @@ contract LicenseRegistry is ILicenseRegistry, AccessManagedUpgradeable, UUPSUpgr mapping(address parentIpId => EnumerableSet.AddressSet childIpIds) childIps; mapping(address ipId => EnumerableSet.UintSet licenseTermsIds) attachedLicenseTerms; mapping(address ipId => address licenseTemplate) licenseTemplates; - mapping(bytes32 ipLicenseHash => Licensing.MintingLicenseConfig mintingLicenseConfig) mintingLicenseConfigs; - mapping(address ipId => Licensing.MintingLicenseConfig mintingLicenseConfig) mintingLicenseConfigsForIp; + mapping(bytes32 ipLicenseHash => Licensing.LicensingConfig licensingConfig) licensingConfigs; + mapping(address ipId => Licensing.LicensingConfig licensingConfig) licensingConfigsForIp; } // keccak256(abi.encode(uint256(keccak256("story-protocol.LicenseRegistry")) - 1)) & ~bytes32(uint256(0xff)); @@ -122,47 +122,44 @@ contract LicenseRegistry is ILicenseRegistry, AccessManagedUpgradeable, UUPSUpgr /// @param ipId The address of the IP for which the configuration is being set. /// @param licenseTemplate The address of the license template used. /// @param licenseTermsId The ID of the license terms within the license template. - /// @param mintingLicenseConfig The configuration for minting the license. - function setMintingLicenseConfigForLicense( + /// @param licensingConfig The configuration for minting the license. + function setLicensingConfigForLicense( address ipId, address licenseTemplate, uint256 licenseTermsId, - Licensing.MintingLicenseConfig calldata mintingLicenseConfig + Licensing.LicensingConfig calldata licensingConfig ) external onlyLicensingModule { LicenseRegistryStorage storage $ = _getLicenseRegistryStorage(); if (!$.registeredLicenseTemplates[licenseTemplate]) { revert Errors.LicenseRegistry__UnregisteredLicenseTemplate(licenseTemplate); } - $.mintingLicenseConfigs[_getIpLicenseHash(ipId, licenseTemplate, licenseTermsId)] = Licensing - .MintingLicenseConfig({ - isSet: true, - mintingFee: mintingLicenseConfig.mintingFee, - mintingFeeModule: mintingLicenseConfig.mintingFeeModule, - receiverCheckModule: mintingLicenseConfig.receiverCheckModule, - receiverCheckData: mintingLicenseConfig.receiverCheckData - }); + $.licensingConfigs[_getIpLicenseHash(ipId, licenseTemplate, licenseTermsId)] = Licensing.LicensingConfig({ + isSet: true, + mintingFee: licensingConfig.mintingFee, + licensingHook: licensingConfig.licensingHook, + hookData: licensingConfig.hookData + }); - emit MintingLicenseConfigSetLicense(ipId, licenseTemplate, licenseTermsId); + emit LicensingConfigSetForLicense(ipId, licenseTemplate, licenseTermsId); } - /// @notice Sets the MintingLicenseConfig for an IP and applies it to all licenses attached to the IP. + /// @notice Sets the LicensingConfig for an IP and applies it to all licenses attached to the IP. /// @dev This function will set a global configuration for all licenses under a specific IP. /// However, this global configuration can be overridden by a configuration set at a specific license level. /// @param ipId The IP ID for which the configuration is being set. - /// @param mintingLicenseConfig The MintingLicenseConfig to be set for all licenses under the given IP. - function setMintingLicenseConfigForIp( + /// @param licensingConfig The LicensingConfig to be set for all licenses under the given IP. + function setLicensingConfigForIp( address ipId, - Licensing.MintingLicenseConfig calldata mintingLicenseConfig + Licensing.LicensingConfig calldata licensingConfig ) external onlyLicensingModule { LicenseRegistryStorage storage $ = _getLicenseRegistryStorage(); - $.mintingLicenseConfigsForIp[ipId] = Licensing.MintingLicenseConfig({ + $.licensingConfigsForIp[ipId] = Licensing.LicensingConfig({ isSet: true, - mintingFee: mintingLicenseConfig.mintingFee, - mintingFeeModule: mintingLicenseConfig.mintingFeeModule, - receiverCheckModule: mintingLicenseConfig.receiverCheckModule, - receiverCheckData: mintingLicenseConfig.receiverCheckData + mintingFee: licensingConfig.mintingFee, + licensingHook: licensingConfig.licensingHook, + hookData: licensingConfig.hookData }); - emit MintingLicenseConfigSetForIP(ipId, mintingLicenseConfig); + emit LicensingConfigSetForIP(ipId, licensingConfig); } /// @notice Attaches license terms to an IP. @@ -251,7 +248,7 @@ contract LicenseRegistry is ILicenseRegistry, AccessManagedUpgradeable, UUPSUpgr address licenseTemplate, uint256 licenseTermsId, bool isMintedByIpOwner - ) external view returns (Licensing.MintingLicenseConfig memory) { + ) external view returns (Licensing.LicensingConfig memory) { LicenseRegistryStorage storage $ = _getLicenseRegistryStorage(); if (_isExpiredNow(licensorIpId)) { revert Errors.LicenseRegistry__ParentIpExpired(licensorIpId); @@ -263,7 +260,7 @@ contract LicenseRegistry is ILicenseRegistry, AccessManagedUpgradeable, UUPSUpgr } else if (!_hasIpAttachedLicenseTerms(licensorIpId, licenseTemplate, licenseTermsId)) { revert Errors.LicenseRegistry__LicensorIpHasNoLicenseTerms(licensorIpId, licenseTemplate, licenseTermsId); } - return _getMintingLicenseConfig(licensorIpId, licenseTemplate, licenseTermsId); + return _getLicensingConfig(licensorIpId, licenseTemplate, licenseTermsId); } /// @notice Checks if a license template is registered. @@ -380,12 +377,12 @@ contract LicenseRegistry is ILicenseRegistry, AccessManagedUpgradeable, UUPSUpgr /// @param licenseTemplate The address of the license template where the license terms are defined. /// @param licenseTermsId The ID of the license terms. /// @return The configuration for minting the license. - function getMintingLicenseConfig( + function getLicensingConfig( address ipId, address licenseTemplate, uint256 licenseTermsId - ) external view returns (Licensing.MintingLicenseConfig memory) { - return _getMintingLicenseConfig(ipId, licenseTemplate, licenseTermsId); + ) external view returns (Licensing.LicensingConfig memory) { + return _getLicensingConfig(ipId, licenseTemplate, licenseTermsId); } /// @notice Gets the expiration time for an IP. @@ -485,19 +482,19 @@ contract LicenseRegistry is ILicenseRegistry, AccessManagedUpgradeable, UUPSUpgr /// @param ipId The address of the IP. /// @param licenseTemplate The address of the license template where the license terms are defined. /// @param licenseTermsId The ID of the license terms. - function _getMintingLicenseConfig( + function _getLicensingConfig( address ipId, address licenseTemplate, uint256 licenseTermsId - ) internal view returns (Licensing.MintingLicenseConfig memory) { + ) internal view returns (Licensing.LicensingConfig memory) { LicenseRegistryStorage storage $ = _getLicenseRegistryStorage(); if (!$.registeredLicenseTemplates[licenseTemplate]) { revert Errors.LicenseRegistry__UnregisteredLicenseTemplate(licenseTemplate); } - if ($.mintingLicenseConfigs[_getIpLicenseHash(ipId, licenseTemplate, licenseTermsId)].isSet) { - return $.mintingLicenseConfigs[_getIpLicenseHash(ipId, licenseTemplate, licenseTermsId)]; + if ($.licensingConfigs[_getIpLicenseHash(ipId, licenseTemplate, licenseTermsId)].isSet) { + return $.licensingConfigs[_getIpLicenseHash(ipId, licenseTemplate, licenseTermsId)]; } - return $.mintingLicenseConfigsForIp[ipId]; + return $.licensingConfigsForIp[ipId]; } /// @dev Get the hash of the IP ID, license template, and license terms ID diff --git a/script/foundry/utils/DeployHelper.sol b/script/foundry/utils/DeployHelper.sol index 7bbf312bc..e1d12dff1 100644 --- a/script/foundry/utils/DeployHelper.sol +++ b/script/foundry/utils/DeployHelper.sol @@ -163,33 +163,7 @@ contract DeployHelper is Script, BroadcastManager, JsonDeploymentHandler, Storag function _deployProtocolContracts() private { require(address(erc20) != address(0), "Deploy: Asset Not Set"); - bytes32 ipAccountImplSalt = keccak256( - abi.encode(type(IPAccountImpl).creationCode, address(this), block.timestamp) - ); - address ipAccountImplAddr = Create3Deployer.getDeployed(ipAccountImplSalt); - - bytes32 licenseTokenSalt = keccak256( - abi.encode(type(LicenseToken).creationCode, address(this), block.timestamp) - ); - address licenseTokenAddr = Create3Deployer.getDeployed(licenseTokenSalt); - - bytes32 licensingModuleSalt = keccak256( - abi.encode(type(LicensingModule).creationCode, address(this), block.timestamp) - ); - address licensingModuleAddr = Create3Deployer.getDeployed(licensingModuleSalt); - - bytes32 disputeModuleSalt = keccak256( - abi.encode(type(DisputeModule).creationCode, address(this), block.timestamp) - ); - address disputeModuleAddr = Create3Deployer.getDeployed(disputeModuleSalt); - - bytes32 licenseRegistrySalt = keccak256( - abi.encode(type(LicenseRegistry).creationCode, address(this), block.timestamp) - ); - address licenseRegistryAddr = Create3Deployer.getDeployed(licenseRegistrySalt); - string memory contractKey; - // Core Protocol Contracts contractKey = "ProtocolAccessManager"; @@ -216,7 +190,7 @@ contract DeployHelper is Script, BroadcastManager, JsonDeploymentHandler, Storag contractKey = "IPAssetRegistry"; _predeploy(contractKey); - impl = address(new IPAssetRegistry(address(erc6551Registry), ipAccountImplAddr)); + impl = address(new IPAssetRegistry(address(erc6551Registry), _getDeployedAddress(type(IPAccountImpl).name))); ipAssetRegistry = IPAssetRegistry( TestProxyHelper.deployUUPSProxy( impl, @@ -242,15 +216,23 @@ contract DeployHelper is Script, BroadcastManager, JsonDeploymentHandler, Storag contractKey = "LicenseRegistry"; _predeploy(contractKey); - impl = address(new LicenseRegistry(licensingModuleAddr, disputeModuleAddr)); + impl = address( + new LicenseRegistry( + _getDeployedAddress(type(LicensingModule).name), + _getDeployedAddress(type(DisputeModule).name) + ) + ); licenseRegistry = LicenseRegistry( TestProxyHelper.deployUUPSProxy( - licenseRegistrySalt, + _getSalt(type(LicenseRegistry).name), impl, abi.encodeCall(LicenseRegistry.initialize, (address(protocolAccessManager))) ) ); - require(licenseRegistryAddr == address(licenseRegistry), "Deploy: License Registry Address Mismatch"); + require( + _getDeployedAddress(type(LicenseRegistry).name) == address(licenseRegistry), + "Deploy: License Registry Address Mismatch" + ); require(_loadProxyImpl(address(licenseRegistry)) == impl, "LicenseRegistry Proxy Implementation Mismatch"); impl = address(0); // Make sure we don't deploy wrong impl _postdeploy(contractKey, address(licenseRegistry)); @@ -266,9 +248,14 @@ contract DeployHelper is Script, BroadcastManager, JsonDeploymentHandler, Storag ) ); _predeploy(contractKey); - ipAccountImpl = IPAccountImpl(payable(Create3Deployer.deploy(ipAccountImplSalt, ipAccountImplCode))); + ipAccountImpl = IPAccountImpl( + payable(Create3Deployer.deploy(_getSalt(type(IPAccountImpl).name), ipAccountImplCode)) + ); _postdeploy(contractKey, address(ipAccountImpl)); - require(ipAccountImplAddr == address(ipAccountImpl), "Deploy: IP Account Impl Address Mismatch"); + require( + _getDeployedAddress(type(IPAccountImpl).name) == address(ipAccountImpl), + "Deploy: IP Account Impl Address Mismatch" + ); contractKey = "DisputeModule"; _predeploy(contractKey); @@ -277,19 +264,28 @@ contract DeployHelper is Script, BroadcastManager, JsonDeploymentHandler, Storag ); disputeModule = DisputeModule( TestProxyHelper.deployUUPSProxy( - disputeModuleSalt, + _getSalt(type(DisputeModule).name), impl, abi.encodeCall(DisputeModule.initialize, address(protocolAccessManager)) ) ); - require(disputeModuleAddr == address(disputeModule), "Deploy: Dispute Module Address Mismatch"); + require( + _getDeployedAddress(type(DisputeModule).name) == address(disputeModule), + "Deploy: Dispute Module Address Mismatch" + ); require(_loadProxyImpl(address(disputeModule)) == impl, "DisputeModule Proxy Implementation Mismatch"); impl = address(0); _postdeploy(contractKey, address(disputeModule)); contractKey = "RoyaltyModule"; _predeploy(contractKey); - impl = address(new RoyaltyModule(licensingModuleAddr, address(disputeModule), address(licenseRegistry))); + impl = address( + new RoyaltyModule( + _getDeployedAddress(type(LicensingModule).name), + address(disputeModule), + address(licenseRegistry) + ) + ); royaltyModule = RoyaltyModule( TestProxyHelper.deployUUPSProxy( impl, @@ -305,20 +301,24 @@ contract DeployHelper is Script, BroadcastManager, JsonDeploymentHandler, Storag new LicensingModule( address(accessController), address(ipAccountRegistry), + address(moduleRegistry), address(royaltyModule), address(licenseRegistry), address(disputeModule), - licenseTokenAddr + _getDeployedAddress(type(LicenseToken).name) ) ); licensingModule = LicensingModule( TestProxyHelper.deployUUPSProxy( - licensingModuleSalt, + _getSalt(type(LicensingModule).name), impl, abi.encodeCall(LicensingModule.initialize, address(protocolAccessManager)) ) ); - require(licensingModuleAddr == address(licensingModule), "Deploy: Licensing Module Address Mismatch"); + require( + _getDeployedAddress(type(LicensingModule).name) == address(licensingModule), + "Deploy: Licensing Module Address Mismatch" + ); require(_loadProxyImpl(address(licensingModule)) == impl, "LicensingModule Proxy Implementation Mismatch"); impl = address(0); // Make sure we don't deploy wrong impl _postdeploy(contractKey, address(licensingModule)); @@ -328,7 +328,7 @@ contract DeployHelper is Script, BroadcastManager, JsonDeploymentHandler, Storag impl = address(new LicenseToken(address(licensingModule), address(disputeModule))); licenseToken = LicenseToken( TestProxyHelper.deployUUPSProxy( - licenseTokenSalt, + _getSalt(type(LicenseToken).name), impl, abi.encodeCall( LicenseToken.initialize, @@ -339,7 +339,10 @@ contract DeployHelper is Script, BroadcastManager, JsonDeploymentHandler, Storag ) ) ); - require(licenseTokenAddr == address(licenseToken), "Deploy: License Token Address Mismatch"); + require( + _getDeployedAddress(type(LicenseToken).name) == address(licenseToken), + "Deploy: License Token Address Mismatch" + ); require(_loadProxyImpl(address(licenseToken)) == impl, "LicenseToken Proxy Implementation Mismatch"); impl = address(0); _postdeploy(contractKey, address(licenseToken)); @@ -541,6 +544,16 @@ contract DeployHelper is Script, BroadcastManager, JsonDeploymentHandler, Storag protocolAccessManager.renounceRole(ProtocolAdmin.PROTOCOL_ADMIN_ROLE, deployer); } + /// @dev get the salt for the contract deployment with CREATE3 + function _getSalt(string memory name) private view returns (bytes32 salt) { + salt = keccak256(abi.encode(name, block.number)); + } + + /// @dev Get the deterministic deployed address of a contract with CREATE3 + function _getDeployedAddress(string memory name) private view returns (address) { + return Create3Deployer.getDeployed(_getSalt(name)); + } + /// @dev Load the implementation address from the proxy contract function _loadProxyImpl(address proxy) private view returns (address) { return address(uint160(uint256(vm.load(proxy, IMPLEMENTATION_SLOT)))); diff --git a/test/foundry/mocks/module/MockLicensingHook.sol b/test/foundry/mocks/module/MockLicensingHook.sol new file mode 100644 index 000000000..c4cbf140f --- /dev/null +++ b/test/foundry/mocks/module/MockLicensingHook.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.23; + +import { IERC165 } from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; + +import { BaseModule } from "../../../../contracts/modules/BaseModule.sol"; +import { ILicensingHook } from "contracts/interfaces/modules/licensing/ILicensingHook.sol"; + +contract MockLicensingHook is BaseModule, ILicensingHook { + string public constant override name = "MockLicensingHook"; + + function beforeMintLicenseTokens( + address caller, + address licensorIpId, + address licenseTemplate, + uint256 licenseTermsId, + uint256 amount, + address receiver, + bytes calldata hookData + ) external returns (uint256 totalMintingFee) { + address unqualifiedAddress = abi.decode(hookData, (address)); + if (caller == unqualifiedAddress) revert("MockLicensingHook: caller is invalid"); + if (receiver == unqualifiedAddress) revert("MockLicensingHook: receiver is invalid"); + return amount * 100; + } + + function beforeRegisterDerivative( + address caller, + address childIpId, + address parentIpId, + address licenseTemplate, + uint256 licenseTermsId, + bytes calldata hookData + ) external returns (uint256 mintingFee) { + address unqualifiedAddress = abi.decode(hookData, (address)); + if (caller == unqualifiedAddress) revert("MockLicensingHook: caller is invalid"); + return 100; + } + + function supportsInterface(bytes4 interfaceId) public view virtual override(BaseModule, IERC165) returns (bool) { + return interfaceId == type(ILicensingHook).interfaceId || super.supportsInterface(interfaceId); + } +} diff --git a/test/foundry/modules/licensing/LicensingModule.t.sol b/test/foundry/modules/licensing/LicensingModule.t.sol index 527a3acd4..b68792bde 100644 --- a/test/foundry/modules/licensing/LicensingModule.t.sol +++ b/test/foundry/modules/licensing/LicensingModule.t.sol @@ -11,7 +11,9 @@ import { PILFlavors } from "../../../../contracts/lib/PILFlavors.sol"; import { ILicensingModule } from "../../../../contracts/interfaces/modules/licensing/ILicensingModule.sol"; import { MockTokenGatedHook } from "../../mocks/MockTokenGatedHook.sol"; import { MockLicenseTemplate } from "../../mocks/module/MockLicenseTemplate.sol"; +import { MockLicensingHook } from "../../mocks/module/MockLicensingHook.sol"; import { PILTerms } from "../../../../contracts/interfaces/modules/licensing/IPILicenseTemplate.sol"; +import { Licensing } from "../../../../contracts/lib/Licensing.sol"; // test import { MockERC721 } from "../../mocks/token/MockERC721.sol"; @@ -73,7 +75,7 @@ contract LicensingModuleTest is BaseTest { assertEq(licenseRegistry.getAttachedLicenseTermsCount(ipId1), 1); assertEq(licenseRegistry.getDerivativeIpCount(ipId1), 0); assertEq(licenseRegistry.getParentIpCount(ipId1), 0); - assertFalse(licenseRegistry.getMintingLicenseConfig(ipId1, address(pilTemplate), termsId).isSet); + assertFalse(licenseRegistry.getLicensingConfig(ipId1, address(pilTemplate), termsId).isSet); assertEq(licenseRegistry.getExpireTime(ipId1), 0); assertFalse(licenseRegistry.isDerivativeIp(ipId1)); assertTrue(licenseRegistry.exists(address(pilTemplate), termsId)); @@ -94,7 +96,7 @@ contract LicensingModuleTest is BaseTest { assertEq(licenseRegistry.getAttachedLicenseTermsCount(ipId1), 1); assertEq(licenseRegistry.getDerivativeIpCount(ipId1), 0); assertEq(licenseRegistry.getParentIpCount(ipId1), 0); - assertFalse(licenseRegistry.getMintingLicenseConfig(ipId1, address(pilTemplate), termsId).isSet); + assertFalse(licenseRegistry.getLicensingConfig(ipId1, address(pilTemplate), termsId).isSet); assertEq(licenseRegistry.getExpireTime(ipId1), 0); assertFalse(licenseRegistry.isDerivativeIp(ipId1)); assertTrue(licenseRegistry.exists(address(pilTemplate), termsId)); @@ -110,7 +112,7 @@ contract LicensingModuleTest is BaseTest { assertEq(licenseRegistry.getAttachedLicenseTermsCount(ipId2), 1); assertEq(licenseRegistry.getDerivativeIpCount(ipId2), 0); assertEq(licenseRegistry.getParentIpCount(ipId2), 0); - assertFalse(licenseRegistry.getMintingLicenseConfig(ipId2, address(pilTemplate), termsId).isSet); + assertFalse(licenseRegistry.getLicensingConfig(ipId2, address(pilTemplate), termsId).isSet); assertEq(licenseRegistry.getExpireTime(ipId2), 0); assertFalse(licenseRegistry.isDerivativeIp(ipId2)); assertTrue(licenseRegistry.exists(address(pilTemplate), termsId)); @@ -129,7 +131,7 @@ contract LicensingModuleTest is BaseTest { assertEq(licenseRegistry.getAttachedLicenseTermsCount(ipId1), 1); assertEq(licenseRegistry.getDerivativeIpCount(ipId1), 0); assertEq(licenseRegistry.getParentIpCount(ipId1), 0); - assertFalse(licenseRegistry.getMintingLicenseConfig(ipId1, address(pilTemplate), termsId1).isSet); + assertFalse(licenseRegistry.getLicensingConfig(ipId1, address(pilTemplate), termsId1).isSet); assertEq(licenseRegistry.getExpireTime(ipId1), 0); assertFalse(licenseRegistry.isDerivativeIp(ipId1)); assertTrue(licenseRegistry.exists(address(pilTemplate), termsId1)); @@ -146,7 +148,7 @@ contract LicensingModuleTest is BaseTest { assertEq(licenseRegistry.getAttachedLicenseTermsCount(ipId1), 2); assertEq(licenseRegistry.getDerivativeIpCount(ipId1), 0); assertEq(licenseRegistry.getParentIpCount(ipId1), 0); - assertFalse(licenseRegistry.getMintingLicenseConfig(ipId1, address(pilTemplate), termsId2).isSet); + assertFalse(licenseRegistry.getLicensingConfig(ipId1, address(pilTemplate), termsId2).isSet); assertEq(licenseRegistry.getExpireTime(ipId1), 0); assertFalse(licenseRegistry.isDerivativeIp(ipId1)); assertTrue(licenseRegistry.exists(address(pilTemplate), termsId2)); @@ -1173,6 +1175,261 @@ contract LicensingModuleTest is BaseTest { licensingModule.registerDerivative(ipId3, parentIpIds, licenseTermsIds, address(pilTemplate), ""); } + function test_LicensingModule_setLicensingConfig() public { + uint256 socialRemixTermsId = pilTemplate.registerLicenseTerms(PILFlavors.nonCommercialSocialRemixing()); + MockLicensingHook licensingHook = new MockLicensingHook(); + vm.prank(admin); + moduleRegistry.registerModule("MockLicensingHook", address(licensingHook)); + Licensing.LicensingConfig memory licensingConfig = Licensing.LicensingConfig({ + isSet: true, + mintingFee: 100, + licensingHook: address(licensingHook), + hookData: abi.encode(address(0x123)) + }); + vm.prank(ipOwner1); + licensingModule.setLicensingConfig(ipId1, address(pilTemplate), socialRemixTermsId, licensingConfig); + assertEq(licenseRegistry.getLicensingConfig(ipId1, address(pilTemplate), socialRemixTermsId).isSet, true); + assertEq(licenseRegistry.getLicensingConfig(ipId1, address(pilTemplate), socialRemixTermsId).mintingFee, 100); + assertEq( + licenseRegistry.getLicensingConfig(ipId1, address(pilTemplate), socialRemixTermsId).licensingHook, + address(licensingHook) + ); + assertEq( + licenseRegistry.getLicensingConfig(ipId1, address(pilTemplate), socialRemixTermsId).hookData, + abi.encode(address(0x123)) + ); + + vm.prank(ipOwner2); + licensingModule.setLicensingConfig(ipId2, address(0), 0, licensingConfig); + assertEq(licenseRegistry.getLicensingConfig(ipId1, address(pilTemplate), socialRemixTermsId).isSet, true); + assertEq(licenseRegistry.getLicensingConfig(ipId1, address(pilTemplate), socialRemixTermsId).mintingFee, 100); + assertEq( + licenseRegistry.getLicensingConfig(ipId1, address(pilTemplate), socialRemixTermsId).licensingHook, + address(licensingHook) + ); + assertEq( + licenseRegistry.getLicensingConfig(ipId1, address(pilTemplate), socialRemixTermsId).hookData, + abi.encode(address(0x123)) + ); + } + + function test_LicensingModule_setLicensingConfig_revert_invalidTermsId() public { + uint256 socialRemixTermsId = pilTemplate.registerLicenseTerms(PILFlavors.nonCommercialSocialRemixing()); + MockLicensingHook licensingHook = new MockLicensingHook(); + vm.prank(admin); + moduleRegistry.registerModule("MockLicensingHook", address(licensingHook)); + Licensing.LicensingConfig memory licensingConfig = Licensing.LicensingConfig({ + isSet: true, + mintingFee: 100, + licensingHook: address(licensingHook), + hookData: abi.encode(address(0x123)) + }); + vm.expectRevert( + abi.encodeWithSelector(Errors.LicensingModule__InvalidLicenseTermsId.selector, address(pilTemplate), 0) + ); + vm.prank(ipOwner1); + licensingModule.setLicensingConfig(ipId1, address(pilTemplate), 0, licensingConfig); + + vm.expectRevert( + abi.encodeWithSelector( + Errors.LicensingModule__InvalidLicenseTermsId.selector, + address(0), + socialRemixTermsId + ) + ); + vm.prank(ipOwner1); + licensingModule.setLicensingConfig(ipId1, address(0), socialRemixTermsId, licensingConfig); + } + + function test_LicensingModule_setLicensingConfig_revert_invalidLicensingHook() public { + uint256 socialRemixTermsId = pilTemplate.registerLicenseTerms(PILFlavors.nonCommercialSocialRemixing()); + // unregistered the licensing hook + MockLicensingHook licensingHook = new MockLicensingHook(); + Licensing.LicensingConfig memory licensingConfig = Licensing.LicensingConfig({ + isSet: true, + mintingFee: 100, + licensingHook: address(licensingHook), + hookData: abi.encode(address(0x123)) + }); + vm.expectRevert( + abi.encodeWithSelector(Errors.LicensingModule__InvalidLicensingHook.selector, address(licensingHook)) + ); + vm.prank(ipOwner1); + licensingModule.setLicensingConfig(ipId1, address(pilTemplate), 0, licensingConfig); + + // unsupport licensing hook interface + MockTokenGatedHook tokenGatedHook = new MockTokenGatedHook(); + vm.prank(admin); + moduleRegistry.registerModule("MockTokenGatedHook", address(tokenGatedHook)); + + licensingConfig = Licensing.LicensingConfig({ + isSet: true, + mintingFee: 100, + licensingHook: address(tokenGatedHook), + hookData: abi.encode(address(0x123)) + }); + vm.expectRevert( + abi.encodeWithSelector(Errors.LicensingModule__InvalidLicensingHook.selector, address(tokenGatedHook)) + ); + vm.prank(ipOwner1); + licensingModule.setLicensingConfig(ipId1, address(pilTemplate), 0, licensingConfig); + } + + function test_LicensingModule_mintLicenseTokens_revert_licensingHookRevert() public { + uint256 termsId = pilTemplate.registerLicenseTerms(PILFlavors.defaultValuesLicenseTerms()); + MockLicensingHook licensingHook = new MockLicensingHook(); + vm.prank(admin); + moduleRegistry.registerModule("MockLicensingHook", address(licensingHook)); + Licensing.LicensingConfig memory licensingConfig = Licensing.LicensingConfig({ + isSet: true, + mintingFee: 100, + licensingHook: address(licensingHook), + hookData: abi.encode(address(0x123)) + }); + vm.prank(ipOwner1); + licensingModule.setLicensingConfig(ipId1, address(pilTemplate), termsId, licensingConfig); + vm.prank(ipOwner1); + licensingModule.attachLicenseTerms(ipId1, address(pilTemplate), termsId); + + address receiver = address(0x123); + vm.expectRevert("MockLicensingHook: receiver is invalid"); + licensingModule.mintLicenseTokens({ + licensorIpId: ipId1, + licenseTemplate: address(pilTemplate), + licenseTermsId: termsId, + amount: 1, + receiver: receiver, + royaltyContext: "" + }); + } + + function test_LicensingModule_mintLicenseTokens_withMintingFeeFromHook() public { + uint256 termsId = pilTemplate.registerLicenseTerms( + PILFlavors.commercialRemix({ + mintingFee: 999, + commercialRevShare: 10, + currencyToken: address(erc20), + royaltyPolicy: address(royaltyPolicyLAP) + }) + ); + + MockLicensingHook licensingHook = new MockLicensingHook(); + vm.prank(admin); + moduleRegistry.registerModule("MockLicensingHook", address(licensingHook)); + Licensing.LicensingConfig memory licensingConfig = Licensing.LicensingConfig({ + isSet: true, + mintingFee: 999999, + licensingHook: address(licensingHook), + hookData: abi.encode(address(0x123)) + }); + vm.prank(ipOwner1); + licensingModule.setLicensingConfig(ipId1, address(pilTemplate), termsId, licensingConfig); + + vm.prank(ipOwner1); + licensingModule.attachLicenseTerms(ipId1, address(pilTemplate), termsId); + + address minter = vm.addr(777); + + vm.startPrank(minter); + + erc20.mint(minter, 1000); + erc20.approve(address(royaltyPolicyLAP), 100); + + address receiver = address(0x111); + vm.expectEmit(); + emit ILicensingModule.LicenseTokensMinted(minter, ipId1, address(pilTemplate), termsId, 1, receiver, 0); + + uint256 lcTokenId = licensingModule.mintLicenseTokens({ + licensorIpId: ipId1, + licenseTemplate: address(pilTemplate), + licenseTermsId: termsId, + amount: 1, + receiver: receiver, + royaltyContext: "" + }); + vm.stopPrank(); + + assertEq(erc20.balanceOf(minter), 900); + assertEq(licenseToken.ownerOf(lcTokenId), receiver); + } + + function test_LicensingModule_registerDerivative_withMintingFeeFromHook() public { + uint256 termsId = pilTemplate.registerLicenseTerms( + PILFlavors.commercialRemix({ + mintingFee: 999, + commercialRevShare: 10, + currencyToken: address(erc20), + royaltyPolicy: address(royaltyPolicyLAP) + }) + ); + + MockLicensingHook licensingHook = new MockLicensingHook(); + vm.prank(admin); + moduleRegistry.registerModule("MockLicensingHook", address(licensingHook)); + Licensing.LicensingConfig memory licensingConfig = Licensing.LicensingConfig({ + isSet: true, + mintingFee: 999999, + licensingHook: address(licensingHook), + hookData: abi.encode(address(0x123)) + }); + vm.prank(ipOwner1); + licensingModule.setLicensingConfig(ipId1, address(pilTemplate), termsId, licensingConfig); + + vm.prank(ipOwner1); + licensingModule.attachLicenseTerms(ipId1, address(pilTemplate), termsId); + + vm.startPrank(ipOwner2); + erc20.mint(ipOwner2, 1000); + erc20.approve(address(royaltyPolicyLAP), 100); + + address[] memory parentIpIds = new address[](1); + uint256[] memory licenseTermsIds = new uint256[](1); + parentIpIds[0] = ipId1; + licenseTermsIds[0] = termsId; + + vm.expectEmit(); + emit ILicensingModule.DerivativeRegistered( + ipOwner2, + ipId2, + new uint256[](0), + parentIpIds, + licenseTermsIds, + address(pilTemplate) + ); + licensingModule.registerDerivative(ipId2, parentIpIds, licenseTermsIds, address(pilTemplate), ""); + vm.stopPrank(); + + assertEq(erc20.balanceOf(ipOwner2), 900); + assertEq(licenseRegistry.hasIpAttachedLicenseTerms(ipId2, address(pilTemplate), termsId), true); + assertEq(licenseRegistry.isDerivativeIp(ipId2), true); + assertEq(licenseRegistry.getParentIp(ipId2, 0), ipId1); + } + + function test_LicensingModule_registerDerivative_revert_licensingHookRevert() public { + uint256 termsId = pilTemplate.registerLicenseTerms(PILFlavors.nonCommercialSocialRemixing()); + MockLicensingHook licensingHook = new MockLicensingHook(); + vm.prank(admin); + moduleRegistry.registerModule("MockLicensingHook", address(licensingHook)); + Licensing.LicensingConfig memory licensingConfig = Licensing.LicensingConfig({ + isSet: true, + mintingFee: 100, + licensingHook: address(licensingHook), + hookData: abi.encode(address(ipOwner2)) + }); + vm.prank(ipOwner1); + licensingModule.setLicensingConfig(ipId1, address(pilTemplate), termsId, licensingConfig); + vm.prank(ipOwner1); + licensingModule.attachLicenseTerms(ipId1, address(pilTemplate), termsId); + + address[] memory parentIpIds = new address[](1); + uint256[] memory licenseTermsIds = new uint256[](1); + parentIpIds[0] = ipId1; + licenseTermsIds[0] = termsId; + vm.prank(ipOwner2); + vm.expectRevert("MockLicensingHook: caller is invalid"); + licensingModule.registerDerivative(ipId2, parentIpIds, licenseTermsIds, address(pilTemplate), ""); + } + function onERC721Received(address, address, uint256, bytes memory) public pure returns (bytes4) { return this.onERC721Received.selector; } diff --git a/test/foundry/registries/LicenseRegistry.t.sol b/test/foundry/registries/LicenseRegistry.t.sol index e36eb45e6..15e06add8 100644 --- a/test/foundry/registries/LicenseRegistry.t.sol +++ b/test/foundry/registries/LicenseRegistry.t.sol @@ -90,79 +90,69 @@ contract LicenseRegistryTest is BaseTest { ); } - function test_LicenseRegistry_setMintingLicenseConfigForLicense() public { + function test_LicenseRegistry_setLicensingConfigForLicense() public { uint256 defaultTermsId = pilTemplate.registerLicenseTerms(PILFlavors.defaultValuesLicenseTerms()); - Licensing.MintingLicenseConfig memory mintingLicenseConfig = Licensing.MintingLicenseConfig({ + Licensing.LicensingConfig memory mintingLicenseConfig = Licensing.LicensingConfig({ isSet: true, mintingFee: 100, - mintingFeeModule: address(0), - receiverCheckModule: address(0), - receiverCheckData: "" + licensingHook: address(0), + hookData: "" }); vm.prank(address(licensingModule)); - licenseRegistry.setMintingLicenseConfigForLicense( + licenseRegistry.setLicensingConfigForLicense( ipAcct[1], address(pilTemplate), defaultTermsId, mintingLicenseConfig ); - Licensing.MintingLicenseConfig memory returnedMintingLicenseConfig = licenseRegistry.getMintingLicenseConfig( + Licensing.LicensingConfig memory returnedLicensingConfig = licenseRegistry.getLicensingConfig( ipAcct[1], address(pilTemplate), defaultTermsId ); - assertEq(returnedMintingLicenseConfig.mintingFee, 100); - assertEq(returnedMintingLicenseConfig.mintingFeeModule, address(0)); - assertEq(returnedMintingLicenseConfig.receiverCheckModule, address(0)); - assertEq(returnedMintingLicenseConfig.receiverCheckData, ""); + assertEq(returnedLicensingConfig.mintingFee, 100); + assertEq(returnedLicensingConfig.licensingHook, address(0)); + assertEq(returnedLicensingConfig.hookData, ""); } - function test_LicenseRegistry_setMintingLicenseConfigForLicense_revert_UnregisteredTemplate() public { + function test_LicenseRegistry_setLicensingConfigForLicense_revert_UnregisteredTemplate() public { MockLicenseTemplate pilTemplate2 = new MockLicenseTemplate(); uint256 termsId = pilTemplate2.registerLicenseTerms(); - Licensing.MintingLicenseConfig memory mintingLicenseConfig = Licensing.MintingLicenseConfig({ + Licensing.LicensingConfig memory mintingLicenseConfig = Licensing.LicensingConfig({ isSet: true, mintingFee: 100, - mintingFeeModule: address(0), - receiverCheckModule: address(0), - receiverCheckData: "" + licensingHook: address(0), + hookData: "" }); vm.expectRevert( abi.encodeWithSelector(Errors.LicenseRegistry__UnregisteredLicenseTemplate.selector, address(pilTemplate2)) ); vm.prank(address(licensingModule)); - licenseRegistry.setMintingLicenseConfigForLicense( - ipAcct[1], - address(pilTemplate2), - termsId, - mintingLicenseConfig - ); + licenseRegistry.setLicensingConfigForLicense(ipAcct[1], address(pilTemplate2), termsId, mintingLicenseConfig); } - function test_LicenseRegistry_setMintingLicenseConfigForIp() public { + function test_LicenseRegistry_setLicensingConfigForIp() public { uint256 defaultTermsId = pilTemplate.registerLicenseTerms(PILFlavors.defaultValuesLicenseTerms()); - Licensing.MintingLicenseConfig memory mintingLicenseConfig = Licensing.MintingLicenseConfig({ + Licensing.LicensingConfig memory mintingLicenseConfig = Licensing.LicensingConfig({ isSet: true, mintingFee: 100, - mintingFeeModule: address(0), - receiverCheckModule: address(0), - receiverCheckData: "" + licensingHook: address(0), + hookData: "" }); vm.prank(address(licensingModule)); - licenseRegistry.setMintingLicenseConfigForIp(ipAcct[1], mintingLicenseConfig); + licenseRegistry.setLicensingConfigForIp(ipAcct[1], mintingLicenseConfig); - Licensing.MintingLicenseConfig memory returnedMintingLicenseConfig = licenseRegistry.getMintingLicenseConfig( + Licensing.LicensingConfig memory returnedLicensingConfig = licenseRegistry.getLicensingConfig( ipAcct[1], address(pilTemplate), defaultTermsId ); - assertEq(returnedMintingLicenseConfig.mintingFee, 100); - assertEq(returnedMintingLicenseConfig.mintingFeeModule, address(0)); - assertEq(returnedMintingLicenseConfig.receiverCheckModule, address(0)); - assertEq(returnedMintingLicenseConfig.receiverCheckData, ""); + assertEq(returnedLicensingConfig.mintingFee, 100); + assertEq(returnedLicensingConfig.licensingHook, address(0)); + assertEq(returnedLicensingConfig.hookData, ""); } // test attachLicenseTermsToIp