diff --git a/contracts/LicenseToken.sol b/contracts/LicenseToken.sol index 822425cc..0913218d 100644 --- a/contracts/LicenseToken.sol +++ b/contracts/LicenseToken.sol @@ -10,6 +10,7 @@ import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils import { AccessManagedUpgradeable } from "@openzeppelin/contracts-upgradeable/access/manager/AccessManagedUpgradeable.sol"; import { ILicenseToken } from "./interfaces/ILicenseToken.sol"; +import { ILicenseRegistry } from "./interfaces/registries/ILicenseRegistry.sol"; import { ILicensingModule } from "./interfaces/modules/licensing/ILicensingModule.sol"; import { IDisputeModule } from "./interfaces/modules/dispute/IDisputeModule.sol"; import { Errors } from "./lib/Errors.sol"; @@ -19,6 +20,8 @@ import { ILicenseTemplate } from "./interfaces/modules/licensing/ILicenseTemplat contract LicenseToken is ILicenseToken, ERC721EnumerableUpgradeable, AccessManagedUpgradeable, UUPSUpgradeable { using Strings for *; + /// @custom:oz-upgrades-unsafe-allow state-variable-immutable + ILicenseRegistry public immutable LICENSE_REGISTRY; /// @custom:oz-upgrades-unsafe-allow state-variable-immutable ILicensingModule public immutable LICENSING_MODULE; /// @custom:oz-upgrades-unsafe-allow state-variable-immutable @@ -48,9 +51,10 @@ contract LicenseToken is ILicenseToken, ERC721EnumerableUpgradeable, AccessManag } /// @custom:oz-upgrades-unsafe-allow constructor - constructor(address licensingModule, address disputeModule) { + constructor(address licensingModule, address disputeModule, address licenseRegistry) { LICENSING_MODULE = ILicensingModule(licensingModule); DISPUTE_MODULE = IDisputeModule(disputeModule); + LICENSE_REGISTRY = ILicenseRegistry(licenseRegistry); _disableInitializers(); } @@ -94,7 +98,8 @@ contract LicenseToken is ILicenseToken, ERC721EnumerableUpgradeable, AccessManag licensorIpId: licensorIpId, licenseTemplate: licenseTemplate, licenseTermsId: licenseTermsId, - transferable: ILicenseTemplate(licenseTemplate).isLicenseTransferable(licenseTermsId) + transferable: ILicenseTemplate(licenseTemplate).isLicenseTransferable(licenseTermsId), + commercialRevShare: LICENSE_REGISTRY.getRoyaltyPercent(licensorIpId, licenseTemplate, licenseTermsId) }); LicenseTokenStorage storage $ = _getLicenseTokenStorage(); @@ -128,6 +133,7 @@ contract LicenseToken is ILicenseToken, ERC721EnumerableUpgradeable, AccessManag /// @return licenseTemplate The address of the License Template associated with the License Tokens. /// @return licensorIpIds An array of licensor IPs associated with each License Token. /// @return licenseTermsIds An array of License Terms associated with each validated License Token. + /// @return commercialRevShares An array of commercial revenue share percentages associated with each License Token. function validateLicenseTokensForDerivative( address caller, address childIpId, @@ -135,12 +141,13 @@ contract LicenseToken is ILicenseToken, ERC721EnumerableUpgradeable, AccessManag ) external view - returns (address licenseTemplate, address[] memory licensorIpIds, uint256[] memory licenseTermsIds) + returns (address licenseTemplate, address[] memory licensorIpIds, uint256[] memory licenseTermsIds, uint32[] memory commercialRevShares) { LicenseTokenStorage storage $ = _getLicenseTokenStorage(); licenseTemplate = $.licenseTokenMetadatas[tokenIds[0]].licenseTemplate; licensorIpIds = new address[](tokenIds.length); licenseTermsIds = new uint256[](tokenIds.length); + commercialRevShares = new uint32[](tokenIds.length); for (uint256 i = 0; i < tokenIds.length; i++) { LicenseTokenMetadata memory ltm = $.licenseTokenMetadatas[tokenIds[i]]; @@ -160,6 +167,7 @@ contract LicenseToken is ILicenseToken, ERC721EnumerableUpgradeable, AccessManag licensorIpIds[i] = ltm.licensorIpId; licenseTermsIds[i] = ltm.licenseTermsId; + commercialRevShares[i] = ltm.commercialRevShare; } } diff --git a/contracts/interfaces/ILicenseToken.sol b/contracts/interfaces/ILicenseToken.sol index 031ae618..cbac434e 100644 --- a/contracts/interfaces/ILicenseToken.sol +++ b/contracts/interfaces/ILicenseToken.sol @@ -24,6 +24,7 @@ interface ILicenseToken is IERC721Metadata, IERC721Enumerable { address licenseTemplate; uint256 licenseTermsId; bool transferable; + uint32 commercialRevShare; } /// @notice Emitted when a License Token is minted. @@ -98,9 +99,18 @@ interface ILicenseToken is IERC721Metadata, IERC721Enumerable { /// @return licenseTemplate The address of the License Template associated with the License Tokens. /// @return licensorIpIds An array of licensor IPs associated with each License Token. /// @return licenseTermsIds An array of License Terms associated with each validated License Token. + /// @return commercialRevShares An array of commercial revenue share percentages associated with each License Token. function validateLicenseTokensForDerivative( address caller, address childIpId, uint256[] calldata tokenIds - ) external view returns (address licenseTemplate, address[] memory licensorIpIds, uint256[] memory licenseTermsIds); + ) + external + view + returns ( + address licenseTemplate, + address[] memory licensorIpIds, + uint256[] memory licenseTermsIds, + uint32[] memory commercialRevShares + ); } diff --git a/contracts/interfaces/registries/ILicenseRegistry.sol b/contracts/interfaces/registries/ILicenseRegistry.sol index 59ef53b1..926a7a15 100644 --- a/contracts/interfaces/registries/ILicenseRegistry.sol +++ b/contracts/interfaces/registries/ILicenseRegistry.sol @@ -217,4 +217,17 @@ interface ILicenseRegistry { address childIpId, address parentIpId ) external view returns (address licenseTemplate, uint256 licenseTermsId); + + /// @notice Return the Royalty percentage of the license terms of the IP. + /// There are 2 places to get the royalty percentage: license terms, LicenseConfig + /// The order of priority is LicenseConfig > license terms + /// @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. + /// @return royaltyPercent The Royalty percentage 100% is 100_000_000. + function getRoyaltyPercent( + address ipId, + address licenseTemplate, + uint256 licenseTermsId + ) external view returns (uint32 royaltyPercent); } diff --git a/contracts/modules/licensing/LicensingModule.sol b/contracts/modules/licensing/LicensingModule.sol index b556a164..44c080d9 100644 --- a/contracts/modules/licensing/LicensingModule.sol +++ b/contracts/modules/licensing/LicensingModule.sol @@ -335,7 +335,7 @@ contract LicensingModule is // Confirm that the license token has not been revoked. // Validate that the owner of the derivative IP is also the owner of the license tokens. address childIpOwner = IIPAccount(payable(childIpId)).owner(); - (address licenseTemplate, address[] memory parentIpIds, uint256[] memory licenseTermsIds) = LICENSE_NFT + (address licenseTemplate, address[] memory parentIpIds, uint256[] memory licenseTermsIds, uint32[] memory royaltyPercents) = LICENSE_NFT .validateLicenseTokensForDerivative(msg.sender, childIpId, licenseTokenIds); _verifyIpNotDisputed(childIpId); @@ -359,7 +359,7 @@ contract LicensingModule is // all license terms. LICENSE_REGISTRY.registerDerivativeIp(childIpId, parentIpIds, licenseTemplate, licenseTermsIds, true); - _setupRoyalty(childIpId, parentIpIds, licenseTermsIds, licenseTemplate, royaltyContext, maxRts); + _setupRoyalty(childIpId, parentIpIds, royaltyPercents, licenseTermsIds, licenseTemplate, royaltyContext, maxRts); // burn license tokens LICENSE_NFT.burnLicenseTokens(childIpOwner, licenseTokenIds); @@ -516,6 +516,7 @@ contract LicensingModule is function _setupRoyalty( address childIpId, address[] memory parentIpIds, + uint32[] memory royaltyPercents, uint256[] memory licenseTermsIds, address licenseTemplate, bytes memory royaltyContext, @@ -524,24 +525,14 @@ contract LicensingModule is ILicenseTemplate lct = ILicenseTemplate(licenseTemplate); // Confirm that the royalty policies defined in all license terms of the parent IPs are identical. address[] memory rPolicies = new address[](parentIpIds.length); - uint32[] memory rPercents = new uint32[](parentIpIds.length); for (uint256 i = 0; i < parentIpIds.length; i++) { - (address royaltyPolicy, uint32 royaltyPercent, , ) = lct.getRoyaltyPolicy(licenseTermsIds[i]); - Licensing.LicensingConfig memory lsc = LICENSE_REGISTRY.getLicensingConfig( - parentIpIds[i], - licenseTemplate, - licenseTermsIds[i] - ); - if (lsc.isSet && lsc.commercialRevShare != 0) { - royaltyPercent = lsc.commercialRevShare; - } - rPercents[i] = royaltyPercent; + (address royaltyPolicy, , , ) = lct.getRoyaltyPolicy(licenseTermsIds[i]); rPolicies[i] = royaltyPolicy; } if (rPolicies.length != 0 && rPolicies[0] != address(0)) { // Notify the royalty module - ROYALTY_MODULE.onLinkToParents(childIpId, parentIpIds, rPolicies, rPercents, royaltyContext, maxRts); + ROYALTY_MODULE.onLinkToParents(childIpId, parentIpIds, rPolicies, royaltyPercents, royaltyContext, maxRts); } } diff --git a/contracts/registries/LicenseRegistry.sol b/contracts/registries/LicenseRegistry.sol index 0897bc80..854ebcc7 100644 --- a/contracts/registries/LicenseRegistry.sol +++ b/contracts/registries/LicenseRegistry.sol @@ -519,6 +519,26 @@ contract LicenseRegistry is ILicenseRegistry, AccessManagedUpgradeable, UUPSUpgr return (_getLicenseTemplate(parentIpId), $.parentLicenseTerms[childIpId][parentIpId]); } + /// @notice Return the Royalty percentage of the license terms of the IP. + /// There are 2 places to get the royalty percentage: license terms, LicenseConfig + /// The order of priority is LicenseConfig > license terms + /// @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. + /// @return royaltyPercent The Royalty percentage 100% is 100_000_000. + function getRoyaltyPercent( + address ipId, + address licenseTemplate, + uint256 licenseTermsId + ) external view returns (uint32 royaltyPercent) { + ILicenseTemplate lct = ILicenseTemplate(licenseTemplate); + (, royaltyPercent, , ) = lct.getRoyaltyPolicy(licenseTermsId); + Licensing.LicensingConfig memory lsc = _getLicensingConfig(ipId, licenseTemplate, licenseTermsId); + if (lsc.isSet && lsc.commercialRevShare > 0) { + royaltyPercent = lsc.commercialRevShare; + } + } + /// @dev verify the child IP can be registered as a derivative of the parent IP /// @param parentIpId The address of the parent IP /// @param childIpId The address of the child IP diff --git a/script/foundry/utils/DeployHelper.sol b/script/foundry/utils/DeployHelper.sol index e8a9d141..1e0a7d20 100644 --- a/script/foundry/utils/DeployHelper.sol +++ b/script/foundry/utils/DeployHelper.sol @@ -494,7 +494,7 @@ contract DeployHelper is Script, BroadcastManager, JsonDeploymentHandler, Storag contractKey = "LicenseToken"; _predeploy(contractKey); - impl = address(new LicenseToken(address(licensingModule), address(disputeModule))); + impl = address(new LicenseToken(address(licensingModule), address(disputeModule), address(licenseRegistry))); licenseToken = LicenseToken( TestProxyHelper.deployUUPSProxy( create3Deployer, diff --git a/test/foundry/LicenseToken.t.sol b/test/foundry/LicenseToken.t.sol index 7edfd29a..7a0eac5c 100644 --- a/test/foundry/LicenseToken.t.sol +++ b/test/foundry/LicenseToken.t.sol @@ -9,6 +9,7 @@ import { PILFlavors } from "../../contracts/lib/PILFlavors.sol"; import { PILTerms } from "../../contracts/interfaces/modules/licensing/IPILicenseTemplate.sol"; import { LicenseToken } from "../../contracts/LicenseToken.sol"; import { ILicenseToken } from "../../contracts/interfaces/ILicenseToken.sol"; +import { Licensing } from "../../contracts/lib/Licensing.sol"; // test import { BaseTest } from "./utils/BaseTest.t.sol"; @@ -148,7 +149,7 @@ contract LicenseTokenTest is BaseTest { function test_LicenseToken_TokenURI() public { uint256 licenseTermsId = pilTemplate.registerLicenseTerms(PILFlavors.nonCommercialSocialRemixing()); - + uint256 commercialRevShare = 10_000_000; vm.prank(address(licensingModule)); uint256 licenseTokenId = licenseToken.mintLicenseTokens({ licensorIpId: ipAcct[1], @@ -204,4 +205,42 @@ contract LicenseTokenTest is BaseTest { assertEq(lmt.licenseTermsId, licenseTermsId); assertEq(lmt.transferable, true); } + + function test_LicenseToken_getLicenseTokenMetadata_commercialRevShare() public { + uint256 licenseTermsId = pilTemplate.registerLicenseTerms( + PILFlavors.commercialRemix(0, 10_000_000, address(royaltyPolicyLAP), address(USDC)) + ); + + // attach license terms to the ipAcct + Licensing.LicensingConfig memory licensingConfig = Licensing.LicensingConfig({ + isSet: true, + mintingFee: 0, + licensingHook: address(0), + hookData: "", + commercialRevShare: 20_000_000, + disabled: false, + expectMinimumGroupRewardShare: 0, + expectGroupRewardPool: address(0) + }); + vm.startPrank(ipOwner[1]); + licensingModule.attachLicenseTerms(ipAcct[1], address(pilTemplate), licenseTermsId); + licensingModule.setLicensingConfig(ipAcct[1], address(pilTemplate), licenseTermsId, licensingConfig); + vm.stopPrank(); + vm.prank(address(licensingModule)); + uint256 licenseTokenId = licenseToken.mintLicenseTokens({ + licensorIpId: ipAcct[1], + licenseTemplate: address(pilTemplate), + licenseTermsId: licenseTermsId, + amount: 1, + minter: ipOwner[1], + receiver: ipOwner[1] + }); + + ILicenseToken.LicenseTokenMetadata memory lmt = licenseToken.getLicenseTokenMetadata(licenseTokenId); + assertEq(lmt.licensorIpId, ipAcct[1]); + assertEq(lmt.licenseTemplate, address(pilTemplate)); + assertEq(lmt.licenseTermsId, licenseTermsId); + assertEq(lmt.commercialRevShare, 20_000_000); + assertEq(lmt.transferable, true); + } }