From 1d28bf8a595455d64d6c5a242074af7cdd246748 Mon Sep 17 00:00:00 2001 From: Kingter <83567446+kingster-will@users.noreply.github.com> Date: Tue, 10 Sep 2024 20:10:32 -0700 Subject: [PATCH] Add Function to Predict License Minting Fee in Licensing Module (#200) --- .../modules/licensing/ILicensingHook.sol | 23 +++ .../modules/licensing/ILicensingModule.sol | 19 ++ .../modules/licensing/LicensingModule.sol | 57 ++++++ .../mocks/module/MockLicensingHook.sol | 15 ++ .../modules/licensing/LicensingModule.t.sol | 165 ++++++++++++++++++ 5 files changed, 279 insertions(+) diff --git a/contracts/interfaces/modules/licensing/ILicensingHook.sol b/contracts/interfaces/modules/licensing/ILicensingHook.sol index 240cc077..d0c3981b 100644 --- a/contracts/interfaces/modules/licensing/ILicensingHook.sol +++ b/contracts/interfaces/modules/licensing/ILicensingHook.sol @@ -51,4 +51,27 @@ interface ILicensingHook is IModule { uint256 licenseTermsId, bytes calldata hookData ) external returns (uint256 mintingFee); + + /// @notice This function is called when the LicensingModule calculates/predict the minting fee for license tokens. + /// @dev The hook should guarantee the minting fee calculation is correct and return the minting fee which is + /// the exact same amount with returned by beforeMintLicenseTokens(). + /// The hook should revert if the minting fee calculation 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 calculateMintingFee( + address caller, + address licensorIpId, + address licenseTemplate, + uint256 licenseTermsId, + uint256 amount, + address receiver, + bytes calldata hookData + ) external view returns (uint256 totalMintingFee); } diff --git a/contracts/interfaces/modules/licensing/ILicensingModule.sol b/contracts/interfaces/modules/licensing/ILicensingModule.sol index 20dacb6b..29411dbf 100644 --- a/contracts/interfaces/modules/licensing/ILicensingModule.sol +++ b/contracts/interfaces/modules/licensing/ILicensingModule.sol @@ -138,4 +138,23 @@ interface ILicensingModule is IModule { uint256 licenseTermsId, Licensing.LicensingConfig memory licensingConfig ) external; + + /// @notice pre-compute the minting license fee for the given IP and license terms. + /// the function can be used to calculate the minting license fee before minting license tokens. + /// @param licensorIpId The IP ID of the licensor. + /// @param licenseTemplate The address of the license template. + /// @param licenseTermsId The ID of the license terms. + /// @param amount The amount of license tokens to mint. + /// @param receiver The address of the receiver. + /// @param royaltyContext The context of the royalty. + /// @return currencyToken The address of the ERC20 token used for minting license fee. + /// @return tokenAmount The amount of the currency token to be paid for minting license tokens. + function predictMintingLicenseFee( + address licensorIpId, + address licenseTemplate, + uint256 licenseTermsId, + uint256 amount, + address receiver, + bytes calldata royaltyContext + ) external view returns (address currencyToken, uint256 tokenAmount); } diff --git a/contracts/modules/licensing/LicensingModule.sol b/contracts/modules/licensing/LicensingModule.sol index 58c7b9b8..44b87951 100644 --- a/contracts/modules/licensing/LicensingModule.sol +++ b/contracts/modules/licensing/LicensingModule.sol @@ -382,6 +382,63 @@ contract LicensingModule is } } + /// @notice pre-compute the minting license fee for the given IP and license terms. + /// the function can be used to calculate the minting license fee before minting license tokens. + /// @param licensorIpId The IP ID of the licensor. + /// @param licenseTemplate The address of the license template. + /// @param licenseTermsId The ID of the license terms. + /// @param amount The amount of license tokens to mint. + /// @param receiver The address of the receiver. + /// @param royaltyContext The context of the royalty. + /// @return currencyToken The address of the ERC20 token used for minting license fee. + /// @return tokenAmount The amount of the currency token to be paid for minting license tokens. + function predictMintingLicenseFee( + address licensorIpId, + address licenseTemplate, + uint256 licenseTermsId, + uint256 amount, + address receiver, + bytes calldata royaltyContext + ) external view returns (address currencyToken, uint256 tokenAmount) { + tokenAmount = 0; + if (amount == 0) { + revert Errors.LicensingModule__MintAmountZero(); + } + if (receiver == address(0)) { + revert Errors.LicensingModule__ReceiverZeroAddress(); + } + if (!IP_ACCOUNT_REGISTRY.isIpAccount(licensorIpId)) { + revert Errors.LicensingModule__LicensorIpNotRegistered(); + } + Licensing.LicensingConfig memory lsc = LICENSE_REGISTRY.verifyMintLicenseToken( + licensorIpId, + licenseTemplate, + licenseTermsId, + _hasPermission(licensorIpId) + ); + uint256 mintingFeeByHook = 0; + if (lsc.isSet && lsc.licensingHook != address(0)) { + mintingFeeByHook = ILicensingHook(lsc.licensingHook).calculateMintingFee( + msg.sender, + licensorIpId, + licenseTemplate, + licenseTermsId, + amount, + receiver, + lsc.hookData + ); + } + + ILicenseTemplate lct = ILicenseTemplate(licenseTemplate); + uint256 mintingFeeByLicense = 0; + address royaltyPolicy = address(0); + (royaltyPolicy, , mintingFeeByLicense, currencyToken) = lct.getRoyaltyPolicy(licenseTermsId); + + if (royaltyPolicy != address(0)) { + tokenAmount = _getTotalMintingFee(lsc, mintingFeeByHook, mintingFeeByLicense, amount); + } + } + /// @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 diff --git a/test/foundry/mocks/module/MockLicensingHook.sol b/test/foundry/mocks/module/MockLicensingHook.sol index 2e30588c..d03bf2ac 100644 --- a/test/foundry/mocks/module/MockLicensingHook.sol +++ b/test/foundry/mocks/module/MockLicensingHook.sol @@ -37,6 +37,21 @@ contract MockLicensingHook is BaseModule, ILicensingHook { return 100; } + function calculateMintingFee( + address caller, + address licensorIpId, + address licenseTemplate, + uint256 licenseTermsId, + uint256 amount, + address receiver, + bytes calldata hookData + ) external view 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 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 7b7421d2..99f6bbd4 100644 --- a/test/foundry/modules/licensing/LicensingModule.t.sol +++ b/test/foundry/modules/licensing/LicensingModule.t.sol @@ -1671,6 +1671,171 @@ contract LicensingModuleTest is BaseTest { }); } + function test_LicensingModule_calculatingMintingFee_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 receiver = address(0x111); + (address token, uint256 mintingFee) = licensingModule.predictMintingLicenseFee( + ipId1, + address(pilTemplate), + termsId, + 5, + receiver, + "" + ); + assertEq(mintingFee, 100 * 5); + assertEq(token, address(erc20)); + + address minter = vm.addr(777); + vm.startPrank(minter); + + erc20.mint(minter, 1000); + erc20.approve(address(royaltyModule), 100 * 5); + + vm.expectEmit(); + emit ILicensingModule.LicenseTokensMinted(minter, ipId1, address(pilTemplate), termsId, 5, receiver, 0); + + uint256 lcTokenId = licensingModule.mintLicenseTokens({ + licensorIpId: ipId1, + licenseTemplate: address(pilTemplate), + licenseTermsId: termsId, + amount: 5, + receiver: receiver, + royaltyContext: "" + }); + vm.stopPrank(); + + assertEq(erc20.balanceOf(minter), 500); + assertEq(licenseToken.ownerOf(lcTokenId), receiver); + } + + function test_LicensingModule_calculatingMintingFee_withMintingFeeFromLicenseConfig() public { + uint256 termsId = pilTemplate.registerLicenseTerms( + PILFlavors.commercialRemix({ + mintingFee: 999, + commercialRevShare: 10, + currencyToken: address(erc20), + royaltyPolicy: address(royaltyPolicyLAP) + }) + ); + + Licensing.LicensingConfig memory licensingConfig = Licensing.LicensingConfig({ + isSet: true, + mintingFee: 1000, + licensingHook: address(0), + 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(0x111); + (address token, uint256 mintingFee) = licensingModule.predictMintingLicenseFee( + ipId1, + address(pilTemplate), + termsId, + 5, + receiver, + "" + ); + assertEq(mintingFee, 1000 * 5); + assertEq(token, address(erc20)); + + address minter = vm.addr(777); + vm.startPrank(minter); + + erc20.mint(minter, 5000); + erc20.approve(address(royaltyModule), 1000 * 5); + + vm.expectEmit(); + emit ILicensingModule.LicenseTokensMinted(minter, ipId1, address(pilTemplate), termsId, 5, receiver, 0); + + uint256 lcTokenId = licensingModule.mintLicenseTokens({ + licensorIpId: ipId1, + licenseTemplate: address(pilTemplate), + licenseTermsId: termsId, + amount: 5, + receiver: receiver, + royaltyContext: "" + }); + vm.stopPrank(); + + assertEq(erc20.balanceOf(minter), 0); + assertEq(licenseToken.ownerOf(lcTokenId), receiver); + } + + function test_LicensingModule_calculatingMintingFee_withMintingFeeFromLicense() public { + uint256 termsId = pilTemplate.registerLicenseTerms( + PILFlavors.commercialRemix({ + mintingFee: 10000, + commercialRevShare: 10, + currencyToken: address(erc20), + royaltyPolicy: address(royaltyPolicyLAP) + }) + ); + + vm.prank(ipOwner1); + licensingModule.attachLicenseTerms(ipId1, address(pilTemplate), termsId); + + address receiver = address(0x111); + (address token, uint256 mintingFee) = licensingModule.predictMintingLicenseFee( + ipId1, + address(pilTemplate), + termsId, + 5, + receiver, + "" + ); + assertEq(mintingFee, 10000 * 5); + assertEq(token, address(erc20)); + + address minter = vm.addr(777); + vm.startPrank(minter); + + erc20.mint(minter, 50000); + erc20.approve(address(royaltyModule), 10000 * 5); + + vm.expectEmit(); + emit ILicensingModule.LicenseTokensMinted(minter, ipId1, address(pilTemplate), termsId, 5, receiver, 0); + + uint256 lcTokenId = licensingModule.mintLicenseTokens({ + licensorIpId: ipId1, + licenseTemplate: address(pilTemplate), + licenseTermsId: termsId, + amount: 5, + receiver: receiver, + royaltyContext: "" + }); + vm.stopPrank(); + + assertEq(erc20.balanceOf(minter), 0); + assertEq(licenseToken.ownerOf(lcTokenId), receiver); + } + function test_LicensingModule_mintLicenseTokens_withMintingFeeFromHook() public { uint256 termsId = pilTemplate.registerLicenseTerms( PILFlavors.commercialRemix({