From df15b0990eef7eb22e766634db3b64fa1876b1e8 Mon Sep 17 00:00:00 2001 From: kingster-will <83567446+kingster-will@users.noreply.github.com> Date: Thu, 11 Apr 2024 17:50:18 -0700 Subject: [PATCH] Unit Tests and Enhancements for Licensing Components (#64) * Add licensing tests * Add More tests --- contracts/LicenseToken.sol | 8 +- .../modules/licensing/IPILicenseTemplate.sol | 5 + contracts/lib/Errors.sol | 21 +- .../BaseLicenseTemplateUpgradeable.sol | 30 +- .../modules/licensing/LicensingModule.sol | 4 +- .../modules/licensing/PILicenseTemplate.sol | 26 +- .../LicensorApprovalChecker.sol | 45 +- contracts/registries/LicenseRegistry.sol | 23 +- deploy-out/deployment-11155111.json | 2 +- script/foundry/utils/DeployHelper.sol | 3 +- .../licensing/LicensingIntegration.t.sol | 402 +++++ .../mocks/module/MockLicenseTemplate.sol | 145 ++ .../modules/licensing/LicensingModule.t.sol | 1309 +++++++++++++---- .../modules/licensing/PILicenseTemplate.t.sol | 526 +++++++ test/foundry/registries/LicenseRegistry.t.sol | 268 ++++ 15 files changed, 2492 insertions(+), 325 deletions(-) create mode 100644 test/foundry/integration/flows/licensing/LicensingIntegration.t.sol create mode 100644 test/foundry/mocks/module/MockLicenseTemplate.sol create mode 100644 test/foundry/modules/licensing/PILicenseTemplate.t.sol create mode 100644 test/foundry/registries/LicenseRegistry.t.sol diff --git a/contracts/LicenseToken.sol b/contracts/LicenseToken.sol index 034176e70..ae5554e73 100644 --- a/contracts/LicenseToken.sol +++ b/contracts/LicenseToken.sol @@ -22,7 +22,7 @@ contract LicenseToken is ILicenseToken, ERC721EnumerableUpgradeable, AccessManag /// @notice Emitted for metadata updates, per EIP-4906 event BatchMetadataUpdate(uint256 _fromTokenId, uint256 _toTokenId); - /// @dev Storage of the LicenseToken + /// @dev Storage structure for the LicenseToken /// @custom:storage-location erc7201:story-protocol.LicenseToken struct LicenseTokenStorage { string imageUrl; @@ -32,10 +32,9 @@ contract LicenseToken is ILicenseToken, ERC721EnumerableUpgradeable, AccessManag mapping(uint256 tokenId => LicenseTokenMetadata) licenseTokenMetadatas; } - // TODO: update the storage location // keccak256(abi.encode(uint256(keccak256("story-protocol.LicenseToken")) - 1)) & ~bytes32(uint256(0xff)); bytes32 private constant LicenseTokenStorageLocation = - 0x5ed898e10dedf257f39672a55146f3fecade9da16f4ff022557924a10d60a900; + 0x62a0d75e37bea0c3e666dc72a74112fc6af15ce635719127e380d8ca1e555d00; modifier onlyLicensingModule() { if (msg.sender != address(_getLicenseTokenStorage().licensingModule)) { @@ -319,7 +318,8 @@ contract LicenseToken is ILicenseToken, ERC721EnumerableUpgradeable, AccessManag // Upgrades related // //////////////////////////////////////////////////////////////////////////// - function _getLicenseTokenStorage() internal pure returns (LicenseTokenStorage storage $) { + /// @dev Returns the storage struct of LicenseToken. + function _getLicenseTokenStorage() private pure returns (LicenseTokenStorage storage $) { assembly { $.slot := LicenseTokenStorageLocation } diff --git a/contracts/interfaces/modules/licensing/IPILicenseTemplate.sol b/contracts/interfaces/modules/licensing/IPILicenseTemplate.sol index 76fb3b943..299a8edd7 100644 --- a/contracts/interfaces/modules/licensing/IPILicenseTemplate.sol +++ b/contracts/interfaces/modules/licensing/IPILicenseTemplate.sol @@ -57,4 +57,9 @@ interface IPILicenseTemplate is ILicenseTemplate { /// @param terms The PILTerms to get the ID for. /// @return selectedLicenseTermsId The ID of the given license terms. function getLicenseTermsId(PILTerms calldata terms) external view returns (uint256 selectedLicenseTermsId); + + /// @notice Gets license terms of the given ID. + /// @param selectedLicenseTermsId The ID of the license terms. + /// @return terms The PILTerms associate with the given ID. + function getLicenseTerms(uint256 selectedLicenseTermsId) external view returns (PILTerms memory terms); } diff --git a/contracts/lib/Errors.sol b/contracts/lib/Errors.sol index f86d7cbcf..1632a1ae6 100644 --- a/contracts/lib/Errors.sol +++ b/contracts/lib/Errors.sol @@ -123,6 +123,7 @@ library Errors { error LicenseRegistry__ParentIpExpired(address ipId); error LicenseRegistry__LicenseTermsNotExists(address licenseTemplate, uint256 licenseTermsId); error LicenseRegistry__ParentIpHasNoLicenseTerms(address ipId, uint256 licenseTermsId); + error LicenseRegistry__LicensorIpHasNoLicenseTerms(address ipId, address licenseTemplate, uint256 licenseTermsId); error LicenseRegistry__NoParentIp(); error LicenseRegistry__DerivativeIpAlreadyHasLicense(address childIpId); error LicenseRegistry__DerivativeAlreadyRegistered(address childIpId); @@ -130,6 +131,8 @@ library Errors { error LicenseRegistry__DerivativeIsParent(address ipId); error LicenseRegistry__ParentIpUnmachedLicenseTemplate(address ipId, address licenseTemplate); error LicenseRegistry__IndexOutOfBounds(address ipId, uint256 index, uint256 length); + error LicenseRegistry__LicenseTermsAlreadyAttached(address ipId, address licenseTemplate, uint256 licenseTermsId); + error LicenseRegistry__UnmatchedLicenseTemplate(address ipId, address licenseTemplate, address newLicenseTemplate); //////////////////////////////////////////////////////////////////////////// // LicenseToken // @@ -204,19 +207,11 @@ library Errors { error LicensingModule__LicenseNotCompatibleForDerivative(address childIpId); error LicensingModule__NoLicenseToken(); error LicensingModule__LicenseTokenNotCompatibleForDerivative(address childIpId, uint256[] licenseTokenIds); - - //////////////////////////////////////////////////////////////////////////// - // BasePolicyFrameworkManager // - //////////////////////////////////////////////////////////////////////////// - - error BasePolicyFrameworkManager__CallerNotLicensingModule(); - - //////////////////////////////////////////////////////////////////////////// - // PolicyFrameworkManager // - //////////////////////////////////////////////////////////////////////////// - - error PolicyFrameworkManager__GettingPolicyWrongFramework(); - error PolicyFrameworkManager__CommercializerCheckerDoesNotSupportHook(address commercializer); + error LicensingModule__LicenseDenyMintLicenseToken( + address licenseTemplate, + uint256 licenseTermsId, + address licensorIpId + ); //////////////////////////////////////////////////////////////////////////// // LicensorApprovalChecker // diff --git a/contracts/modules/licensing/BaseLicenseTemplateUpgradeable.sol b/contracts/modules/licensing/BaseLicenseTemplateUpgradeable.sol index 1e646197c..219d79afb 100644 --- a/contracts/modules/licensing/BaseLicenseTemplateUpgradeable.sol +++ b/contracts/modules/licensing/BaseLicenseTemplateUpgradeable.sol @@ -9,16 +9,17 @@ import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/I import { ILicenseTemplate } from "../../interfaces/modules/licensing/ILicenseTemplate.sol"; abstract contract BaseLicenseTemplateUpgradeable is ILicenseTemplate, ERC165, Initializable { - /// @custom:storage-location erc7201:story-protocol.BaseLicenseTemplate - struct BaseLicenseTemplateStorage { + /// @dev Storage structure for the BaseLicenseTemplateUpgradeable + /// @custom:storage-location erc7201:story-protocol.BaseLicenseTemplateUpgradeable + struct BaseLicenseTemplateUpgradeableStorage { string name; string metadataURI; } - // keccak256(abi.encode(uint256(keccak256("story-protocol.BaseLicenseTemplate")) - 1)) - // & ~bytes32(uint256(0xff)); - bytes32 private constant BaseLicenseTemplateStorageLocation = - 0xa55803740ac9329334ad7b6cde0ec056cc3ba32125b59c579552512bed001f00; + // keccak256(abi.encode(uint256(keccak256("story-protocol.BaseLicenseTemplateUpgradeable")) - 1)) & + // ~bytes32(uint256(0xff)); + bytes32 private constant BaseLicenseTemplateUpgradeableStorageLocation = + 0x96c2f019b095cfe7c4d1f26aa9d2741961fe73294777688374a3299707c2fb00; /// @custom:oz-upgrades-unsafe-allow constructor constructor() { @@ -29,18 +30,18 @@ abstract contract BaseLicenseTemplateUpgradeable is ILicenseTemplate, ERC165, In /// @param _name The name of the license template /// @param _metadataURI The URL to the off chain metadata function __BaseLicenseTemplate_init(string memory _name, string memory _metadataURI) internal onlyInitializing { - _getBaseLicenseTemplateStorage().name = _name; - _getBaseLicenseTemplateStorage().metadataURI = _metadataURI; + _getBaseLicenseTemplateUpgradeableStorage().name = _name; + _getBaseLicenseTemplateUpgradeableStorage().metadataURI = _metadataURI; } /// @notice Returns the name of the license template function name() public view override returns (string memory) { - return _getBaseLicenseTemplateStorage().name; + return _getBaseLicenseTemplateUpgradeableStorage().name; } /// @notice Returns the URL to the off chain metadata function getMetadataURI() public view override returns (string memory) { - return _getBaseLicenseTemplateStorage().metadataURI; + return _getBaseLicenseTemplateUpgradeableStorage().metadataURI; } /// @notice IERC165 interface support. @@ -48,9 +49,14 @@ abstract contract BaseLicenseTemplateUpgradeable is ILicenseTemplate, ERC165, In return interfaceId == type(ILicenseTemplate).interfaceId || super.supportsInterface(interfaceId); } - function _getBaseLicenseTemplateStorage() internal pure returns (BaseLicenseTemplateStorage storage $) { + /// @dev Returns the storage struct of BaseLicenseTemplateUpgradeable. + function _getBaseLicenseTemplateUpgradeableStorage() + private + pure + returns (BaseLicenseTemplateUpgradeableStorage storage $) + { assembly { - $.slot := BaseLicenseTemplateStorageLocation + $.slot := BaseLicenseTemplateUpgradeableStorageLocation } } } diff --git a/contracts/modules/licensing/LicensingModule.sol b/contracts/modules/licensing/LicensingModule.sol index 5e25ac574..503f0288c 100644 --- a/contracts/modules/licensing/LicensingModule.sol +++ b/contracts/modules/licensing/LicensingModule.sol @@ -167,7 +167,9 @@ contract LicensingModule is _payMintingFee(licensorIpId, licenseTemplate, licenseTermsId, amount, royaltyContext, mlc); - ILicenseTemplate(licenseTemplate).verifyMintLicenseToken(licenseTermsId, receiver, licensorIpId, amount); + if (!ILicenseTemplate(licenseTemplate).verifyMintLicenseToken(licenseTermsId, receiver, licensorIpId, amount)) { + revert Errors.LicensingModule__LicenseDenyMintLicenseToken(licenseTemplate, licenseTermsId, licensorIpId); + } startLicenseTokenId = LICENSE_NFT.mintLicenseTokens( licensorIpId, diff --git a/contracts/modules/licensing/PILicenseTemplate.sol b/contracts/modules/licensing/PILicenseTemplate.sol index 206dfc5f1..581280154 100644 --- a/contracts/modules/licensing/PILicenseTemplate.sol +++ b/contracts/modules/licensing/PILicenseTemplate.sol @@ -27,6 +27,7 @@ contract PILicenseTemplate is using ERC165Checker for address; using Strings for *; + /// @dev Storage structure for the PILicenseTemplate /// @custom:storage-location erc7201:story-protocol.PILicenseTemplate struct PILicenseTemplateStorage { mapping(uint256 licenseTermsId => PILTerms) licenseTerms; @@ -39,20 +40,17 @@ contract PILicenseTemplate is /// @custom:oz-upgrades-unsafe-allow state-variable-immutable IRoyaltyModule public immutable ROYALTY_MODULE; - // TODO: update storage location - // keccak256(abi.encode(uint256(keccak256("story-protocol.BaseLicenseTemplate")) - 1)) - // & ~bytes32(uint256(0xff)); + // keccak256(abi.encode(uint256(keccak256("story-protocol.PILicenseTemplate")) - 1)) & ~bytes32(uint256(0xff)); bytes32 private constant PILicenseTemplateStorageLocation = - 0xa55803740ac9329334ad7b6cde0ec056cc3ba32125b59c579552512bed001f00; + 0xc6c6991297bc120d0383f0017fab72b8ca34fd4849ed6478dbaac67a33c3a700; /// @custom:oz-upgrades-unsafe-allow constructor constructor( address accessController, address ipAccountRegistry, address licenseRegistry, - address royaltyModule, - address licenseToken - ) LicensorApprovalChecker(accessController, ipAccountRegistry, licenseToken) { + address royaltyModule + ) LicensorApprovalChecker(accessController, ipAccountRegistry) { LICENSE_REGISTRY = ILicenseRegistry(licenseRegistry); ROYALTY_MODULE = IRoyaltyModule(royaltyModule); _disableInitializers(); @@ -234,7 +232,7 @@ contract PILicenseTemplate is uint expireTime = _getExpireTime(licenseTermsIds[0], start); for (uint i = 1; i < licenseTermsIds.length; i++) { uint newExpireTime = _getExpireTime(licenseTermsIds[i], start); - if (newExpireTime < expireTime) { + if (newExpireTime < expireTime || expireTime == 0) { expireTime = newExpireTime; } } @@ -258,6 +256,14 @@ contract PILicenseTemplate is return $.hashedLicenseTerms[licenseTermsHash]; } + /// @notice Gets license terms of the given ID. + /// @param selectedLicenseTermsId The ID of the license terms. + /// @return terms The PILTerms associate with the given ID. + function getLicenseTerms(uint256 selectedLicenseTermsId) external view returns (PILTerms memory terms) { + PILicenseTemplateStorage storage $ = _getPILicenseTemplateStorage(); + return $.licenseTerms[selectedLicenseTermsId]; + } + /// @notice Returns the total number of registered license terms. /// @return The total number of registered license terms. function totalRegisteredLicenseTerms() external view returns (uint256) { @@ -289,7 +295,7 @@ contract PILicenseTemplate is terms.expiration == 0 ? "never" : terms.expiration.toString(), '"},', '{"trait_type": "Currency", "value": "', - terms.currency == address(0) ? "Native Token" : terms.currency.toHexString(), + terms.currency.toHexString(), '"},', // Skip transferable, it's already added in the common attributes by the LicenseRegistry. _policyCommercialTraitsToJson(terms), @@ -419,7 +425,7 @@ contract PILicenseTemplate is // If the policy defines the licensor must approve derivatives, check if the // derivative is approved by the licensor - if (terms.derivativesApproval && !isDerivativeApproved(licenseTermsId, childIpId)) { + if (terms.derivativesApproval && !isDerivativeApproved(parentIpId, licenseTermsId, childIpId)) { return false; } // Check if the commercializerChecker allows the link diff --git a/contracts/modules/licensing/parameter-helpers/LicensorApprovalChecker.sol b/contracts/modules/licensing/parameter-helpers/LicensorApprovalChecker.sol index d01621b83..a221b98e8 100644 --- a/contracts/modules/licensing/parameter-helpers/LicensorApprovalChecker.sol +++ b/contracts/modules/licensing/parameter-helpers/LicensorApprovalChecker.sol @@ -2,8 +2,6 @@ pragma solidity 0.8.23; import { AccessControlled } from "../../../access/AccessControlled.sol"; -import { ILicenseToken } from "../../../interfaces/ILicenseToken.sol"; - import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; /// @title LicensorApprovalChecker @@ -11,12 +9,12 @@ import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/I /// licensing terms like "Derivatives With Approval" in PIL. abstract contract LicensorApprovalChecker is AccessControlled, Initializable { /// @notice Emits when a derivative IP account is approved by the parentIp. - /// @param licenseTokenId The ID of the license waiting for approval + /// @param licenseTermsId The ID of the license waiting for approval /// @param ipId The ID of the derivative IP to be approved /// @param caller The executor of the approval /// @param approved Result of the approval event DerivativeApproved( - uint256 indexed licenseTokenId, + uint256 indexed licenseTermsId, address indexed ipId, address indexed caller, bool approved @@ -30,10 +28,6 @@ abstract contract LicensorApprovalChecker is AccessControlled, Initializable { mapping(uint256 => mapping(address => mapping(address => bool))) approvals; } - /// @notice Returns the licenseToken address - /// @custom:oz-upgrades-unsafe-allow state-variable-immutable - ILicenseToken public immutable LICENSE_NFT; - // keccak256(abi.encode(uint256(keccak256("story-protocol.LicensorApprovalChecker")) - 1)) // & ~bytes32(uint256(0xff)); bytes32 private constant LicensorApprovalCheckerStorageLocation = @@ -42,50 +36,49 @@ abstract contract LicensorApprovalChecker is AccessControlled, Initializable { /// @notice Constructor function /// @param accessController The address of the AccessController contract /// @param ipAccountRegistry The address of the IPAccountRegistry contract - /// @param licenseToken The address of the LicenseRegistry contract /// @custom:oz-upgrades-unsafe-allow constructor constructor( address accessController, - address ipAccountRegistry, - address licenseToken - ) AccessControlled(accessController, ipAccountRegistry) { - LICENSE_NFT = ILicenseToken(licenseToken); - } + address ipAccountRegistry + ) AccessControlled(accessController, ipAccountRegistry) {} /// @notice Approves or disapproves a derivative IP account. - /// @param licenseTokenId The ID of the license waiting for approval + /// @param parentIpId The ID of the parent IP grant the approval + /// @param licenseTermsId The ID of the license waiting for approval /// @param childIpId The ID of the derivative IP to be approved /// @param approved Result of the approval - function setApproval(uint256 licenseTokenId, address childIpId, bool approved) external { - address parentIpId = LICENSE_NFT.getLicensorIpId(licenseTokenId); - _setApproval(parentIpId, licenseTokenId, childIpId, approved); + function setApproval(address parentIpId, uint256 licenseTermsId, address childIpId, bool approved) external { + _setApproval(parentIpId, licenseTermsId, childIpId, approved); } /// @notice Checks if a derivative IP account is approved by the parent. - /// @param licenseTokenId The ID of the license NFT issued from a policy of the parent + /// @param licenseTermsId The ID of the license NFT issued from a policy of the parent /// @param childIpId The ID of the derivative IP to be approved /// @return approved True if the derivative IP account using the license is approved - function isDerivativeApproved(uint256 licenseTokenId, address childIpId) public view returns (bool) { - address parentIpId = LICENSE_NFT.getLicensorIpId(licenseTokenId); + function isDerivativeApproved( + address parentIpId, + uint256 licenseTermsId, + address childIpId + ) public view returns (bool) { LicensorApprovalCheckerStorage storage $ = _getLicensorApprovalCheckerStorage(); - return $.approvals[licenseTokenId][parentIpId][childIpId]; + return $.approvals[licenseTermsId][parentIpId][childIpId]; } /// @notice Sets the approval for a derivative IP account. /// @dev This function is only callable by the parent IP account. /// @param parentIpId The ID of the parent IP account - /// @param licenseTokenId The ID of the license waiting for approval + /// @param licenseTermsId The ID of the license waiting for approval /// @param childIpId The ID of the derivative IP to be approved /// @param approved Result of the approval function _setApproval( address parentIpId, - uint256 licenseTokenId, + uint256 licenseTermsId, address childIpId, bool approved ) internal verifyPermission(parentIpId) { LicensorApprovalCheckerStorage storage $ = _getLicensorApprovalCheckerStorage(); - $.approvals[licenseTokenId][parentIpId][childIpId] = approved; - emit DerivativeApproved(licenseTokenId, parentIpId, msg.sender, approved); + $.approvals[licenseTermsId][parentIpId][childIpId] = approved; + emit DerivativeApproved(licenseTermsId, parentIpId, msg.sender, approved); } /// @dev Returns the storage struct of LicensorApprovalChecker. diff --git a/contracts/registries/LicenseRegistry.sol b/contracts/registries/LicenseRegistry.sol index e0dcc84ec..f18ba8efd 100644 --- a/contracts/registries/LicenseRegistry.sol +++ b/contracts/registries/LicenseRegistry.sol @@ -43,6 +43,7 @@ contract LicenseRegistry is ILicenseRegistry, AccessManagedUpgradeable, UUPSUpgr /// @param mintingLicenseConfigs Mapping of minting license configs to a licenseTerms of an IP /// @param mintingLicenseConfigsForIp 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 struct LicenseRegistryStorage { ILicensingModule licensingModule; @@ -58,7 +59,6 @@ contract LicenseRegistry is ILicenseRegistry, AccessManagedUpgradeable, UUPSUpgr mapping(address ipId => Licensing.MintingLicenseConfig mintingLicenseConfig) mintingLicenseConfigsForIp; } - // TODO: update the storage location // keccak256(abi.encode(uint256(keccak256("story-protocol.LicenseRegistry")) - 1)) & ~bytes32(uint256(0xff)); bytes32 private constant LicenseRegistryStorageLocation = 0x5ed898e10dedf257f39672a55146f3fecade9da16f4ff022557924a10d60a900; @@ -193,14 +193,23 @@ contract LicenseRegistry is ILicenseRegistry, AccessManagedUpgradeable, UUPSUpgr revert Errors.LicensingModule__LicenseTermsNotFound(licenseTemplate, licenseTermsId); } + if (_isExpiredNow(ipId)) { + revert Errors.LicenseRegistry__IpExpired(ipId); + } + + if (_hasIpAttachedLicenseTerms(ipId, licenseTemplate, licenseTermsId)) { + revert Errors.LicenseRegistry__LicenseTermsAlreadyAttached(ipId, licenseTemplate, licenseTermsId); + } + if (_isDerivativeIp(ipId)) { revert Errors.LicensingModule__DerivativesCannotAddLicenseTerms(); } LicenseRegistryStorage storage $ = _getLicenseRegistryStorage(); - if (_isExpiredNow(ipId)) { - revert Errors.LicenseRegistry__IpExpired(ipId); + if ($.licenseTemplates[ipId] != address(0) && $.licenseTemplates[ipId] != licenseTemplate) { + revert Errors.LicenseRegistry__UnmatchedLicenseTemplate(ipId, $.licenseTemplates[ipId], licenseTemplate); } + $.licenseTemplates[ipId] = licenseTemplate; $.attachedLicenseTerms[ipId].add(licenseTermsId); } @@ -220,12 +229,12 @@ contract LicenseRegistry is ILicenseRegistry, AccessManagedUpgradeable, UUPSUpgr revert Errors.LicenseRegistry__NoParentIp(); } LicenseRegistryStorage storage $ = _getLicenseRegistryStorage(); - if ($.attachedLicenseTerms[childIpId].length() > 0) { - revert Errors.LicenseRegistry__DerivativeIpAlreadyHasLicense(childIpId); - } if ($.parentIps[childIpId].length() > 0) { revert Errors.LicenseRegistry__DerivativeAlreadyRegistered(childIpId); } + if ($.attachedLicenseTerms[childIpId].length() > 0) { + revert Errors.LicenseRegistry__DerivativeIpAlreadyHasLicense(childIpId); + } for (uint256 i = 0; i < parentIpIds.length; i++) { _verifyDerivativeFromParent(parentIpIds[i], childIpId, licenseTemplate, licenseTermsIds[i]); @@ -262,7 +271,7 @@ contract LicenseRegistry is ILicenseRegistry, AccessManagedUpgradeable, UUPSUpgr revert Errors.LicenseRegistry__LicenseTermsNotExists(licenseTemplate, licenseTermsId); } } else if (!_hasIpAttachedLicenseTerms(licensorIpId, licenseTemplate, licenseTermsId)) { - revert Errors.LicenseRegistry__ParentIpHasNoLicenseTerms(licensorIpId, licenseTermsId); + revert Errors.LicenseRegistry__LicensorIpHasNoLicenseTerms(licensorIpId, licenseTemplate, licenseTermsId); } return _getMintingLicenseConfig(licensorIpId, licenseTemplate, licenseTermsId); } diff --git a/deploy-out/deployment-11155111.json b/deploy-out/deployment-11155111.json index 172cd1fb8..8418b1e48 100644 --- a/deploy-out/deployment-11155111.json +++ b/deploy-out/deployment-11155111.json @@ -13,7 +13,7 @@ "MockERC721": "0x5eB725102781e57E2FF93c74822A7e22Ae6b8810", "MockTokenGatedHook": "0x0f5900B48245F29D10a3cEE68B3E66e01F416E89", "ModuleRegistry": "0xECaaD3dCB6bD1f6D92b308C115c0698952A65769", - "PILPolicyFrameworkManager": "0x4DbFB22d5Fe7f05C1C0a2977B7dB9E688E396016", + "PILicenseTemplate": "0x4DbFB22d5Fe7f05C1C0a2977B7dB9E688E396016", "RoyaltyModule": "0xaac2E226b2Fa900cfEbC9d4bcdb9267De5E13573", "RoyaltyPolicyLAP": "0x8524E19E9e64B8ba299c7821BD7968d4E8b670A8", "TokenWithdrawalModule": "0xCbCdCf569383c0C743c1E79Fdc1F1d7442D9cE63" diff --git a/script/foundry/utils/DeployHelper.sol b/script/foundry/utils/DeployHelper.sol index 0f7f629b7..83a6eee82 100644 --- a/script/foundry/utils/DeployHelper.sol +++ b/script/foundry/utils/DeployHelper.sol @@ -310,8 +310,7 @@ contract DeployHelper is Script, BroadcastManager, JsonDeploymentHandler, Storag address(accessController), address(ipAccountRegistry), address(licenseRegistry), - address(royaltyModule), - address(licenseToken) + address(royaltyModule) ) ); pilTemplate = PILicenseTemplate( diff --git a/test/foundry/integration/flows/licensing/LicensingIntegration.t.sol b/test/foundry/integration/flows/licensing/LicensingIntegration.t.sol new file mode 100644 index 000000000..2746c02a3 --- /dev/null +++ b/test/foundry/integration/flows/licensing/LicensingIntegration.t.sol @@ -0,0 +1,402 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.23; + +// external +import { Test } from "forge-std/Test.sol"; +import { ERC6551Registry } from "erc6551/ERC6551Registry.sol"; +import { UpgradeableBeacon } from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; +import { AccessManager } from "@openzeppelin/contracts/access/manager/AccessManager.sol"; + +// contracts +import { AccessController } from "../../../../../contracts/access/AccessController.sol"; +import { IPAccountImpl } from "../../../../../contracts/IPAccountImpl.sol"; +import { IPAssetRegistry } from "../../../../../contracts/registries/IPAssetRegistry.sol"; +import { ModuleRegistry } from "../../../../../contracts/registries/ModuleRegistry.sol"; +import { LicenseRegistry } from "../../../../../contracts/registries/LicenseRegistry.sol"; +import { RoyaltyModule } from "../../../../../contracts/modules/royalty/RoyaltyModule.sol"; +import { RoyaltyPolicyLAP } from "../../../../../contracts/modules/royalty/policies/RoyaltyPolicyLAP.sol"; +import { DisputeModule } from "../../../../../contracts/modules/dispute/DisputeModule.sol"; +import { LicensingModule } from "../../../../../contracts/modules/licensing/LicensingModule.sol"; +import { ArbitrationPolicySP } from "../../../../../contracts/modules/dispute/policies/ArbitrationPolicySP.sol"; +import { IpRoyaltyVault } from "../../../../../contracts/modules/royalty/policies/IpRoyaltyVault.sol"; +import { LicenseToken } from "../../../../../contracts/LicenseToken.sol"; +import { DISPUTE_MODULE_KEY, LICENSING_MODULE_KEY, ROYALTY_MODULE_KEY } from "contracts/lib/modules/Module.sol"; +import { PILicenseTemplate } from "../../../../../contracts/modules/licensing/PILicenseTemplate.sol"; +import { PILFlavors } from "../../../../../contracts/lib/PILFlavors.sol"; +import { MockERC20 } from "../../../mocks/token/MockERC20.sol"; +import { MockERC721 } from "../../../mocks/token/MockERC721.sol"; +import { TestProxyHelper } from "../../../utils/TestProxyHelper.sol"; + +contract e2e is Test { + MockERC20 erc20; + MockERC721 mockNft; + uint256 internal constant ARBITRATION_PRICE = 1000; + + address admin; + address alice; + address bob; + address charlie; + address dave; + address eve; + + AccessManager protocolAccessManager; + AccessController accessController; + ModuleRegistry moduleRegistry; + ERC6551Registry erc6551Registry; + IPAccountImpl ipAccountImpl; + IPAssetRegistry ipAssetRegistry; + LicenseRegistry licenseRegistry; + LicenseToken licenseToken; + RoyaltyModule royaltyModule; + DisputeModule disputeModule; + LicensingModule licensingModule; + PILicenseTemplate piLicenseTemplate; + RoyaltyPolicyLAP royaltyPolicyLAP; + + address ipId1; + address ipId2; + address ipId3; + address ipId6; + address ipId7; + + error ERC721NonexistentToken(uint256 tokenId); + + function setUp() public { + admin = vm.addr(1); + alice = vm.addr(2); + bob = vm.addr(3); + charlie = vm.addr(4); + dave = vm.addr(5); + eve = vm.addr(6); + + erc20 = new MockERC20(); + mockNft = new MockERC721("ape"); + protocolAccessManager = new AccessManager(admin); + + // Deploy contracts + address impl = address(new AccessController()); + accessController = AccessController( + TestProxyHelper.deployUUPSProxy( + impl, + abi.encodeCall(AccessController.initialize, address(protocolAccessManager)) + ) + ); + + impl = address(new ModuleRegistry()); + moduleRegistry = ModuleRegistry( + TestProxyHelper.deployUUPSProxy( + impl, + abi.encodeCall(AccessController.initialize, address(protocolAccessManager)) + ) + ); + + erc6551Registry = new ERC6551Registry(); + ipAccountImpl = new IPAccountImpl(address(accessController)); + ipAssetRegistry = new IPAssetRegistry(address(erc6551Registry), address(ipAccountImpl)); + + impl = address(new LicenseRegistry()); + licenseRegistry = LicenseRegistry( + TestProxyHelper.deployUUPSProxy( + impl, + abi.encodeCall(LicenseRegistry.initialize, (address(protocolAccessManager))) + ) + ); + + impl = address(new LicenseToken()); + licenseToken = LicenseToken( + TestProxyHelper.deployUUPSProxy( + impl, + abi.encodeCall(LicenseToken.initialize, (address(protocolAccessManager), "image_url")) + ) + ); + + impl = address(new RoyaltyModule()); + royaltyModule = RoyaltyModule( + TestProxyHelper.deployUUPSProxy( + impl, + abi.encodeCall(RoyaltyModule.initialize, (address(protocolAccessManager))) + ) + ); + vm.label(address(royaltyModule), "RoyaltyModule"); + + impl = address(new DisputeModule(address(accessController), address(ipAssetRegistry))); + disputeModule = DisputeModule( + TestProxyHelper.deployUUPSProxy( + impl, + abi.encodeCall(DisputeModule.initialize, (address(protocolAccessManager))) + ) + ); + + impl = address( + new LicensingModule( + address(accessController), + address(ipAssetRegistry), + address(royaltyModule), + address(licenseRegistry), + address(disputeModule), + address(licenseToken) + ) + ); + + licensingModule = LicensingModule( + TestProxyHelper.deployUUPSProxy( + impl, + abi.encodeCall(LicensingModule.initialize, (address(protocolAccessManager))) + ) + ); + + erc20 = new MockERC20(); + mockNft = new MockERC721("ape"); + + impl = address(new ArbitrationPolicySP(address(disputeModule), address(erc20), ARBITRATION_PRICE)); + ArbitrationPolicySP arbitrationPolicySP = ArbitrationPolicySP( + TestProxyHelper.deployUUPSProxy( + impl, + abi.encodeCall(ArbitrationPolicySP.initialize, (address(protocolAccessManager))) + ) + ); + + impl = address(new RoyaltyPolicyLAP(address(royaltyModule), address(licensingModule))); + royaltyPolicyLAP = RoyaltyPolicyLAP( + TestProxyHelper.deployUUPSProxy( + impl, + abi.encodeCall(RoyaltyPolicyLAP.initialize, (address(protocolAccessManager))) + ) + ); + + impl = address( + new PILicenseTemplate( + address(accessController), + address(ipAssetRegistry), + address(licenseRegistry), + address(royaltyModule) + ) + ); + piLicenseTemplate = PILicenseTemplate( + TestProxyHelper.deployUUPSProxy( + impl, + abi.encodeCall(PILicenseTemplate.initialize, ("PIL", "PIL-metadata-url")) + ) + ); + + // Configure protocol + vm.startPrank(admin); + + address ipRoyaltyVaultImplementation = address( + new IpRoyaltyVault(address(royaltyPolicyLAP), address(disputeModule)) + ); + address ipRoyaltyVaultBeacon = address( + new UpgradeableBeacon(ipRoyaltyVaultImplementation, address(protocolAccessManager)) + ); + royaltyPolicyLAP.setIpRoyaltyVaultBeacon(ipRoyaltyVaultBeacon); + + royaltyPolicyLAP.setSnapshotInterval(7 days); + + accessController.setAddresses(address(ipAssetRegistry), address(moduleRegistry)); + + moduleRegistry.registerModule(DISPUTE_MODULE_KEY, address(disputeModule)); + moduleRegistry.registerModule(LICENSING_MODULE_KEY, address(licensingModule)); + moduleRegistry.registerModule(ROYALTY_MODULE_KEY, address(royaltyModule)); + + royaltyModule.setLicensingModule(address(licensingModule)); + royaltyModule.setDisputeModule(address(disputeModule)); + royaltyModule.whitelistRoyaltyToken(address(erc20), true); + royaltyModule.whitelistRoyaltyPolicy(address(royaltyPolicyLAP), true); + + disputeModule.whitelistDisputeTag("PLAGIARISM", true); + disputeModule.whitelistArbitrationPolicy(address(arbitrationPolicySP), true); + disputeModule.whitelistArbitrationRelayer(address(arbitrationPolicySP), admin, true); + disputeModule.setBaseArbitrationPolicy(address(arbitrationPolicySP)); + + licenseRegistry.setDisputeModule(address(disputeModule)); + licenseRegistry.setLicensingModule(address(licensingModule)); + licenseRegistry.registerLicenseTemplate(address(piLicenseTemplate)); + + licenseToken.setDisputeModule(address(disputeModule)); + licenseToken.setLicensingModule(address(licensingModule)); + + vm.stopPrank(); + } + + function test_e2e() public { + uint256 tokenId1 = mockNft.mint(alice); + uint256 tokenId2 = mockNft.mint(bob); + uint256 tokenId3 = mockNft.mint(charlie); + uint256 tokenId6 = mockNft.mint(dave); + uint256 tokenId7 = mockNft.mint(eve); + + ipId1 = ipAssetRegistry.register(address(mockNft), tokenId1); + ipId2 = ipAssetRegistry.register(address(mockNft), tokenId2); + ipId3 = ipAssetRegistry.register(address(mockNft), tokenId3); + ipId6 = ipAssetRegistry.register(address(mockNft), tokenId6); + ipId7 = ipAssetRegistry.register(address(mockNft), tokenId7); + + // register license terms + uint256 lcId1 = piLicenseTemplate.registerLicenseTerms(PILFlavors.nonCommercialSocialRemixing()); + assertEq(lcId1, 1); + + uint256 lcId2 = piLicenseTemplate.registerLicenseTerms( + PILFlavors.commercialRemix(100, 10, address(royaltyPolicyLAP), address(erc20)) + ); + assertEq(lcId2, 2); + assertEq( + piLicenseTemplate.getLicenseTermsId( + PILFlavors.commercialRemix(100, 10, address(royaltyPolicyLAP), address(erc20)) + ), + 2 + ); + assertTrue(piLicenseTemplate.exists(2)); + + assertTrue(piLicenseTemplate.exists(lcId2)); + + // attach licenses + vm.startPrank(alice); + licensingModule.attachLicenseTerms(ipId1, address(piLicenseTemplate), 1); + + assertEq(licenseRegistry.hasIpAttachedLicenseTerms(ipId1, address(piLicenseTemplate), 1), true); + assertEq(licenseRegistry.getAttachedLicenseTermsCount(ipId1), 1); + + (address attachedTemplate, uint256 attachedId) = licenseRegistry.getAttachedLicenseTerms(ipId1, 0); + assertEq(attachedTemplate, address(piLicenseTemplate)); + assertEq(attachedId, 1); + + licensingModule.attachLicenseTerms(ipId1, address(piLicenseTemplate), 2); + + assertEq(licenseRegistry.hasIpAttachedLicenseTerms(ipId1, address(piLicenseTemplate), 2), true); + assertEq(licenseRegistry.getAttachedLicenseTermsCount(ipId1), 2); + + (attachedTemplate, attachedId) = licenseRegistry.getAttachedLicenseTerms(ipId1, 1); + assertEq(attachedTemplate, address(piLicenseTemplate)); + assertEq(attachedId, 2); + vm.stopPrank(); + + // register derivative directly + vm.startPrank(bob); + address[] memory parentIpIds = new address[](1); + uint256[] memory licenseTermsIds = new uint256[](1); + parentIpIds[0] = ipId1; + licenseTermsIds[0] = 1; + + licensingModule.registerDerivative(ipId2, parentIpIds, licenseTermsIds, address(piLicenseTemplate), ""); + + assertEq(licenseRegistry.hasIpAttachedLicenseTerms(ipId2, address(piLicenseTemplate), 1), true); + assertEq(licenseRegistry.getAttachedLicenseTermsCount(ipId2), 1); + assertEq(licenseRegistry.isDerivativeIp(ipId2), true); + assertEq(licenseRegistry.hasDerivativeIps(ipId2), false); + assertEq(licenseRegistry.hasDerivativeIps(ipId1), true); + assertEq(licenseRegistry.isDerivativeIp(ipId1), false); + assertEq(licenseRegistry.getDerivativeIpCount(ipId1), 1); + assertEq(licenseRegistry.getDerivativeIpCount(ipId2), 0); + assertEq(licenseRegistry.getDerivativeIp(ipId1, 0), ipId2); + assertEq(licenseRegistry.getParentIp(ipId2, 0), ipId1); + assertEq(licenseRegistry.getParentIpCount(ipId2), 1); + vm.stopPrank(); + + // mint license token + vm.startPrank(charlie); + uint256 lcTokenId = licensingModule.mintLicenseTokens( + ipId1, + address(piLicenseTemplate), + 1, + 1, + address(charlie), + "" + ); + assertEq(licenseToken.ownerOf(lcTokenId), charlie); + assertEq(licenseToken.getLicenseTermsId(lcTokenId), 1); + assertEq(licenseToken.getLicenseTemplate(lcTokenId), address(piLicenseTemplate)); + assertEq(licenseToken.getLicensorIpId(lcTokenId), ipId1); + assertEq(licenseToken.getExpirationTime(lcTokenId), 0); + assertEq(licenseToken.totalMintedTokens(), 1); + + // register derivative with license tokens + uint256[] memory licenseTokens = new uint256[](1); + licenseTokens[0] = lcTokenId; + + licensingModule.registerDerivativeWithLicenseTokens(ipId3, licenseTokens, ""); + + assertEq(licenseRegistry.hasIpAttachedLicenseTerms(ipId3, address(piLicenseTemplate), 1), true); + assertEq(licenseRegistry.getAttachedLicenseTermsCount(ipId3), 1); + assertEq(licenseRegistry.isDerivativeIp(ipId3), true); + assertEq(licenseRegistry.hasDerivativeIps(ipId3), false); + assertEq(licenseRegistry.hasDerivativeIps(ipId1), true); + assertEq(licenseRegistry.isDerivativeIp(ipId1), false); + assertEq(licenseRegistry.getDerivativeIpCount(ipId1), 2); + assertEq(licenseRegistry.getDerivativeIpCount(ipId2), 0); + assertEq(licenseRegistry.getDerivativeIp(ipId1, 0), ipId2); + assertEq(licenseRegistry.getDerivativeIp(ipId1, 1), ipId3); + assertEq(licenseRegistry.getParentIp(ipId3, 0), ipId1); + assertEq(licenseRegistry.getParentIpCount(ipId3), 1); + + vm.expectRevert(abi.encodeWithSelector(ERC721NonexistentToken.selector, lcTokenId)); + assertEq(licenseToken.ownerOf(lcTokenId), address(0)); + assertEq(licenseToken.totalMintedTokens(), 1); + vm.stopPrank(); + + // mint license token with payments + vm.startPrank(dave); + erc20.mint(dave, 1000); + erc20.approve(address(royaltyPolicyLAP), 100); + + lcTokenId = licensingModule.mintLicenseTokens(ipId1, address(piLicenseTemplate), 2, 1, address(dave), ""); + + assertEq(licenseToken.ownerOf(lcTokenId), dave); + assertEq(licenseToken.getLicenseTermsId(lcTokenId), 2); + assertEq(licenseToken.getLicenseTemplate(lcTokenId), address(piLicenseTemplate)); + assertEq(licenseToken.getLicensorIpId(lcTokenId), ipId1); + assertEq(licenseToken.getExpirationTime(lcTokenId), 0); + assertEq(licenseToken.totalMintedTokens(), 2); + assertEq(erc20.balanceOf(dave), 900); + + // register derivative with license tokens + licenseTokens = new uint256[](1); + licenseTokens[0] = lcTokenId; + + licensingModule.registerDerivativeWithLicenseTokens(ipId6, licenseTokens, ""); + + assertEq(licenseRegistry.hasIpAttachedLicenseTerms(ipId6, address(piLicenseTemplate), 2), true); + assertEq(licenseRegistry.getAttachedLicenseTermsCount(ipId6), 1); + assertEq(licenseRegistry.isDerivativeIp(ipId6), true); + assertEq(licenseRegistry.hasDerivativeIps(ipId6), false); + assertEq(licenseRegistry.hasDerivativeIps(ipId1), true); + assertEq(licenseRegistry.isDerivativeIp(ipId1), false); + assertEq(licenseRegistry.getDerivativeIpCount(ipId1), 3); + assertEq(licenseRegistry.getDerivativeIpCount(ipId6), 0); + assertEq(licenseRegistry.getDerivativeIp(ipId1, 0), ipId2); + assertEq(licenseRegistry.getDerivativeIp(ipId1, 1), ipId3); + assertEq(licenseRegistry.getDerivativeIp(ipId1, 2), ipId6); + assertEq(licenseRegistry.getParentIp(ipId6, 0), ipId1); + assertEq(licenseRegistry.getParentIpCount(ipId6), 1); + + vm.expectRevert(abi.encodeWithSelector(ERC721NonexistentToken.selector, lcTokenId)); + assertEq(licenseToken.ownerOf(lcTokenId), address(0)); + assertEq(licenseToken.totalMintedTokens(), 2); + vm.stopPrank(); + + // register derivative directly with payments + vm.startPrank(eve); + erc20.mint(eve, 1000); + erc20.approve(address(royaltyPolicyLAP), 100); + parentIpIds = new address[](1); + licenseTermsIds = new uint256[](1); + parentIpIds[0] = ipId1; + licenseTermsIds[0] = 2; + + licensingModule.registerDerivative(ipId7, parentIpIds, licenseTermsIds, address(piLicenseTemplate), ""); + + assertEq(licenseRegistry.hasIpAttachedLicenseTerms(ipId7, address(piLicenseTemplate), 2), true); + assertEq(licenseRegistry.getAttachedLicenseTermsCount(ipId7), 1); + assertEq(licenseRegistry.isDerivativeIp(ipId7), true); + assertEq(licenseRegistry.hasDerivativeIps(ipId7), false); + assertEq(licenseRegistry.hasDerivativeIps(ipId1), true); + assertEq(licenseRegistry.isDerivativeIp(ipId1), false); + assertEq(licenseRegistry.getDerivativeIpCount(ipId1), 4); + assertEq(licenseRegistry.getDerivativeIpCount(ipId7), 0); + assertEq(licenseRegistry.getDerivativeIp(ipId1, 3), ipId7); + assertEq(licenseRegistry.getParentIp(ipId7, 0), ipId1); + assertEq(licenseRegistry.getParentIpCount(ipId7), 1); + assertEq(licenseToken.totalMintedTokens(), 2); + assertEq(erc20.balanceOf(eve), 900); + vm.stopPrank(); + } +} diff --git a/test/foundry/mocks/module/MockLicenseTemplate.sol b/test/foundry/mocks/module/MockLicenseTemplate.sol new file mode 100644 index 000000000..974bd2ecd --- /dev/null +++ b/test/foundry/mocks/module/MockLicenseTemplate.sol @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity 0.8.23; + +import { BaseLicenseTemplateUpgradeable } from "contracts/modules/licensing/BaseLicenseTemplateUpgradeable.sol"; + +contract MockLicenseTemplate is BaseLicenseTemplateUpgradeable { + uint256 public licenseTermsCounter; + mapping(uint256 => bool) public licenseTerms; + + function registerLicenseTerms() external returns (uint256 id) { + id = licenseTermsCounter++; + licenseTerms[id] = true; + } + + /// @notice Checks if a license terms exists. + /// @param licenseTermsId The ID of the license terms. + /// @return True if the license terms exists, false otherwise. + function exists(uint256 licenseTermsId) external view override returns (bool) { + return licenseTerms[licenseTermsId]; + } + + /// @notice Verifies the minting of a license token. + /// @dev the function will be called by the LicensingModule when minting a license token to + /// verify the minting is whether allowed by the license terms. + /// @param licenseTermsId The ID of the license terms. + /// @param licensee The address of the licensee who will receive the license token. + /// @param licensorIpId The IP ID of the licensor who attached the license terms minting the license token. + /// @return True if the minting is verified, false otherwise. + function verifyMintLicenseToken( + uint256 licenseTermsId, + address licensee, + address licensorIpId, + uint256 amount + ) external override returns (bool) { + return true; + } + + /// @notice Verifies the registration of a derivative. + /// @dev This function is invoked by the LicensingModule during the registration of a derivative work + //// to ensure compliance with the parent IP's licensing terms. + /// It verifies whether the derivative's registration is permitted under those terms. + /// @param childIpId The IP ID of the derivative. + /// @param parentIpId The IP ID of the parent. + /// @param licenseTermsId The ID of the license terms. + /// @param licensee The address of the licensee. + /// @return True if the registration is verified, false otherwise. + function verifyRegisterDerivative( + address childIpId, + address parentIpId, + uint256 licenseTermsId, + address licensee + ) external override returns (bool) { + return true; + } + + /// @notice Verifies if the licenses are compatible. + /// @dev This function is called by the LicensingModule to verify license compatibility + /// when registering a derivative IP to multiple parent IPs. + /// It ensures that the licenses of all parent IPs are compatible with each other during the registration process. + /// @param licenseTermsIds The IDs of the license terms. + /// @return True if the licenses are compatible, false otherwise. + function verifyCompatibleLicenses(uint256[] calldata licenseTermsIds) external view override returns (bool) { + return true; + } + + /// @notice Verifies the registration of a derivative for all parent IPs. + /// @dev This function is called by the LicensingModule to verify licenses for registering a derivative IP + /// to multiple parent IPs. + /// the function will verify the derivative for each parent IP's license and + /// also verify all licenses are compatible. + /// @param childIpId The IP ID of the derivative. + /// @param parentIpIds The IP IDs of the parents. + /// @param licenseTermsIds The IDs of the license terms. + /// @param childIpOwner The address of the derivative IP owner. + /// @return True if the registration is verified, false otherwise. + function verifyRegisterDerivativeForAllParents( + address childIpId, + address[] calldata parentIpIds, + uint256[] calldata licenseTermsIds, + address childIpOwner + ) external override returns (bool) { + return true; + } + + /// @notice Returns the royalty policy of a license terms. + /// @param licenseTermsId The ID of the license terms. + /// @return royaltyPolicy The address of the royalty policy specified for the license terms. + /// @return royaltyData The data of the royalty policy. + /// @return mintingFee The fee for minting a license. + /// @return currency The address of the ERC20 token, used for minting license fee and royalties. + /// the currency token will used for pay for license token minting fee and royalties. + function getRoyaltyPolicy( + uint256 licenseTermsId + ) external view returns (address royaltyPolicy, bytes memory royaltyData, uint256 mintingFee, address currency) { + return (address(0), "", 0, address(0)); + } + + /// @notice Checks if a license terms is transferable. + /// @param licenseTermsId The ID of the license terms. + /// @return True if the license terms is transferable, false otherwise. + function isLicenseTransferable(uint256 licenseTermsId) external view override returns (bool) { + return true; + } + + /// @notice Returns the earliest expiration time among the given license terms. + /// @param start The start time. + /// @param licenseTermsIds The IDs of the license terms. + /// @return The earliest expiration time. + function getEarlierExpireTime( + uint256[] calldata licenseTermsIds, + uint256 start + ) external view override returns (uint256) { + return 0; + } + + /// @notice Returns the expiration time of a license terms. + /// @param start The start time. + /// @param licenseTermsId The ID of the license terms. + /// @return The expiration time. + function getExpireTime(uint256 licenseTermsId, uint256 start) external view returns (uint256) { + return 0; + } + + /// @notice Returns the total number of registered license terms. + /// @return The total number of registered license terms. + function totalRegisteredLicenseTerms() external view returns (uint256) { + return licenseTermsCounter; + } + + /// @notice checks the contract whether supports the given interface. + function supportsInterface( + bytes4 interfaceId + ) public view virtual override(BaseLicenseTemplateUpgradeable) returns (bool) { + return super.supportsInterface(interfaceId); + } + + /// @notice Converts the license terms to a JSON string which will be part of the metadata of license token. + /// @dev Must return OpenSea standard compliant metadata. + /// @param licenseTermsId The ID of the license terms. + /// @return The JSON string of the license terms, follow the OpenSea metadata standard. + function toJson(uint256 licenseTermsId) public view returns (string memory) { + return ""; + } +} diff --git a/test/foundry/modules/licensing/LicensingModule.t.sol b/test/foundry/modules/licensing/LicensingModule.t.sol index 9aeef8eab..e60f8ee87 100644 --- a/test/foundry/modules/licensing/LicensingModule.t.sol +++ b/test/foundry/modules/licensing/LicensingModule.t.sol @@ -5,9 +5,12 @@ pragma solidity 0.8.23; import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; // contracts -import { IIPAccount } from "../../../../contracts/interfaces/IIPAccount.sol"; import { Errors } from "../../../../contracts/lib/Errors.sol"; 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 { PILTerms } from "../../../../contracts/interfaces/modules/licensing/IPILicenseTemplate.sol"; // test import { MockERC721 } from "../../mocks/token/MockERC721.sol"; @@ -16,378 +19,1186 @@ import { BaseTest } from "../../utils/BaseTest.t.sol"; contract LicensingModuleTest is BaseTest { using Strings for *; - MockERC721 internal nft = new MockERC721("MockERC721"); + error ERC721NonexistentToken(uint256 tokenId); + + MockERC721 internal mockNft = new MockERC721("MockERC721"); MockERC721 internal gatedNftFoo = new MockERC721{ salt: bytes32(uint256(1)) }("GatedNftFoo"); MockERC721 internal gatedNftBar = new MockERC721{ salt: bytes32(uint256(2)) }("GatedNftBar"); - string public licenseUrl = "https://example.com/license"; address public ipId1; address public ipId2; address public ipId3; - address public ipOwner = address(0x100); // use static address, otherwise uri check fails because licensor changes - address public licenseHolder = address(0x101); + address public ipId5; + address public ipOwner1 = address(0x111); + address public ipOwner2 = address(0x222); + address public ipOwner3 = address(0x333); + address public ipOwner5 = address(0x444); + uint256 public tokenId1 = 1; + uint256 public tokenId2 = 2; + uint256 public tokenId3 = 3; + uint256 public tokenId5 = 5; - uint256 internal commRemixTermsId; - uint256 internal commUseTermsId; + address public licenseHolder = address(0x101); function setUp() public override { super.setUp(); - - vm.prank(u.admin); - royaltyModule.whitelistRoyaltyToken(address(0x123), true); - - commRemixTermsId = registerSelectedPILicenseTerms( - "commercial_remix", - PILFlavors.commercialRemix({ - mintingFee: 0, - commercialRevShare: 0, - royaltyPolicy: address(royaltyPolicyLAP), - currencyToken: address(0x123) - }) - ); - - commUseTermsId = registerSelectedPILicenseTerms( - "commercial_use", - PILFlavors.commercialUse({ - mintingFee: 0, - currencyToken: address(0x123), - royaltyPolicy: address(royaltyPolicyLAP) - }) - ); - // Create IPAccounts - nft.mintId(ipOwner, 1); - nft.mintId(ipOwner, 2); - nft.mintId(ipOwner, 3); + mockNft.mintId(ipOwner1, tokenId1); + mockNft.mintId(ipOwner2, tokenId2); + mockNft.mintId(ipOwner3, tokenId3); + mockNft.mintId(ipOwner5, tokenId5); - ipId1 = ipAccountRegistry.registerIpAccount(block.chainid, address(nft), 1); - ipId2 = ipAccountRegistry.registerIpAccount(block.chainid, address(nft), 2); - ipId3 = ipAccountRegistry.registerIpAccount(block.chainid, address(nft), 3); + ipId1 = ipAssetRegistry.register(address(mockNft), tokenId1); + ipId2 = ipAssetRegistry.register(address(mockNft), tokenId2); + ipId3 = ipAssetRegistry.register(address(mockNft), tokenId3); + ipId5 = ipAssetRegistry.register(address(mockNft), tokenId5); vm.label(ipId1, "IPAccount1"); vm.label(ipId2, "IPAccount2"); vm.label(ipId3, "IPAccount3"); - - useMock_RoyaltyPolicyLAP(); + vm.label(ipId5, "IPAccount5"); } - function test_LicensingModule_attachLicenseTerms() public { - vm.prank(ipOwner); - licensingModule.attachLicenseTerms(ipId1, address(pilTemplate), commRemixTermsId); - assertEq(commRemixTermsId, 1, "policyId not 1"); - assertTrue(licenseRegistry.exists(address(pilTemplate), commRemixTermsId)); - assertTrue(licenseRegistry.hasIpAttachedLicenseTerms(ipId1, address(pilTemplate), commRemixTermsId)); + function test_LicensingModule_attachLicenseTerms_attachOneLicenseToOneIP() public { + uint256 termsId = pilTemplate.registerLicenseTerms(PILFlavors.defaultValuesLicenseTerms()); + vm.expectEmit(); + emit ILicensingModule.LicenseTermsAttached(ipOwner1, ipId1, address(pilTemplate), termsId); + vm.prank(ipOwner1); + licensingModule.attachLicenseTerms(ipId1, address(pilTemplate), termsId); + (address licenseTemplate, uint256 licenseTermsId) = licenseRegistry.getAttachedLicenseTerms(ipId1, 0); + assertEq(licenseTemplate, address(pilTemplate)); + assertEq(licenseTermsId, termsId); + assertTrue(licenseRegistry.hasIpAttachedLicenseTerms(ipId1, address(pilTemplate), termsId)); + assertEq(licenseRegistry.getAttachedLicenseTermsCount(ipId1), 1); + assertEq(licenseRegistry.getDerivativeIpCount(ipId1), 0); + assertEq(licenseRegistry.getParentIpCount(ipId1), 0); + assertFalse(licenseRegistry.getMintingLicenseConfig(ipId1, address(pilTemplate), termsId).isSet); + assertEq(licenseRegistry.getExpireTime(ipId1), 0); assertFalse(licenseRegistry.isDerivativeIp(ipId1)); + assertTrue(licenseRegistry.exists(address(pilTemplate), termsId)); } - function test_LicensingModule_attachLicenseTerms_sameReusePolicyId() public { - address licenseTemplate; - uint256 licenseTermsId; + function test_LicensingModule_attachLicenseTerms_sameLicenseAttachMultipleIP() public { + uint256 termsId = pilTemplate.registerLicenseTerms(PILFlavors.defaultValuesLicenseTerms()); - vm.startPrank(ipOwner); + vm.expectEmit(); + emit ILicensingModule.LicenseTermsAttached(ipOwner1, ipId1, address(pilTemplate), termsId); + vm.prank(ipOwner1); + licensingModule.attachLicenseTerms(ipId1, address(pilTemplate), termsId); - licensingModule.attachLicenseTerms(ipId1, address(pilTemplate), commRemixTermsId); - (licenseTemplate, licenseTermsId) = licenseRegistry.getAttachedLicenseTerms(ipId1, 0); - assertTrue(licenseRegistry.exists(address(pilTemplate), commRemixTermsId)); - assertTrue(licenseRegistry.hasIpAttachedLicenseTerms(ipId1, address(pilTemplate), commRemixTermsId)); + (address licenseTemplate, uint256 licenseTermsId) = licenseRegistry.getAttachedLicenseTerms(ipId1, 0); assertEq(licenseTemplate, address(pilTemplate)); - assertEq(licenseTermsId, commRemixTermsId); + assertEq(licenseTermsId, termsId); + assertTrue(licenseRegistry.hasIpAttachedLicenseTerms(ipId1, address(pilTemplate), termsId)); + assertEq(licenseRegistry.getAttachedLicenseTermsCount(ipId1), 1); + assertEq(licenseRegistry.getDerivativeIpCount(ipId1), 0); + assertEq(licenseRegistry.getParentIpCount(ipId1), 0); + assertFalse(licenseRegistry.getMintingLicenseConfig(ipId1, address(pilTemplate), termsId).isSet); + assertEq(licenseRegistry.getExpireTime(ipId1), 0); + assertFalse(licenseRegistry.isDerivativeIp(ipId1)); + assertTrue(licenseRegistry.exists(address(pilTemplate), termsId)); - licensingModule.attachLicenseTerms(ipId2, address(pilTemplate), commRemixTermsId); + vm.expectEmit(); + emit ILicensingModule.LicenseTermsAttached(ipOwner2, ipId2, address(pilTemplate), termsId); + vm.prank(ipOwner2); + licensingModule.attachLicenseTerms(ipId2, address(pilTemplate), termsId); (licenseTemplate, licenseTermsId) = licenseRegistry.getAttachedLicenseTerms(ipId2, 0); - assertTrue(licenseRegistry.exists(address(pilTemplate), commRemixTermsId)); - assertTrue(licenseRegistry.hasIpAttachedLicenseTerms(ipId2, address(pilTemplate), commRemixTermsId)); assertEq(licenseTemplate, address(pilTemplate)); - assertEq(licenseTermsId, commRemixTermsId); + assertEq(licenseTermsId, termsId); + assertTrue(licenseRegistry.hasIpAttachedLicenseTerms(ipId2, address(pilTemplate), termsId)); + assertEq(licenseRegistry.getAttachedLicenseTermsCount(ipId2), 1); + assertEq(licenseRegistry.getDerivativeIpCount(ipId2), 0); + assertEq(licenseRegistry.getParentIpCount(ipId2), 0); + assertFalse(licenseRegistry.getMintingLicenseConfig(ipId2, address(pilTemplate), termsId).isSet); + assertEq(licenseRegistry.getExpireTime(ipId2), 0); + assertFalse(licenseRegistry.isDerivativeIp(ipId2)); + assertTrue(licenseRegistry.exists(address(pilTemplate), termsId)); } - function test_LicensingModule_attachLicenseTerms_TwoPoliciesToOneIpId() public { - address licenseTemplate; - uint256 licenseTermsId; + function test_LicensingModule_attachLicenseTerms_DifferentLicensesAttachToSameIP() public { + uint256 termsId1 = pilTemplate.registerLicenseTerms(PILFlavors.defaultValuesLicenseTerms()); + vm.expectEmit(); + emit ILicensingModule.LicenseTermsAttached(ipOwner1, ipId1, address(pilTemplate), termsId1); + vm.prank(ipOwner1); + licensingModule.attachLicenseTerms(ipId1, address(pilTemplate), termsId1); + (address licenseTemplate1, uint256 licenseTermsId1) = licenseRegistry.getAttachedLicenseTerms(ipId1, 0); + assertEq(licenseTemplate1, address(pilTemplate)); + assertEq(licenseTermsId1, termsId1); + assertTrue(licenseRegistry.hasIpAttachedLicenseTerms(ipId1, address(pilTemplate), termsId1)); + assertEq(licenseRegistry.getAttachedLicenseTermsCount(ipId1), 1); + assertEq(licenseRegistry.getDerivativeIpCount(ipId1), 0); + assertEq(licenseRegistry.getParentIpCount(ipId1), 0); + assertFalse(licenseRegistry.getMintingLicenseConfig(ipId1, address(pilTemplate), termsId1).isSet); + assertEq(licenseRegistry.getExpireTime(ipId1), 0); + assertFalse(licenseRegistry.isDerivativeIp(ipId1)); + assertTrue(licenseRegistry.exists(address(pilTemplate), termsId1)); + + uint256 termsId2 = pilTemplate.registerLicenseTerms(PILFlavors.nonCommercialSocialRemixing()); + vm.expectEmit(); + emit ILicensingModule.LicenseTermsAttached(ipOwner1, ipId1, address(pilTemplate), termsId2); + vm.prank(ipOwner1); + licensingModule.attachLicenseTerms(ipId1, address(pilTemplate), termsId2); + (address licenseTemplate2, uint256 licenseTermsId2) = licenseRegistry.getAttachedLicenseTerms(ipId1, 1); + assertEq(licenseTemplate2, address(pilTemplate)); + assertEq(licenseTermsId2, termsId2); + assertTrue(licenseRegistry.hasIpAttachedLicenseTerms(ipId1, address(pilTemplate), termsId2)); + assertEq(licenseRegistry.getAttachedLicenseTermsCount(ipId1), 2); + assertEq(licenseRegistry.getDerivativeIpCount(ipId1), 0); + assertEq(licenseRegistry.getParentIpCount(ipId1), 0); + assertFalse(licenseRegistry.getMintingLicenseConfig(ipId1, address(pilTemplate), termsId2).isSet); + assertEq(licenseRegistry.getExpireTime(ipId1), 0); + assertFalse(licenseRegistry.isDerivativeIp(ipId1)); + assertTrue(licenseRegistry.exists(address(pilTemplate), termsId2)); + } - vm.startPrank(ipOwner); + function test_LicensingModule_attachLicenseTerms_revert_licenseNotExist() public { + uint256 nonExistTermsId = 9999; + assertFalse(licenseRegistry.exists(address(pilTemplate), nonExistTermsId)); - licensingModule.attachLicenseTerms(ipId1, address(pilTemplate), commRemixTermsId); - (licenseTemplate, licenseTermsId) = licenseRegistry.getAttachedLicenseTerms(ipId1, 0); - assertTrue(licenseRegistry.hasIpAttachedLicenseTerms(ipId1, address(pilTemplate), commRemixTermsId)); - assertEq(licenseTemplate, address(pilTemplate)); - assertEq(licenseTermsId, commRemixTermsId); - assertFalse(licenseRegistry.isDerivativeIp(ipId1)); + vm.expectRevert( + abi.encodeWithSelector( + Errors.LicensingModule__LicenseTermsNotFound.selector, + address(pilTemplate), + nonExistTermsId + ) + ); + vm.prank(ipOwner1); + licensingModule.attachLicenseTerms(ipId1, address(pilTemplate), nonExistTermsId); + } + + function test_LicensingModule_attachLicenseTerms_revert_IpExpired() public { + vm.prank(u.admin); + royaltyModule.whitelistRoyaltyToken(address(0x123), true); + PILTerms memory terms = PILTerms({ + transferable: true, + royaltyPolicy: address(royaltyPolicyLAP), + mintingFee: 0, + expiration: 10 days, + commercialUse: true, + commercialAttribution: true, + commercializerChecker: address(0), + commercializerCheckerData: "", + commercialRevShare: 0, + commercialRevCelling: 0, + derivativesAllowed: true, + derivativesAttribution: true, + derivativesApproval: false, + derivativesReciprocal: true, + derivativeRevCelling: 0, + currency: address(0x123) + }); - licensingModule.attachLicenseTerms(ipId1, address(pilTemplate), commUseTermsId); - (licenseTemplate, licenseTermsId) = licenseRegistry.getAttachedLicenseTerms(ipId1, 1); - assertTrue(licenseRegistry.hasIpAttachedLicenseTerms(ipId1, address(pilTemplate), commUseTermsId)); + uint256 termsId = pilTemplate.registerLicenseTerms(terms); + vm.prank(ipOwner1); + licensingModule.attachLicenseTerms(ipId1, address(pilTemplate), termsId); + + uint256 lcTokenId = licensingModule.mintLicenseTokens({ + licensorIpId: ipId1, + licenseTemplate: address(pilTemplate), + licenseTermsId: termsId, + amount: 1, + receiver: ipOwner2, + royaltyContext: "" + }); + assertEq(licenseToken.ownerOf(lcTokenId), ipOwner2); + assertEq(licenseToken.getLicenseTermsId(lcTokenId), termsId); + assertEq(licenseToken.getLicenseTemplate(lcTokenId), address(pilTemplate)); + assertEq(licenseToken.getLicensorIpId(lcTokenId), ipId1); + assertEq(licenseToken.getExpirationTime(lcTokenId), block.timestamp + 10 days); + assertEq(licenseToken.totalMintedTokens(), 1); + assertEq(licenseToken.totalSupply(), 1); + assertEq(licenseToken.balanceOf(ipOwner2), 1); + + uint256[] memory licenseTokens = new uint256[](1); + licenseTokens[0] = lcTokenId; + vm.prank(ipOwner2); + licensingModule.registerDerivativeWithLicenseTokens(ipId2, licenseTokens, ""); + + assertEq(licenseRegistry.hasIpAttachedLicenseTerms(ipId2, address(pilTemplate), termsId), true); + assertEq(licenseRegistry.getAttachedLicenseTermsCount(ipId2), 1); + assertEq(licenseRegistry.isDerivativeIp(ipId2), true); + assertEq(licenseRegistry.hasDerivativeIps(ipId2), false); + assertEq(licenseRegistry.hasDerivativeIps(ipId1), true); + assertEq(licenseRegistry.isDerivativeIp(ipId1), false); + assertEq(licenseRegistry.getDerivativeIpCount(ipId1), 1); + assertEq(licenseRegistry.getDerivativeIpCount(ipId2), 0); + assertEq(licenseRegistry.getDerivativeIp(ipId1, 0), ipId2); + assertEq(licenseRegistry.getParentIp(ipId2, 0), ipId1); + assertEq(licenseRegistry.getParentIpCount(ipId2), 1); + assertEq(licenseToken.totalSupply(), 0); + assertEq(licenseToken.totalMintedTokens(), 1); + vm.expectRevert(abi.encodeWithSelector(ERC721NonexistentToken.selector, lcTokenId)); + licenseToken.ownerOf(lcTokenId); + + (address licenseTemplate, uint256 licenseTermsId) = licenseRegistry.getAttachedLicenseTerms(ipId2, 0); assertEq(licenseTemplate, address(pilTemplate)); - assertEq(licenseTermsId, commUseTermsId); - assertFalse(licenseRegistry.isDerivativeIp(ipId1)); + assertEq(licenseTermsId, termsId); + + licensingModule.mintLicenseTokens({ + licensorIpId: ipId1, + licenseTemplate: address(pilTemplate), + licenseTermsId: termsId, + amount: 1, + receiver: address(ipOwner2), + royaltyContext: "" + }); + + vm.warp(11 days); + uint256 anotherTermsId = pilTemplate.registerLicenseTerms(PILFlavors.defaultValuesLicenseTerms()); + vm.expectRevert(abi.encodeWithSelector(Errors.LicenseRegistry__IpExpired.selector, ipId2)); + vm.prank(ipOwner2); + licensingModule.attachLicenseTerms(ipId2, address(pilTemplate), anotherTermsId); } - function test_LicensingModule_attachLicenseTerms_revert_policyNotFound() public { - uint256 undefinedPILTermsId = 111222333222111; - assertFalse(licenseRegistry.exists(address(pilTemplate), undefinedPILTermsId)); + function test_LicensingModule_attachLicenseTerms_revert_attachSameLicenseToIpTwice() public { + uint256 termsId = pilTemplate.registerLicenseTerms(PILFlavors.defaultValuesLicenseTerms()); + vm.prank(ipOwner1); + licensingModule.attachLicenseTerms(ipId1, address(pilTemplate), termsId); + vm.expectRevert( + abi.encodeWithSelector( + Errors.LicenseRegistry__LicenseTermsAlreadyAttached.selector, + ipId1, + address(pilTemplate), + termsId + ) + ); + vm.prank(ipOwner1); + licensingModule.attachLicenseTerms(ipId1, address(pilTemplate), termsId); + } - vm.expectRevert(abi.encodeWithSelector(Errors.LicenseRegistry__IndexOutOfBounds.selector, ipId1, 0, 0)); - licenseRegistry.getAttachedLicenseTerms(ipId1, 0); + function test_LicensingModule_attachLicenseTerms_revert_attachLicenseDifferentTemplate() public { + uint256 termsId = pilTemplate.registerLicenseTerms(PILFlavors.defaultValuesLicenseTerms()); + vm.prank(ipOwner1); + licensingModule.attachLicenseTerms(ipId1, address(pilTemplate), termsId); + MockLicenseTemplate mockLicenseTemplate = new MockLicenseTemplate(); + vm.prank(admin); + licenseRegistry.registerLicenseTemplate(address(mockLicenseTemplate)); + uint256 mockTermsId = mockLicenseTemplate.registerLicenseTerms(); vm.expectRevert( abi.encodeWithSelector( - Errors.LicensingModule__LicenseTermsNotFound.selector, + Errors.LicenseRegistry__UnmatchedLicenseTemplate.selector, + ipId1, address(pilTemplate), - undefinedPILTermsId + address(mockLicenseTemplate) ) ); - vm.prank(ipOwner); - licensingModule.attachLicenseTerms(ipId1, address(pilTemplate), undefinedPILTermsId); + vm.prank(ipOwner1); + licensingModule.attachLicenseTerms(ipId1, address(mockLicenseTemplate), mockTermsId); } - function test_LicensingModule_attachLicenseTerms_revert_policyAlreadySetForIpId() public { - address licenseTemplate; - uint256 licenseTermsId; + function test_LicensingModule_attachLicenseTerms_revert_Unregistered_LicenseTemplate() public { + MockLicenseTemplate mockLicenseTemplate = new MockLicenseTemplate(); + uint256 mockTermsId = mockLicenseTemplate.registerLicenseTerms(); + vm.expectRevert( + abi.encodeWithSelector( + Errors.LicensingModule__LicenseTermsNotFound.selector, + address(mockLicenseTemplate), + mockTermsId + ) + ); + vm.prank(ipOwner1); + licensingModule.attachLicenseTerms(ipId1, address(mockLicenseTemplate), mockTermsId); + } - vm.startPrank(ipOwner); + function test_LicensingModule_attachLicenseTerms_revert_NonExists_LicenseTerms() public { + uint256 nonExistsTermsId = 9999; + vm.expectRevert( + abi.encodeWithSelector( + Errors.LicensingModule__LicenseTermsNotFound.selector, + address(pilTemplate), + nonExistsTermsId + ) + ); + vm.prank(ipOwner1); + licensingModule.attachLicenseTerms(ipId1, address(pilTemplate), nonExistsTermsId); + } - licensingModule.attachLicenseTerms(ipId1, address(pilTemplate), commRemixTermsId); - (licenseTemplate, licenseTermsId) = licenseRegistry.getAttachedLicenseTerms(ipId1, 0); - assertTrue(licenseRegistry.exists(address(pilTemplate), commRemixTermsId)); - assertTrue(licenseRegistry.hasIpAttachedLicenseTerms(ipId1, address(pilTemplate), commRemixTermsId)); - assertEq(licenseTemplate, address(pilTemplate)); - assertEq(licenseTermsId, commRemixTermsId); + function test_LicensingModule_mintLicenseTokens_singleToken() public { + uint256 termsId = pilTemplate.registerLicenseTerms(PILFlavors.defaultValuesLicenseTerms()); + vm.prank(ipOwner1); + licensingModule.attachLicenseTerms(ipId1, address(pilTemplate), termsId); - // TODO: This should revert! - // vm.expectRevert(Errors.LicensingModule__PolicyAlreadySetForIpId.selector); - licensingModule.attachLicenseTerms(ipId1, address(pilTemplate), commRemixTermsId); + address receiver = address(0x111); + vm.expectEmit(); + emit ILicensingModule.LicenseTokensMinted(address(this), ipId1, address(pilTemplate), termsId, 1, receiver, 0); + uint256 lcTokenId = licensingModule.mintLicenseTokens({ + licensorIpId: ipId1, + licenseTemplate: address(pilTemplate), + licenseTermsId: termsId, + amount: 1, + receiver: receiver, + royaltyContext: "" + }); + assertEq(licenseToken.ownerOf(lcTokenId), receiver); + assertEq(licenseToken.getLicenseTermsId(lcTokenId), termsId); + assertEq(licenseToken.getLicenseTemplate(lcTokenId), address(pilTemplate)); + assertEq(licenseToken.getLicensorIpId(lcTokenId), ipId1); + assertEq(licenseToken.getExpirationTime(lcTokenId), 0); + assertEq(licenseToken.tokenOfOwnerByIndex(receiver, 0), lcTokenId); + assertEq(licenseToken.totalMintedTokens(), 1); + assertEq(licenseToken.totalSupply(), 1); + assertEq(licenseToken.balanceOf(receiver), 1); } - function test_LicensingModule_mintLicenseTokens() public { - vm.prank(ipOwner); - licensingModule.attachLicenseTerms(ipId1, address(pilTemplate), commRemixTermsId); - assertTrue(licenseRegistry.hasIpAttachedLicenseTerms(ipId1, address(pilTemplate), commRemixTermsId)); + function test_LicensingModule_mintLicenseTokens_mintMultipleTokens() public { + uint256 termsId = pilTemplate.registerLicenseTerms(PILFlavors.defaultValuesLicenseTerms()); + vm.prank(ipOwner1); + licensingModule.attachLicenseTerms(ipId1, address(pilTemplate), termsId); - uint256 startLicenseId = attachAndMint_PILCommRemix_LicenseTokens({ - ipId: ipId1, + address receiver = address(0x111); + vm.expectEmit(); + emit ILicensingModule.LicenseTokensMinted(address(this), ipId1, address(pilTemplate), termsId, 2, receiver, 0); + uint256 firstTokenId = licensingModule.mintLicenseTokens({ + licensorIpId: ipId1, + licenseTemplate: address(pilTemplate), + licenseTermsId: termsId, amount: 2, - receiver: licenseHolder + receiver: receiver, + royaltyContext: "" }); - assertEq(licenseToken.balanceOf(licenseHolder), 2); - assertEq(licenseToken.tokenOfOwnerByIndex(licenseHolder, 0), startLicenseId); - assertEq(licenseToken.tokenOfOwnerByIndex(licenseHolder, 1), startLicenseId + 1); + assertEq(licenseToken.ownerOf(firstTokenId), receiver); + assertEq(licenseToken.getLicenseTermsId(firstTokenId), termsId); + assertEq(licenseToken.getLicenseTemplate(firstTokenId), address(pilTemplate)); + assertEq(licenseToken.getLicensorIpId(firstTokenId), ipId1); + assertEq(licenseToken.getExpirationTime(firstTokenId), 0); + assertEq(licenseToken.tokenOfOwnerByIndex(receiver, 0), firstTokenId); + + uint256 secondTokenId = firstTokenId + 1; + + assertEq(licenseToken.ownerOf(secondTokenId), receiver); + assertEq(licenseToken.getLicenseTermsId(secondTokenId), termsId); + assertEq(licenseToken.getLicenseTemplate(secondTokenId), address(pilTemplate)); + assertEq(licenseToken.getLicensorIpId(secondTokenId), ipId1); + assertEq(licenseToken.getExpirationTime(secondTokenId), 0); + assertEq(licenseToken.tokenOfOwnerByIndex(receiver, 1), secondTokenId); + assertEq(licenseToken.totalMintedTokens(), 2); + assertEq(licenseToken.totalSupply(), 2); + assertEq(licenseToken.balanceOf(receiver), 2); } - function test_LIcensingModule_mintLicenseTokens_revert_inputValidations() public {} + function test_LicensingModule_mintLicenseTokens_mintMultipleTimes() public { + uint256 termsId = pilTemplate.registerLicenseTerms(PILFlavors.defaultValuesLicenseTerms()); + vm.prank(ipOwner1); + licensingModule.attachLicenseTerms(ipId1, address(pilTemplate), termsId); - function test_LicensingModule_mintLicenseTokens_revert_callerNotLicensorAndIpIdHasNoPolicy() public {} + address receiver = address(0x111); + for (uint i = 0; i < 10; i++) { + vm.expectEmit(); + emit ILicensingModule.LicenseTokensMinted( + address(this), + ipId1, + address(pilTemplate), + termsId, + 1, + receiver, + i + ); + uint256 tokenId = licensingModule.mintLicenseTokens({ + licensorIpId: ipId1, + licenseTemplate: address(pilTemplate), + licenseTermsId: termsId, + amount: 1, + receiver: receiver, + royaltyContext: "" + }); + assertEq(licenseToken.ownerOf(tokenId), receiver); + assertEq(licenseToken.getLicenseTermsId(tokenId), termsId); + assertEq(licenseToken.getLicenseTemplate(tokenId), address(pilTemplate)); + assertEq(licenseToken.getLicensorIpId(tokenId), ipId1); + assertEq(licenseToken.getExpirationTime(tokenId), 0); + assertEq(licenseToken.tokenOfOwnerByIndex(receiver, i), tokenId); + assertEq(licenseToken.totalMintedTokens(), i + 1); + assertEq(licenseToken.totalSupply(), i + 1); + assertEq(licenseToken.balanceOf(receiver), i + 1); + } + } + + function test_LicensingModule_mintLicenseTokens_ExpirationTime() public { + vm.prank(u.admin); + royaltyModule.whitelistRoyaltyToken(address(0x123), true); + PILTerms memory terms = PILTerms({ + transferable: true, + royaltyPolicy: address(royaltyPolicyLAP), + mintingFee: 0, + expiration: 10 days, + commercialUse: true, + commercialAttribution: true, + commercializerChecker: address(0), + commercializerCheckerData: "", + commercialRevShare: 0, + commercialRevCelling: 0, + derivativesAllowed: true, + derivativesAttribution: true, + derivativesApproval: false, + derivativesReciprocal: true, + derivativeRevCelling: 0, + currency: address(0x123) + }); - function test_LicensingModule_mintLicenseTokens_ipIdHasNoPolicyButCallerIsLicensor() public { - vm.prank(IIPAccount(payable(ipId1)).owner()); - uint256 startLicenseId = licensingModule.mintLicenseTokens({ + uint256 termsId = pilTemplate.registerLicenseTerms(terms); + vm.prank(ipOwner1); + licensingModule.attachLicenseTerms(ipId1, address(pilTemplate), termsId); + + uint256 lcTokenId = licensingModule.mintLicenseTokens({ licensorIpId: ipId1, licenseTemplate: address(pilTemplate), - licenseTermsId: commRemixTermsId, - amount: 2, - receiver: ipId1, + licenseTermsId: termsId, + amount: 1, + receiver: ipOwner2, royaltyContext: "" }); - assertEq(licenseToken.balanceOf(ipId1), 2); - assertEq(licenseToken.tokenOfOwnerByIndex(ipId1, 0), startLicenseId); - assertEq(licenseToken.tokenOfOwnerByIndex(ipId1, 1), startLicenseId + 1); - - // Licensor (IP Account owner) calls via IP Account execute - // The returned license ID (from decoding `result`) should be the same as above, as we're not creating a new - // license, but rather minting an existing one (existing ID, minted above). - vm.prank(IIPAccount(payable(ipId1)).owner()); - bytes memory result = IIPAccount(payable(ipId1)).execute( - address(licensingModule), - 0, - abi.encodeWithSignature( - "mintLicenseTokens(address,address,uint256,uint256,address,bytes)", + assertEq(licenseToken.ownerOf(lcTokenId), ipOwner2); + assertEq(licenseToken.getLicenseTermsId(lcTokenId), termsId); + assertEq(licenseToken.getLicenseTemplate(lcTokenId), address(pilTemplate)); + assertEq(licenseToken.getLicensorIpId(lcTokenId), ipId1); + assertEq(licenseToken.getExpirationTime(lcTokenId), block.timestamp + 10 days); + assertEq(licenseToken.totalMintedTokens(), 1); + assertEq(licenseToken.totalSupply(), 1); + assertEq(licenseToken.balanceOf(ipOwner2), 1); + } + + function test_LicensingModule_mintLicenseTokens_revert_invalidInputs() public { + uint256 termsId = pilTemplate.registerLicenseTerms(PILFlavors.defaultValuesLicenseTerms()); + vm.prank(ipOwner1); + licensingModule.attachLicenseTerms(ipId1, address(pilTemplate), termsId); + + address receiver = address(0x111); + vm.expectRevert(Errors.LicensingModule__MintAmountZero.selector); + licensingModule.mintLicenseTokens({ + licensorIpId: ipId1, + licenseTemplate: address(pilTemplate), + licenseTermsId: termsId, + amount: 0, + receiver: receiver, + royaltyContext: "" + }); + + vm.expectRevert(Errors.LicensingModule__ReceiverZeroAddress.selector); + licensingModule.mintLicenseTokens({ + licensorIpId: ipId1, + licenseTemplate: address(pilTemplate), + licenseTermsId: termsId, + amount: 1, + receiver: address(0), + royaltyContext: "" + }); + } + + function test_LicensingModule_mintLicenseTokens_revert_NonIpOwnerMintNotAttachedLicense() public { + uint256 termsId = pilTemplate.registerLicenseTerms(PILFlavors.defaultValuesLicenseTerms()); + address receiver = address(0x111); + vm.expectRevert( + abi.encodeWithSelector( + Errors.LicenseRegistry__LicensorIpHasNoLicenseTerms.selector, ipId1, address(pilTemplate), - commRemixTermsId, - 2, - ipId1, - "" + termsId ) ); - assertEq(startLicenseId + 2, abi.decode(result, (uint256))); - assertEq(licenseToken.balanceOf(ipId1), 4); - assertEq(licenseToken.tokenOfOwnerByIndex(ipId1, 2), startLicenseId + 2); - assertEq(licenseToken.tokenOfOwnerByIndex(ipId1, 3), startLicenseId + 3); - - // IP Account calls directly - vm.prank(ipId1); - startLicenseId = licensingModule.mintLicenseTokens({ + licensingModule.mintLicenseTokens({ + licensorIpId: ipId1, + licenseTemplate: address(pilTemplate), + licenseTermsId: termsId, + amount: 1, + receiver: receiver, + royaltyContext: "" + }); + } + + function test_LicensingModule_mintLicenseTokens_revert_IpOwnerMintNonExistsLicense() public { + address receiver = address(0x111); + vm.expectRevert( + abi.encodeWithSelector(Errors.LicenseRegistry__LicenseTermsNotExists.selector, address(pilTemplate), 9999) + ); + vm.prank(ipOwner1); + licensingModule.mintLicenseTokens({ licensorIpId: ipId1, licenseTemplate: address(pilTemplate), - licenseTermsId: commUseTermsId, // different selected license terms + licenseTermsId: 9999, amount: 1, - receiver: ipId1, + receiver: receiver, royaltyContext: "" }); - assertEq(licenseToken.balanceOf(ipId1), 5); - assertEq(licenseToken.tokenOfOwnerByIndex(ipId1, 4), startLicenseId); + } + + function test_LicensingModule_mintLicenseTokens_IpOwnerMintNotAttachedLicense() public { + uint256 termsId = pilTemplate.registerLicenseTerms(PILFlavors.defaultValuesLicenseTerms()); + address receiver = address(0x111); + vm.prank(ipOwner1); + uint256 lcTokenId = licensingModule.mintLicenseTokens({ + licensorIpId: ipId1, + licenseTemplate: address(pilTemplate), + licenseTermsId: termsId, + amount: 1, + receiver: receiver, + royaltyContext: "" + }); + assertEq(licenseToken.ownerOf(lcTokenId), receiver); + assertEq(licenseToken.getLicenseTermsId(lcTokenId), termsId); + assertEq(licenseToken.getLicenseTemplate(lcTokenId), address(pilTemplate)); + assertEq(licenseToken.getLicensorIpId(lcTokenId), ipId1); + assertEq(licenseToken.getExpirationTime(lcTokenId), 0); + assertEq(licenseToken.tokenOfOwnerByIndex(receiver, 0), lcTokenId); + assertEq(licenseToken.totalMintedTokens(), 1); + assertEq(licenseToken.totalSupply(), 1); + assertEq(licenseToken.balanceOf(receiver), 1); } function test_LicensingModule_registerDerivativeWithLicenseTokens_singleParent() public { - uint256 startLicenseId = attachAndMint_PILCommRemix_LicenseTokens({ - ipId: ipId1, - amount: 2, - receiver: licenseHolder + uint256 termsId = pilTemplate.registerLicenseTerms(PILFlavors.nonCommercialSocialRemixing()); + vm.prank(ipOwner1); + licensingModule.attachLicenseTerms(ipId1, address(pilTemplate), termsId); + + uint256 lcTokenId = licensingModule.mintLicenseTokens({ + licensorIpId: ipId1, + licenseTemplate: address(pilTemplate), + licenseTermsId: termsId, + amount: 1, + receiver: ipOwner2, + royaltyContext: "" }); - uint256 endLicenseId = startLicenseId + 1; + assertEq(licenseToken.ownerOf(lcTokenId), ipOwner2); + assertEq(licenseToken.getLicenseTermsId(lcTokenId), termsId); + assertEq(licenseToken.getLicenseTemplate(lcTokenId), address(pilTemplate)); + assertEq(licenseToken.getLicensorIpId(lcTokenId), ipId1); + assertEq(licenseToken.getExpirationTime(lcTokenId), 0); + assertEq(licenseToken.totalMintedTokens(), 1); + assertEq(licenseToken.totalSupply(), 1); + assertEq(licenseToken.balanceOf(ipOwner2), 1); + + uint256[] memory licenseTokens = new uint256[](1); + licenseTokens[0] = lcTokenId; + vm.prank(ipOwner2); + licensingModule.registerDerivativeWithLicenseTokens(ipId2, licenseTokens, ""); + + assertEq(licenseRegistry.hasIpAttachedLicenseTerms(ipId2, address(pilTemplate), termsId), true); + assertEq(licenseRegistry.getAttachedLicenseTermsCount(ipId2), 1); + assertEq(licenseRegistry.isDerivativeIp(ipId2), true); + assertEq(licenseRegistry.hasDerivativeIps(ipId2), false); + assertEq(licenseRegistry.hasDerivativeIps(ipId1), true); + assertEq(licenseRegistry.isDerivativeIp(ipId1), false); + assertEq(licenseRegistry.getDerivativeIpCount(ipId1), 1); + assertEq(licenseRegistry.getDerivativeIpCount(ipId2), 0); + assertEq(licenseRegistry.getDerivativeIp(ipId1, 0), ipId2); + assertEq(licenseRegistry.getParentIp(ipId2, 0), ipId1); + assertEq(licenseRegistry.getParentIpCount(ipId2), 1); + assertEq(licenseToken.totalSupply(), 0); + assertEq(licenseToken.totalMintedTokens(), 1); + vm.expectRevert(abi.encodeWithSelector(ERC721NonexistentToken.selector, lcTokenId)); + licenseToken.ownerOf(lcTokenId); - vm.prank(licenseHolder); - licenseToken.transferFrom(licenseHolder, ipOwner, endLicenseId); - assertEq(licenseToken.balanceOf(licenseHolder), 1, "not transferred"); - assertEq(licenseToken.ownerOf(startLicenseId), licenseHolder); - assertEq(licenseToken.ownerOf(endLicenseId), ipOwner); + (address licenseTemplate, uint256 licenseTermsId) = licenseRegistry.getAttachedLicenseTerms(ipId2, 0); + assertEq(licenseTemplate, address(pilTemplate)); + assertEq(licenseTermsId, termsId); + } + + function test_LicensingModule_registerDerivativeWithLicenseTokens_twoParents() public { + uint256 termsId = pilTemplate.registerLicenseTerms(PILFlavors.nonCommercialSocialRemixing()); + vm.prank(ipOwner1); + licensingModule.attachLicenseTerms(ipId1, address(pilTemplate), termsId); + vm.prank(ipOwner2); + licensingModule.attachLicenseTerms(ipId2, address(pilTemplate), termsId); - uint256[] memory licenseIds = new uint256[](1); - licenseIds[0] = endLicenseId; + uint256 lcTokenId1 = licensingModule.mintLicenseTokens({ + licensorIpId: ipId1, + licenseTemplate: address(pilTemplate), + licenseTermsId: termsId, + amount: 1, + receiver: ipOwner3, + royaltyContext: "" + }); - vm.prank(ipOwner); - licensingModule.registerDerivativeWithLicenseTokens(ipId2, licenseIds, ""); + uint256 lcTokenId2 = licensingModule.mintLicenseTokens({ + licensorIpId: ipId2, + licenseTemplate: address(pilTemplate), + licenseTermsId: termsId, + amount: 1, + receiver: ipOwner3, + royaltyContext: "" + }); - assertEq(licenseToken.balanceOf(ipOwner), 0, "not burnt"); - assertTrue(licenseRegistry.isDerivativeIp(ipId2)); - assertTrue(licenseRegistry.hasDerivativeIps(ipId1)); - assertEq(licenseRegistry.getParentIpCount(ipId2), 1); + assertEq(licenseToken.ownerOf(lcTokenId1), ipOwner3); + assertEq(licenseToken.getLicenseTermsId(lcTokenId1), termsId); + assertEq(licenseToken.getLicenseTemplate(lcTokenId1), address(pilTemplate)); + assertEq(licenseToken.getLicensorIpId(lcTokenId1), ipId1); + assertEq(licenseToken.getExpirationTime(lcTokenId1), 0); + + assertEq(licenseToken.ownerOf(lcTokenId2), ipOwner3); + assertEq(licenseToken.getLicenseTermsId(lcTokenId2), termsId); + assertEq(licenseToken.getLicenseTemplate(lcTokenId2), address(pilTemplate)); + assertEq(licenseToken.getLicensorIpId(lcTokenId2), ipId2); + assertEq(licenseToken.getExpirationTime(lcTokenId2), 0); + + assertEq(licenseToken.totalMintedTokens(), 2); + assertEq(licenseToken.totalSupply(), 2); + assertEq(licenseToken.balanceOf(ipOwner3), 2); + + uint256[] memory licenseTokens = new uint256[](2); + licenseTokens[0] = lcTokenId1; + licenseTokens[1] = lcTokenId2; + vm.prank(ipOwner3); + licensingModule.registerDerivativeWithLicenseTokens(ipId3, licenseTokens, ""); + + assertEq(licenseRegistry.hasIpAttachedLicenseTerms(ipId3, address(pilTemplate), termsId), true); + assertEq(licenseRegistry.getAttachedLicenseTermsCount(ipId3), 1); + assertEq(licenseRegistry.isDerivativeIp(ipId3), true); + assertEq(licenseRegistry.hasDerivativeIps(ipId3), false); + assertEq(licenseRegistry.getDerivativeIpCount(ipId3), 0); + + assertEq(licenseRegistry.hasDerivativeIps(ipId1), true); + assertEq(licenseRegistry.isDerivativeIp(ipId1), false); + assertEq(licenseRegistry.getDerivativeIp(ipId1, 0), ipId3); assertEq(licenseRegistry.getDerivativeIpCount(ipId1), 1); - assertEq(licenseRegistry.getParentIp(ipId2, 0), ipId1); - assertEq(licenseRegistry.getAttachedLicenseTermsCount(ipId1), 1); - assertEq(licenseRegistry.getAttachedLicenseTermsCount(ipId2), 1); - (address lt1, uint256 ltId1) = licenseRegistry.getAttachedLicenseTerms(ipId1, 0); - (address lt2, uint256 ltId2) = licenseRegistry.getAttachedLicenseTerms(ipId2, 0); - assertEq(lt1, lt2); - assertEq(ltId1, ltId2); + assertEq(licenseRegistry.hasDerivativeIps(ipId2), true); + assertEq(licenseRegistry.isDerivativeIp(ipId2), false); + assertEq(licenseRegistry.getDerivativeIp(ipId2, 0), ipId3); + assertEq(licenseRegistry.getDerivativeIpCount(ipId2), 1); + + assertEq(licenseRegistry.getParentIpCount(ipId3), 2); + assertEq(licenseRegistry.getParentIp(ipId3, 0), ipId1); + assertEq(licenseRegistry.getParentIp(ipId3, 1), ipId2); + + assertEq(licenseToken.totalSupply(), 0); + assertEq(licenseToken.totalMintedTokens(), 2); + + vm.expectRevert(abi.encodeWithSelector(ERC721NonexistentToken.selector, lcTokenId1)); + licenseToken.ownerOf(lcTokenId1); + vm.expectRevert(abi.encodeWithSelector(ERC721NonexistentToken.selector, lcTokenId2)); + licenseToken.ownerOf(lcTokenId2); + + (address licenseTemplate, uint256 licenseTermsId) = licenseRegistry.getAttachedLicenseTerms(ipId3, 0); + assertEq(licenseTemplate, address(pilTemplate)); } function test_LicensingModule_registerDerivativeWithLicenseTokens_revert_parentIsChild() public { - uint256 startLicenseId = attachAndMint_PILCommRemix_LicenseTokens({ - ipId: ipId1, - amount: 2, - receiver: ipOwner + uint256 termsId = pilTemplate.registerLicenseTerms(PILFlavors.nonCommercialSocialRemixing()); + vm.prank(ipOwner1); + + uint256 lcTokenId = licensingModule.mintLicenseTokens({ + licensorIpId: ipId1, + licenseTemplate: address(pilTemplate), + licenseTermsId: termsId, + amount: 1, + receiver: ipOwner1, + royaltyContext: "" }); - assertEq(startLicenseId, 0); - uint256[] memory licenseIds = new uint256[](1); - licenseIds[0] = startLicenseId; + uint256[] memory licenseTokens = new uint256[](1); + licenseTokens[0] = lcTokenId; - // TODO: this error is not descriptive of this test case. - vm.expectRevert(abi.encodeWithSelector(Errors.LicenseRegistry__DerivativeIpAlreadyHasLicense.selector, ipId1)); - vm.prank(ipOwner); - licensingModule.registerDerivativeWithLicenseTokens(ipId1, licenseIds, ""); + vm.expectRevert(abi.encodeWithSelector(Errors.LicenseRegistry__DerivativeIsParent.selector, ipId1)); + vm.prank(ipOwner1); + licensingModule.registerDerivativeWithLicenseTokens(ipId1, licenseTokens, ""); } - function test_LicensingModule_registerDerivativeWithLicenseTokens_revert_linkTwice() public { - uint256[] memory licenseIds = new uint256[](1); - uint256 startLicenseId; + function test_LicensingModule_registerDerivativeWithLicenseTokens_revert_ExpiredLicenseToken() public { + vm.prank(u.admin); + royaltyModule.whitelistRoyaltyToken(address(0x123), true); + PILTerms memory terms = PILTerms({ + transferable: true, + royaltyPolicy: address(royaltyPolicyLAP), + mintingFee: 0, + expiration: 10 days, + commercialUse: true, + commercialAttribution: true, + commercializerChecker: address(0), + commercializerCheckerData: "", + commercialRevShare: 0, + commercialRevCelling: 0, + derivativesAllowed: true, + derivativesAttribution: true, + derivativesApproval: false, + derivativesReciprocal: true, + derivativeRevCelling: 0, + currency: address(0x123) + }); - vm.startPrank(ipOwner); - licensingModule.attachLicenseTerms(ipId1, address(pilTemplate), commRemixTermsId); + uint256 termsId = pilTemplate.registerLicenseTerms(terms); + vm.prank(ipOwner1); + licensingModule.attachLicenseTerms(ipId1, address(pilTemplate), termsId); - startLicenseId = licensingModule.mintLicenseTokens({ + uint256 lcTokenId = licensingModule.mintLicenseTokens({ licensorIpId: ipId1, licenseTemplate: address(pilTemplate), - licenseTermsId: commRemixTermsId, - amount: 2, - receiver: ipOwner, + licenseTermsId: termsId, + amount: 1, + receiver: ipOwner2, royaltyContext: "" }); - assertEq(startLicenseId, 0); - licenseIds[0] = startLicenseId; + uint256 lcTokenExpiredTime = licenseToken.getExpirationTime(lcTokenId); + assertEq(licenseToken.ownerOf(lcTokenId), ipOwner2); + assertEq(licenseToken.getLicenseTermsId(lcTokenId), termsId); + assertEq(licenseToken.getLicenseTemplate(lcTokenId), address(pilTemplate)); + assertEq(licenseToken.getLicensorIpId(lcTokenId), ipId1); + assertEq(lcTokenExpiredTime, block.timestamp + 10 days); + assertEq(licenseToken.totalMintedTokens(), 1); + assertEq(licenseToken.totalSupply(), 1); + assertEq(licenseToken.balanceOf(ipOwner2), 1); - licensingModule.registerDerivativeWithLicenseTokens(ipId3, licenseIds, ""); + vm.warp(11 days); - licensingModule.attachLicenseTerms(ipId2, address(pilTemplate), commRemixTermsId); - startLicenseId = licensingModule.mintLicenseTokens({ + uint256[] memory licenseTokens = new uint256[](1); + licenseTokens[0] = lcTokenId; + + vm.expectRevert( + abi.encodeWithSelector( + Errors.LicenseToken__LicenseTokenExpired.selector, + lcTokenId, + lcTokenExpiredTime, + block.timestamp + ) + ); + vm.prank(ipOwner2); + licensingModule.registerDerivativeWithLicenseTokens(ipId2, licenseTokens, ""); + } + + function test_LicensingModule_registerDerivativeWithLicenseTokens_revert_ParentExpired() public { + uint256 termsId = pilTemplate.registerLicenseTerms(PILFlavors.nonCommercialSocialRemixing()); + PILTerms memory expiredTerms = PILFlavors.nonCommercialSocialRemixing(); + expiredTerms.expiration = 10 days; + uint256 expiredTermsId = pilTemplate.registerLicenseTerms(expiredTerms); + + vm.prank(ipOwner1); + licensingModule.attachLicenseTerms(ipId1, address(pilTemplate), termsId); + vm.prank(ipOwner2); + licensingModule.attachLicenseTerms(ipId2, address(pilTemplate), expiredTermsId); + + uint256 lcTokenId1 = licensingModule.mintLicenseTokens({ + licensorIpId: ipId1, + licenseTemplate: address(pilTemplate), + licenseTermsId: termsId, + amount: 1, + receiver: ipOwner3, + royaltyContext: "" + }); + + uint256 lcTokenId2 = licensingModule.mintLicenseTokens({ licensorIpId: ipId2, licenseTemplate: address(pilTemplate), - licenseTermsId: commRemixTermsId, - amount: 2, - receiver: ipOwner, + licenseTermsId: expiredTermsId, + amount: 1, + receiver: ipOwner3, + royaltyContext: "" + }); + + assertEq(licenseToken.ownerOf(lcTokenId1), ipOwner3); + assertEq(licenseToken.getLicenseTermsId(lcTokenId1), termsId); + assertEq(licenseToken.getLicenseTemplate(lcTokenId1), address(pilTemplate)); + assertEq(licenseToken.getLicensorIpId(lcTokenId1), ipId1); + assertEq(licenseToken.getExpirationTime(lcTokenId1), 0); + + assertEq(licenseToken.ownerOf(lcTokenId2), ipOwner3); + assertEq(licenseToken.getLicenseTermsId(lcTokenId2), expiredTermsId); + assertEq(licenseToken.getLicenseTemplate(lcTokenId2), address(pilTemplate)); + assertEq(licenseToken.getLicensorIpId(lcTokenId2), ipId2); + assertEq(licenseToken.getExpirationTime(lcTokenId2), block.timestamp + 10 days); + + assertEq(licenseToken.totalMintedTokens(), 2); + assertEq(licenseToken.totalSupply(), 2); + assertEq(licenseToken.balanceOf(ipOwner3), 2); + + uint256[] memory licenseTokens = new uint256[](2); + licenseTokens[0] = lcTokenId1; + licenseTokens[1] = lcTokenId2; + vm.prank(ipOwner3); + licensingModule.registerDerivativeWithLicenseTokens(ipId3, licenseTokens, ""); + + assertEq(licenseRegistry.hasIpAttachedLicenseTerms(ipId3, address(pilTemplate), termsId), true); + assertEq(licenseRegistry.hasIpAttachedLicenseTerms(ipId3, address(pilTemplate), expiredTermsId), true); + assertEq(licenseRegistry.getAttachedLicenseTermsCount(ipId3), 2); + assertEq(licenseRegistry.isDerivativeIp(ipId3), true); + assertEq(licenseRegistry.hasDerivativeIps(ipId3), false); + assertEq(licenseRegistry.getDerivativeIpCount(ipId3), 0); + + assertEq(licenseRegistry.hasDerivativeIps(ipId1), true); + assertEq(licenseRegistry.isDerivativeIp(ipId1), false); + assertEq(licenseRegistry.getDerivativeIp(ipId1, 0), ipId3); + assertEq(licenseRegistry.getDerivativeIpCount(ipId1), 1); + + assertEq(licenseRegistry.hasDerivativeIps(ipId2), true); + assertEq(licenseRegistry.isDerivativeIp(ipId2), false); + assertEq(licenseRegistry.getDerivativeIp(ipId2, 0), ipId3); + assertEq(licenseRegistry.getDerivativeIpCount(ipId2), 1); + + assertEq(licenseRegistry.getParentIpCount(ipId3), 2); + assertEq(licenseRegistry.getParentIp(ipId3, 0), ipId1); + assertEq(licenseRegistry.getParentIp(ipId3, 1), ipId2); + + (address licenseTemplate, uint256 licenseTermsId) = licenseRegistry.getAttachedLicenseTerms(ipId3, 0); + assertEq(licenseTemplate, address(pilTemplate)); + assertEq(licenseTermsId, termsId); + (address anotherLicenseTemplate, uint256 anotherLicenseTermsId) = licenseRegistry.getAttachedLicenseTerms( + ipId3, + 1 + ); + assertEq(anotherLicenseTemplate, address(pilTemplate)); + assertEq(anotherLicenseTermsId, expiredTermsId); + uint256[] memory licenseTerms = new uint256[](2); + licenseTerms[0] = termsId; + licenseTerms[1] = expiredTermsId; + + assertEq(licenseRegistry.getExpireTime(ipId3), block.timestamp + 10 days, "IPA has unexpected expiration time"); + vm.warp(5 days); + + uint256 lcTokenId3 = licensingModule.mintLicenseTokens({ + licensorIpId: ipId3, + licenseTemplate: address(pilTemplate), + licenseTermsId: termsId, + amount: 1, + receiver: ipOwner5, royaltyContext: "" }); - assertEq(startLicenseId, 2); - licenseIds[0] = startLicenseId; + vm.warp(11 days); + + licenseTokens = new uint256[](1); + licenseTokens[0] = lcTokenId3; - // TODO: this error is not descriptive of this test case. - vm.expectRevert(abi.encodeWithSelector(Errors.LicenseRegistry__DerivativeIpAlreadyHasLicense.selector, ipId3)); - licensingModule.registerDerivativeWithLicenseTokens(ipId3, licenseIds, ""); - vm.stopPrank(); + vm.expectRevert(abi.encodeWithSelector(Errors.LicenseRegistry__ParentIpExpired.selector, ipId3)); + vm.prank(ipOwner5); + licensingModule.registerDerivativeWithLicenseTokens(ipId5, licenseTokens, ""); } - function test_LicensingModule_registerDerivativeWithLicenseTokens_revert_notLicensee() public { - uint256 startLicenseId = attachAndMint_PILCommRemix_LicenseTokens({ - ipId: ipId1, - amount: 2, - receiver: ipOwner + function test_LicensingModule_registerDerivativeWithLicenseTokens_revert_childAlreadyAttachedLicense() public { + uint256 termsId = pilTemplate.registerLicenseTerms(PILFlavors.nonCommercialSocialRemixing()); + vm.prank(ipOwner1); + licensingModule.attachLicenseTerms(ipId1, address(pilTemplate), termsId); + + uint256 lcTokenId = licensingModule.mintLicenseTokens({ + licensorIpId: ipId1, + licenseTemplate: address(pilTemplate), + licenseTermsId: termsId, + amount: 1, + receiver: ipOwner1, + royaltyContext: "" }); - assertEq(startLicenseId, 0); - vm.stopPrank(); + uint256[] memory licenseTokens = new uint256[](1); + licenseTokens[0] = lcTokenId; + + vm.expectRevert(abi.encodeWithSelector(Errors.LicenseRegistry__DerivativeIpAlreadyHasLicense.selector, ipId1)); + vm.prank(ipOwner1); + licensingModule.registerDerivativeWithLicenseTokens(ipId1, licenseTokens, ""); + } + + function test_LicensingModule_registerDerivativeWithLicenseTokens_revert_AlreadyRegisteredAsDerivative() public { + uint256 termsId = pilTemplate.registerLicenseTerms(PILFlavors.nonCommercialSocialRemixing()); + vm.prank(ipOwner1); + licensingModule.attachLicenseTerms(ipId1, address(pilTemplate), termsId); + vm.prank(ipOwner2); + licensingModule.attachLicenseTerms(ipId2, address(pilTemplate), termsId); + + uint256 lcTokenId1 = licensingModule.mintLicenseTokens({ + licensorIpId: ipId1, + licenseTemplate: address(pilTemplate), + licenseTermsId: termsId, + amount: 1, + receiver: ipOwner3, + royaltyContext: "" + }); + + uint256 lcTokenId2 = licensingModule.mintLicenseTokens({ + licensorIpId: ipId2, + licenseTemplate: address(pilTemplate), + licenseTermsId: termsId, + amount: 1, + receiver: ipOwner3, + royaltyContext: "" + }); + + uint256[] memory licenseTokens = new uint256[](1); + licenseTokens[0] = lcTokenId1; + vm.prank(ipOwner3); + licensingModule.registerDerivativeWithLicenseTokens(ipId3, licenseTokens, ""); + + licenseTokens = new uint256[](1); + licenseTokens[0] = lcTokenId2; + vm.expectRevert(abi.encodeWithSelector(Errors.LicenseRegistry__DerivativeAlreadyRegistered.selector, ipId3)); + vm.prank(ipOwner3); + licensingModule.registerDerivativeWithLicenseTokens(ipId3, licenseTokens, ""); + } + + function test_LicensingModule_registerDerivativeWithLicenseTokens_revert_notLicensee() public { + uint256 termsId = pilTemplate.registerLicenseTerms(PILFlavors.nonCommercialSocialRemixing()); + vm.prank(ipOwner1); + licensingModule.attachLicenseTerms(ipId1, address(pilTemplate), termsId); + + uint256 lcTokenId = licensingModule.mintLicenseTokens({ + licensorIpId: ipId1, + licenseTemplate: address(pilTemplate), + licenseTermsId: termsId, + amount: 1, + receiver: ipOwner2, + royaltyContext: "" + }); - uint256[] memory licenseIds = new uint256[](1); - licenseIds[0] = startLicenseId; + uint256[] memory licenseTokens = new uint256[](1); + licenseTokens[0] = lcTokenId; - // TODO: this error is not descriptive of this test case. vm.expectRevert( - abi.encodeWithSelector( - Errors.AccessController__PermissionDenied.selector, - ipId1, - licenseHolder, - address(licensingModule), - licensingModule.registerDerivativeWithLicenseTokens.selector - ) + abi.encodeWithSelector(Errors.LicenseToken__NotLicenseTokenOwner.selector, lcTokenId, ipOwner3, ipOwner2) ); - vm.prank(licenseHolder); - licensingModule.registerDerivativeWithLicenseTokens(ipId1, licenseIds, ""); + vm.prank(ipOwner3); + licensingModule.registerDerivativeWithLicenseTokens(ipId3, licenseTokens, ""); } function test_LicensingModule_singleTransfer_verifyOk() public { - uint256 startLicenseId = attachAndMint_PILCommRemix_LicenseTokens({ - ipId: ipId1, - amount: 2, - receiver: licenseHolder + uint256 termsId = pilTemplate.registerLicenseTerms(PILFlavors.nonCommercialSocialRemixing()); + vm.prank(ipOwner1); + licensingModule.attachLicenseTerms(ipId1, address(pilTemplate), termsId); + + uint256 lcTokenId = licensingModule.mintLicenseTokens({ + licensorIpId: ipId1, + licenseTemplate: address(pilTemplate), + licenseTermsId: termsId, + amount: 1, + receiver: ipOwner2, + royaltyContext: "" }); - uint256 endLicenseId = startLicenseId + 1; - assertEq(startLicenseId, 0); - address licenseHolder2 = address(0x102); - assertEq(licenseToken.balanceOf(licenseHolder), 2); - assertEq(licenseToken.balanceOf(licenseHolder2), 0); + vm.prank(ipOwner2); + licenseToken.transferFrom(ipOwner2, ipOwner3, lcTokenId); - vm.prank(licenseHolder); - licenseToken.transferFrom(licenseHolder, licenseHolder2, startLicenseId); + uint256[] memory licenseTokens = new uint256[](1); + licenseTokens[0] = lcTokenId; + vm.prank(ipOwner3); + licensingModule.registerDerivativeWithLicenseTokens(ipId3, licenseTokens, ""); - assertEq(licenseToken.balanceOf(licenseHolder), 1); - assertEq(licenseToken.balanceOf(licenseHolder2), 1); - assertEq(licenseToken.ownerOf(startLicenseId), licenseHolder2); - assertEq(licenseToken.ownerOf(endLicenseId), licenseHolder); + assertEq(licenseRegistry.hasIpAttachedLicenseTerms(ipId3, address(pilTemplate), termsId), true); + assertEq(licenseRegistry.getAttachedLicenseTermsCount(ipId3), 1); + assertEq(licenseRegistry.isDerivativeIp(ipId3), true); + assertEq(licenseRegistry.hasDerivativeIps(ipId3), false); + assertEq(licenseRegistry.hasDerivativeIps(ipId1), true); + assertEq(licenseRegistry.isDerivativeIp(ipId1), false); + assertEq(licenseRegistry.getDerivativeIpCount(ipId1), 1); + assertEq(licenseRegistry.getDerivativeIpCount(ipId3), 0); + assertEq(licenseRegistry.getDerivativeIp(ipId1, 0), ipId3); + assertEq(licenseRegistry.getParentIp(ipId3, 0), ipId1); + assertEq(licenseRegistry.getParentIpCount(ipId3), 1); + assertEq(licenseToken.totalSupply(), 0); + assertEq(licenseToken.totalMintedTokens(), 1); + vm.expectRevert(abi.encodeWithSelector(ERC721NonexistentToken.selector, lcTokenId)); + licenseToken.ownerOf(lcTokenId); + + (address licenseTemplate, uint256 licenseTermsId) = licenseRegistry.getAttachedLicenseTerms(ipId3, 0); + assertEq(licenseTemplate, address(pilTemplate)); + assertEq(licenseTermsId, termsId); } - function onERC721Received(address, address, uint256, bytes memory) public pure returns (bytes4) { - return this.onERC721Received.selector; + function test_LicensingModule_mintLicenseTokens_HookVerifyPass() public { + vm.prank(u.admin); + royaltyModule.whitelistRoyaltyToken(address(0x123), true); + + MockTokenGatedHook tokenGatedHook = new MockTokenGatedHook(); + PILTerms memory terms = PILTerms({ + transferable: true, + royaltyPolicy: address(royaltyPolicyLAP), + mintingFee: 0, + expiration: 0, + commercialUse: true, + commercialAttribution: true, + commercializerChecker: address(tokenGatedHook), + commercializerCheckerData: abi.encode(address(gatedNftBar)), + commercialRevShare: 0, + commercialRevCelling: 0, + derivativesAllowed: true, + derivativesAttribution: true, + derivativesApproval: false, + derivativesReciprocal: true, + derivativeRevCelling: 0, + currency: address(0x123) + }); + + uint256 termsId = pilTemplate.registerLicenseTerms(terms); + vm.prank(ipOwner1); + licensingModule.attachLicenseTerms(ipId1, address(pilTemplate), termsId); + + gatedNftBar.mint(ipOwner2); + + uint256 lcTokenId = licensingModule.mintLicenseTokens({ + licensorIpId: ipId1, + licenseTemplate: address(pilTemplate), + licenseTermsId: termsId, + amount: 1, + receiver: ipOwner2, + royaltyContext: "" + }); + + assertEq(licenseToken.ownerOf(lcTokenId), ipOwner2); + assertEq(licenseToken.getLicenseTermsId(lcTokenId), termsId); + assertEq(licenseToken.getLicenseTemplate(lcTokenId), address(pilTemplate)); + assertEq(licenseToken.getLicensorIpId(lcTokenId), ipId1); + assertEq(licenseToken.getExpirationTime(lcTokenId), 0); + assertEq(licenseToken.totalMintedTokens(), 1); + assertEq(licenseToken.totalSupply(), 1); + assertEq(licenseToken.balanceOf(ipOwner2), 1); } - function attachAndMint_PILCommRemix_LicenseTokens( - address ipId, - uint256 amount, - address receiver - ) internal returns (uint256 startLicenseId) { - vm.prank(ipOwner); - licensingModule.attachLicenseTerms(ipId, address(pilTemplate), commRemixTermsId); + function test_LicensingModule_mintLicenseTokens_revert_HookVerifyFail() public { + vm.prank(u.admin); + royaltyModule.whitelistRoyaltyToken(address(0x123), true); + + MockTokenGatedHook tokenGatedHook = new MockTokenGatedHook(); + PILTerms memory terms = PILTerms({ + transferable: true, + royaltyPolicy: address(royaltyPolicyLAP), + mintingFee: 0, + expiration: 0, + commercialUse: true, + commercialAttribution: true, + commercializerChecker: address(tokenGatedHook), + commercializerCheckerData: abi.encode(address(gatedNftBar)), + commercialRevShare: 0, + commercialRevCelling: 0, + derivativesAllowed: true, + derivativesAttribution: true, + derivativesApproval: false, + derivativesReciprocal: true, + derivativeRevCelling: 0, + currency: address(0x123) + }); + + uint256 termsId = pilTemplate.registerLicenseTerms(terms); + vm.prank(ipOwner1); + licensingModule.attachLicenseTerms(ipId1, address(pilTemplate), termsId); - vm.prank(receiver); - startLicenseId = licensingModule.mintLicenseTokens({ - licensorIpId: ipId, + vm.expectRevert( + abi.encodeWithSelector( + Errors.LicensingModule__LicenseDenyMintLicenseToken.selector, + address(pilTemplate), + termsId, + ipId1 + ) + ); + uint256 lcTokenId = licensingModule.mintLicenseTokens({ + licensorIpId: ipId1, licenseTemplate: address(pilTemplate), - licenseTermsId: commRemixTermsId, - amount: amount, - receiver: receiver, + licenseTermsId: termsId, + amount: 1, + receiver: ipOwner2, royaltyContext: "" }); } + + function test_LicensingModule_revert_HookVerifyFail() public { + vm.prank(u.admin); + royaltyModule.whitelistRoyaltyToken(address(0x123), true); + + MockTokenGatedHook tokenGatedHook = new MockTokenGatedHook(); + PILTerms memory terms = PILTerms({ + transferable: true, + royaltyPolicy: address(royaltyPolicyLAP), + mintingFee: 0, + expiration: 0, + commercialUse: true, + commercialAttribution: true, + commercializerChecker: address(tokenGatedHook), + commercializerCheckerData: abi.encode(address(gatedNftBar)), + commercialRevShare: 0, + commercialRevCelling: 0, + derivativesAllowed: true, + derivativesAttribution: true, + derivativesApproval: false, + derivativesReciprocal: true, + derivativeRevCelling: 0, + currency: address(0x123) + }); + + uint256 termsId = pilTemplate.registerLicenseTerms(terms); + vm.prank(ipOwner1); + licensingModule.attachLicenseTerms(ipId1, address(pilTemplate), termsId); + + uint256 gatedNftId = gatedNftBar.mint(ipOwner2); + + uint256 lcTokenId = licensingModule.mintLicenseTokens({ + licensorIpId: ipId1, + licenseTemplate: address(pilTemplate), + licenseTermsId: termsId, + amount: 1, + receiver: ipOwner2, + royaltyContext: "" + }); + assertEq(licenseToken.ownerOf(lcTokenId), ipOwner2); + assertEq(licenseToken.getLicenseTermsId(lcTokenId), termsId); + assertEq(licenseToken.getLicenseTemplate(lcTokenId), address(pilTemplate)); + assertEq(licenseToken.getLicensorIpId(lcTokenId), ipId1); + assertEq(licenseToken.getExpirationTime(lcTokenId), 0); + assertEq(licenseToken.totalMintedTokens(), 1); + assertEq(licenseToken.totalSupply(), 1); + assertEq(licenseToken.balanceOf(ipOwner2), 1); + + vm.prank(ipOwner2); + gatedNftBar.burn(gatedNftId); + + uint256[] memory licenseTokens = new uint256[](1); + licenseTokens[0] = lcTokenId; + vm.expectRevert( + abi.encodeWithSelector( + Errors.LicensingModule__LicenseTokenNotCompatibleForDerivative.selector, + ipId2, + licenseTokens + ) + ); + vm.prank(ipOwner2); + licensingModule.registerDerivativeWithLicenseTokens(ipId2, licenseTokens, ""); + } + + // test registerDerivativeWithLicenseTokens revert licenseTokenIds is empty + function test_LicensingModule_registerDerivativeWithLicenseTokens_revert_emptyLicenseTokens() public { + vm.expectRevert(Errors.LicensingModule__NoLicenseToken.selector); + vm.prank(ipOwner1); + licensingModule.registerDerivativeWithLicenseTokens(ipId1, new uint256[](0), ""); + } + + // test registerDerivative revert parentIpIds is empty + function test_LicensingModule_registerDerivative_revert_emptyParentIpIds() public { + vm.expectRevert(Errors.LicensingModule__NoParentIp.selector); + vm.prank(ipOwner2); + licensingModule.registerDerivative(ipId2, new address[](0), new uint256[](0), address(0), ""); + } + + function test_LicensingModule_registerDerivative_revert_parentIdsLengthMismatchWithLicenseIds() public { + address[] memory parentIpIds = new address[](1); + parentIpIds[0] = ipId1; + vm.expectRevert(abi.encodeWithSelector(Errors.LicensingModule__LicenseTermsLengthMismatch.selector, 1, 0)); + vm.prank(ipOwner2); + licensingModule.registerDerivative(ipId2, parentIpIds, new uint256[](0), address(0), ""); + } + + function test_LicensingModule_registerDerivative_revert_IncompatibleLicenses() public { + uint256 socialRemixTermsId = pilTemplate.registerLicenseTerms(PILFlavors.nonCommercialSocialRemixing()); + uint256 commUseTermsId = pilTemplate.registerLicenseTerms( + PILFlavors.commercialUse({ + mintingFee: 0, + currencyToken: address(erc20), + royaltyPolicy: address(royaltyPolicyLAP) + }) + ); + + vm.prank(ipOwner1); + licensingModule.attachLicenseTerms(ipId1, address(pilTemplate), socialRemixTermsId); + vm.prank(ipOwner2); + licensingModule.attachLicenseTerms(ipId2, address(pilTemplate), commUseTermsId); + + address[] memory parentIpIds = new address[](2); + parentIpIds[0] = ipId1; + parentIpIds[1] = ipId2; + + uint256[] memory licenseTermsIds = new uint256[](2); + licenseTermsIds[0] = socialRemixTermsId; + licenseTermsIds[1] = commUseTermsId; + + vm.expectRevert( + abi.encodeWithSelector(Errors.LicensingModule__LicenseNotCompatibleForDerivative.selector, ipId3) + ); + vm.prank(ipOwner3); + licensingModule.registerDerivative(ipId3, parentIpIds, licenseTermsIds, address(pilTemplate), ""); + } + + function onERC721Received(address, address, uint256, bytes memory) public pure returns (bytes4) { + return this.onERC721Received.selector; + } } diff --git a/test/foundry/modules/licensing/PILicenseTemplate.t.sol b/test/foundry/modules/licensing/PILicenseTemplate.t.sol new file mode 100644 index 000000000..c4d28347b --- /dev/null +++ b/test/foundry/modules/licensing/PILicenseTemplate.t.sol @@ -0,0 +1,526 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.23; + +// external +import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; + +// contracts +import { PILicenseTemplateErrors } from "../../../../contracts/lib/PILicenseTemplateErrors.sol"; +import { PILFlavors } from "../../../../contracts/lib/PILFlavors.sol"; +import { PILTerms } from "../../../../contracts/interfaces/modules/licensing/IPILicenseTemplate.sol"; + +// test +import { MockERC721 } from "../../mocks/token/MockERC721.sol"; +import { BaseTest } from "../../utils/BaseTest.t.sol"; + +contract PILicenseTemplateTest is BaseTest { + using Strings for *; + + MockERC721 internal mockNft = new MockERC721("MockERC721"); + MockERC721 internal gatedNftFoo = new MockERC721{ salt: bytes32(uint256(1)) }("GatedNftFoo"); + MockERC721 internal gatedNftBar = new MockERC721{ salt: bytes32(uint256(2)) }("GatedNftBar"); + + address public ipId1; + address public ipId2; + address public ipId3; + address public ipId5; + address public ipOwner1 = address(0x111); + address public ipOwner2 = address(0x222); + address public ipOwner3 = address(0x333); + address public ipOwner5 = address(0x444); + uint256 public tokenId1 = 1; + uint256 public tokenId2 = 2; + uint256 public tokenId3 = 3; + uint256 public tokenId5 = 5; + + address public licenseHolder = address(0x101); + + function setUp() public override { + super.setUp(); + // Create IPAccounts + mockNft.mintId(ipOwner1, tokenId1); + mockNft.mintId(ipOwner2, tokenId2); + mockNft.mintId(ipOwner3, tokenId3); + mockNft.mintId(ipOwner5, tokenId5); + + ipId1 = ipAssetRegistry.register(address(mockNft), tokenId1); + ipId2 = ipAssetRegistry.register(address(mockNft), tokenId2); + ipId3 = ipAssetRegistry.register(address(mockNft), tokenId3); + ipId5 = ipAssetRegistry.register(address(mockNft), tokenId5); + + vm.label(ipId1, "IPAccount1"); + vm.label(ipId2, "IPAccount2"); + vm.label(ipId3, "IPAccount3"); + vm.label(ipId5, "IPAccount5"); + } + // this contract is for testing for each PILicenseTemplate's functions + // register license terms with PILTerms struct + function test_PILicenseTemplate_registerLicenseTerms() public { + uint256 defaultTermsId = pilTemplate.registerLicenseTerms(PILFlavors.defaultValuesLicenseTerms()); + assertEq(defaultTermsId, 1); + (address royaltyPolicy, bytes memory royaltyData, uint256 mintingFee, address currency) = pilTemplate + .getRoyaltyPolicy(defaultTermsId); + assertEq(royaltyPolicy, address(0), "royaltyPolicy should be address(0)"); + assertEq(royaltyData, abi.encode(0), "royaltyData should be empty"); + assertEq(mintingFee, 0, "mintingFee should be 0"); + assertEq(currency, address(0), "currency should be address(0)"); + assertTrue(pilTemplate.isLicenseTransferable(defaultTermsId), "license should be transferable"); + assertEq(pilTemplate.getLicenseTermsId(PILFlavors.defaultValuesLicenseTerms()), 1); + assertEq(pilTemplate.getExpireTime(defaultTermsId, block.timestamp), 0, "expire time should be 0"); + assertTrue(pilTemplate.exists(defaultTermsId), "license terms should exist"); + + uint256 socialRemixTermsId = pilTemplate.registerLicenseTerms(PILFlavors.nonCommercialSocialRemixing()); + assertEq(socialRemixTermsId, 2); + (royaltyPolicy, royaltyData, mintingFee, currency) = pilTemplate.getRoyaltyPolicy(socialRemixTermsId); + assertEq(royaltyPolicy, address(0)); + assertEq(royaltyData, abi.encode(0)); + assertEq(mintingFee, 0); + assertEq(currency, address(0)); + assertTrue(pilTemplate.isLicenseTransferable(socialRemixTermsId)); + assertEq(pilTemplate.getLicenseTermsId(PILFlavors.nonCommercialSocialRemixing()), 2); + assertEq(pilTemplate.getExpireTime(socialRemixTermsId, block.timestamp), 0, "expire time should be 0"); + assertTrue(pilTemplate.exists(socialRemixTermsId), "license terms should exist"); + + uint256 commUseTermsId = pilTemplate.registerLicenseTerms( + PILFlavors.commercialUse({ + mintingFee: 100, + currencyToken: address(erc20), + royaltyPolicy: address(royaltyPolicyLAP) + }) + ); + assertEq(commUseTermsId, 3); + (royaltyPolicy, royaltyData, mintingFee, currency) = pilTemplate.getRoyaltyPolicy(commUseTermsId); + assertEq(royaltyPolicy, address(royaltyPolicyLAP)); + assertEq(royaltyData, abi.encode(0)); + assertEq(mintingFee, 100); + assertEq(currency, address(erc20)); + assertTrue(pilTemplate.isLicenseTransferable(commUseTermsId)); + assertEq( + pilTemplate.getLicenseTermsId(PILFlavors.commercialUse(100, address(erc20), address(royaltyPolicyLAP))), + 3 + ); + assertEq(pilTemplate.getExpireTime(commUseTermsId, block.timestamp), 0, "expire time should be 0"); + assertEq(pilTemplate.totalRegisteredLicenseTerms(), 3); + + uint256 commRemixTermsId = pilTemplate.registerLicenseTerms( + PILFlavors.commercialRemix({ + mintingFee: 100, + commercialRevShare: 10, + royaltyPolicy: address(royaltyPolicyLAP), + currencyToken: address(erc20) + }) + ); + assertEq(commRemixTermsId, 4); + (royaltyPolicy, royaltyData, mintingFee, currency) = pilTemplate.getRoyaltyPolicy(commRemixTermsId); + assertEq(royaltyPolicy, address(royaltyPolicyLAP)); + assertEq(royaltyData, abi.encode(10)); + assertEq(mintingFee, 100); + assertEq(currency, address(erc20)); + assertTrue(pilTemplate.isLicenseTransferable(commRemixTermsId)); + assertEq( + pilTemplate.getLicenseTermsId( + PILFlavors.commercialRemix(100, 10, address(royaltyPolicyLAP), address(erc20)) + ), + 4 + ); + assertEq(pilTemplate.getExpireTime(commRemixTermsId, block.timestamp), 0, "expire time should be 0"); + assertTrue(pilTemplate.exists(commRemixTermsId), "license terms should exist"); + + assertEq(pilTemplate.totalRegisteredLicenseTerms(), 4); + + uint256[] memory licenseTermsIds = new uint256[](4); + licenseTermsIds[0] = defaultTermsId; + licenseTermsIds[1] = socialRemixTermsId; + licenseTermsIds[2] = commUseTermsId; + licenseTermsIds[3] = commRemixTermsId; + assertEq(pilTemplate.getEarlierExpireTime(licenseTermsIds, block.timestamp), 0); + + assertEq(pilTemplate.toJson(defaultTermsId), _DefaultToJson()); + } + // register license terms twice + function test_PILicenseTemplate_registerLicenseTerms_twice() public { + uint256 defaultTermsId = pilTemplate.registerLicenseTerms(PILFlavors.defaultValuesLicenseTerms()); + uint256 defaultTermsId1 = pilTemplate.registerLicenseTerms(PILFlavors.defaultValuesLicenseTerms()); + assertEq(defaultTermsId, defaultTermsId1); + } + + function test_PILicenseTemplate_registerLicenseTerms_revert_InvalidInputs() public { + // mintingFee is 0 + vm.expectRevert(PILicenseTemplateErrors.PILicenseTemplate__RoyaltyPolicyNotWhitelisted.selector); + pilTemplate.registerLicenseTerms( + PILFlavors.commercialUse({ mintingFee: 0, currencyToken: address(erc20), royaltyPolicy: address(0x9999) }) + ); + vm.expectRevert(PILicenseTemplateErrors.PILicenseTemplate__CurrencyTokenNotWhitelisted.selector); + pilTemplate.registerLicenseTerms( + PILFlavors.commercialUse({ + mintingFee: 0, + currencyToken: address(0x333), + royaltyPolicy: address(royaltyPolicyLAP) + }) + ); + vm.expectRevert(PILicenseTemplateErrors.PILicenseTemplate__CurrencyTokenNotWhitelisted.selector); + pilTemplate.registerLicenseTerms( + PILFlavors.commercialUse({ + mintingFee: 0, + currencyToken: address(0x333), + royaltyPolicy: address(royaltyPolicyLAP) + }) + ); + vm.expectRevert(PILicenseTemplateErrors.PILicenseTemplate__RoyaltyPolicyRequiresCurrencyToken.selector); + pilTemplate.registerLicenseTerms( + PILFlavors.commercialUse({ + mintingFee: 0, + currencyToken: address(0), + royaltyPolicy: address(royaltyPolicyLAP) + }) + ); + + PILTerms memory terms = PILFlavors.nonCommercialSocialRemixing(); + terms.commercialAttribution = true; + vm.expectRevert(PILicenseTemplateErrors.PILicenseTemplate__CommercialDisabled_CantAddAttribution.selector); + pilTemplate.registerLicenseTerms(terms); + + terms = PILFlavors.nonCommercialSocialRemixing(); + terms.commercializerChecker = address(0x9999); + vm.expectRevert(PILicenseTemplateErrors.PILicenseTemplate__CommercialDisabled_CantAddCommercializers.selector); + pilTemplate.registerLicenseTerms(terms); + + terms = PILFlavors.nonCommercialSocialRemixing(); + terms.commercialRevShare = 10; + vm.expectRevert(PILicenseTemplateErrors.PILicenseTemplate__CommercialDisabled_CantAddRevShare.selector); + pilTemplate.registerLicenseTerms(terms); + + terms = PILFlavors.nonCommercialSocialRemixing(); + terms.royaltyPolicy = address(royaltyPolicyLAP); + terms.currency = address(erc20); + vm.expectRevert(PILicenseTemplateErrors.PILicenseTemplate__CommercialDisabled_CantAddRoyaltyPolicy.selector); + pilTemplate.registerLicenseTerms(terms); + + terms = PILFlavors.commercialUse({ + mintingFee: 100, + currencyToken: address(erc20), + royaltyPolicy: address(royaltyPolicyLAP) + }); + terms.royaltyPolicy = address(0); + vm.expectRevert(PILicenseTemplateErrors.PILicenseTemplate__CommercialEnabled_RoyaltyPolicyRequired.selector); + pilTemplate.registerLicenseTerms(terms); + + terms = PILFlavors.commercialUse({ + mintingFee: 100, + currencyToken: address(erc20), + royaltyPolicy: address(royaltyPolicyLAP) + }); + terms.commercializerChecker = address(0x9999); + vm.expectRevert( + abi.encodeWithSelector( + PILicenseTemplateErrors.PILicenseTemplate__CommercializerCheckerDoesNotSupportHook.selector, + address(0x9999) + ) + ); + pilTemplate.registerLicenseTerms(terms); + + terms = PILFlavors.defaultValuesLicenseTerms(); + terms.derivativesAttribution = true; + vm.expectRevert(PILicenseTemplateErrors.PILicenseTemplate__DerivativesDisabled_CantAddAttribution.selector); + pilTemplate.registerLicenseTerms(terms); + + terms = PILFlavors.defaultValuesLicenseTerms(); + terms.derivativesApproval = true; + vm.expectRevert(PILicenseTemplateErrors.PILicenseTemplate__DerivativesDisabled_CantAddApproval.selector); + pilTemplate.registerLicenseTerms(terms); + + terms = PILFlavors.defaultValuesLicenseTerms(); + terms.derivativesReciprocal = true; + vm.expectRevert(PILicenseTemplateErrors.PILicenseTemplate__DerivativesDisabled_CantAddReciprocal.selector); + pilTemplate.registerLicenseTerms(terms); + } + + // get license terms ID by PILTerms struct + function test_PILicenseTemplate_getLicenseTermsId() public { + uint256 commUseTermsId = pilTemplate.registerLicenseTerms( + PILFlavors.commercialUse({ + mintingFee: 100, + currencyToken: address(erc20), + royaltyPolicy: address(royaltyPolicyLAP) + }) + ); + assertEq( + pilTemplate.getLicenseTermsId(PILFlavors.commercialUse(100, address(erc20), address(royaltyPolicyLAP))), + commUseTermsId + ); + + assertEq( + pilTemplate.getLicenseTermsId(PILFlavors.commercialUse(999, address(123), address(royaltyPolicyLAP))), + 0 + ); + } + + // get license terms struct by ID + function test_PILicenseTemplate_getLicenseTerms() public { + uint256 commUseTermsId = pilTemplate.registerLicenseTerms( + PILFlavors.commercialUse({ + mintingFee: 100, + currencyToken: address(erc20), + royaltyPolicy: address(royaltyPolicyLAP) + }) + ); + PILTerms memory terms = pilTemplate.getLicenseTerms(commUseTermsId); + assertEq(terms.mintingFee, 100); + assertEq(terms.currency, address(erc20)); + assertEq(terms.royaltyPolicy, address(royaltyPolicyLAP)); + } + + // test license terms exists + function test_PILicenseTemplate_exists() public { + uint256 commUseTermsId = pilTemplate.registerLicenseTerms( + PILFlavors.commercialUse({ + mintingFee: 100, + currencyToken: address(erc20), + royaltyPolicy: address(royaltyPolicyLAP) + }) + ); + assertTrue(pilTemplate.exists(commUseTermsId)); + assertFalse(pilTemplate.exists(999)); + } + + // test verifyMintLicenseToken + function test_PILicenseTemplate_verifyMintLicenseToken() public { + uint256 commUseTermsId = pilTemplate.registerLicenseTerms( + PILFlavors.commercialUse({ + mintingFee: 100, + currencyToken: address(erc20), + royaltyPolicy: address(royaltyPolicyLAP) + }) + ); + bool result = pilTemplate.verifyMintLicenseToken(commUseTermsId, ipId2, ipId1, 1); + assertTrue(result); + } + + function test_PILicenseTemplate_verifyMintLicenseToken_FromDerivativeIp_ButNotAttachedLicense() public { + uint256 commUseTermsId = pilTemplate.registerLicenseTerms( + PILFlavors.commercialUse({ + mintingFee: 0, + currencyToken: address(erc20), + royaltyPolicy: address(royaltyPolicyLAP) + }) + ); + vm.prank(ipOwner1); + licensingModule.attachLicenseTerms(ipId1, address(pilTemplate), commUseTermsId); + + address[] memory parentIpIds = new address[](1); + parentIpIds[0] = ipId1; + uint256[] memory licenseTermsIds = new uint256[](1); + licenseTermsIds[0] = commUseTermsId; + vm.prank(ipOwner2); + licensingModule.registerDerivative(ipId2, parentIpIds, licenseTermsIds, address(pilTemplate), ""); + + uint256 anotherTermsId = pilTemplate.registerLicenseTerms(PILFlavors.nonCommercialSocialRemixing()); + + bool result = pilTemplate.verifyMintLicenseToken(anotherTermsId, ipOwner3, ipId2, 1); + assertFalse(result); + } + + function test_PILicenseTemplate_verifyMintLicenseToken_FromDerivativeIp_NotReciprocal() public { + uint256 commUseTermsId = pilTemplate.registerLicenseTerms( + PILFlavors.commercialUse({ + mintingFee: 0, + currencyToken: address(erc20), + royaltyPolicy: address(royaltyPolicyLAP) + }) + ); + vm.prank(ipOwner1); + licensingModule.attachLicenseTerms(ipId1, address(pilTemplate), commUseTermsId); + + address[] memory parentIpIds = new address[](1); + parentIpIds[0] = ipId1; + uint256[] memory licenseTermsIds = new uint256[](1); + licenseTermsIds[0] = commUseTermsId; + vm.prank(ipOwner2); + licensingModule.registerDerivative(ipId2, parentIpIds, licenseTermsIds, address(pilTemplate), ""); + + bool result = pilTemplate.verifyMintLicenseToken(commUseTermsId, ipOwner3, ipId2, 1); + assertFalse(result); + } + + // test verifyRegisterDerivative + function test_PILicenseTemplate_verifyRegisterDerivative() public { + uint256 commUseTermsId = pilTemplate.registerLicenseTerms( + PILFlavors.commercialUse({ + mintingFee: 100, + currencyToken: address(erc20), + royaltyPolicy: address(royaltyPolicyLAP) + }) + ); + + bool result = pilTemplate.verifyRegisterDerivative(ipId2, ipId1, commUseTermsId, ipOwner2); + assertTrue(result); + } + + function test_PILicenseTemplate_verifyRegisterDerivative_WithApproval() public { + PILTerms memory terms = PILFlavors.nonCommercialSocialRemixing(); + terms.derivativesApproval = true; + uint256 socialRemixTermsId = pilTemplate.registerLicenseTerms(terms); + vm.prank(ipId1); + pilTemplate.setApproval(ipId1, socialRemixTermsId, ipId2, true); + assertTrue(pilTemplate.isDerivativeApproved(ipId1, socialRemixTermsId, ipId2)); + + bool result = pilTemplate.verifyRegisterDerivative(ipId2, ipId1, socialRemixTermsId, ipOwner2); + assertTrue(result); + } + + function test_PILicenseTemplate_verifyRegisterDerivative_WithoutApproval() public { + PILTerms memory terms = PILFlavors.nonCommercialSocialRemixing(); + terms.derivativesApproval = true; + uint256 socialRemixTermsId = pilTemplate.registerLicenseTerms(terms); + vm.prank(ipId1); + pilTemplate.setApproval(ipId1, socialRemixTermsId, ipId2, false); + assertFalse(pilTemplate.isDerivativeApproved(ipId1, socialRemixTermsId, ipId2)); + + bool result = pilTemplate.verifyRegisterDerivative(ipId2, ipId1, socialRemixTermsId, ipOwner2); + assertFalse(result); + } + + function test_PILicenseTemplate_verifyRegisterDerivative_derivativeNotAllowed() public { + PILTerms memory terms = PILFlavors.defaultValuesLicenseTerms(); + uint256 socialRemixTermsId = pilTemplate.registerLicenseTerms(terms); + bool result = pilTemplate.verifyRegisterDerivative(ipId2, ipId1, socialRemixTermsId, ipOwner2); + assertFalse(result); + } + + // test verifyCompatibleLicenses + function test_PILicenseTemplate_verifyCompatibleLicenses() public { + uint256 commUseTermsId = pilTemplate.registerLicenseTerms( + PILFlavors.commercialUse({ + mintingFee: 100, + currencyToken: address(erc20), + royaltyPolicy: address(royaltyPolicyLAP) + }) + ); + uint256 commRemixTermsId = pilTemplate.registerLicenseTerms( + PILFlavors.commercialRemix({ + mintingFee: 100, + commercialRevShare: 10, + royaltyPolicy: address(royaltyPolicyLAP), + currencyToken: address(erc20) + }) + ); + uint256[] memory licenseTermsIds = new uint256[](2); + licenseTermsIds[0] = commUseTermsId; + licenseTermsIds[1] = commRemixTermsId; + assertFalse(pilTemplate.verifyCompatibleLicenses(licenseTermsIds)); + + uint256 defaultTermsId = pilTemplate.registerLicenseTerms(PILFlavors.defaultValuesLicenseTerms()); + uint256 socialRemixTermsId = pilTemplate.registerLicenseTerms(PILFlavors.nonCommercialSocialRemixing()); + licenseTermsIds[0] = defaultTermsId; + licenseTermsIds[1] = socialRemixTermsId; + assertFalse(pilTemplate.verifyCompatibleLicenses(licenseTermsIds)); + + licenseTermsIds[0] = socialRemixTermsId; + licenseTermsIds[1] = commRemixTermsId; + assertFalse(pilTemplate.verifyCompatibleLicenses(licenseTermsIds)); + + licenseTermsIds[0] = socialRemixTermsId; + licenseTermsIds[1] = socialRemixTermsId; + assertTrue(pilTemplate.verifyCompatibleLicenses(licenseTermsIds)); + } + + // test verifyRegisterDerivativeForAllParents + function test_PILicenseTemplate_verifyRegisterDerivativeForAllParents() public { + uint256 commUseTermsId = pilTemplate.registerLicenseTerms( + PILFlavors.commercialUse({ + mintingFee: 100, + currencyToken: address(erc20), + royaltyPolicy: address(royaltyPolicyLAP) + }) + ); + uint256 commRemixTermsId = pilTemplate.registerLicenseTerms( + PILFlavors.commercialRemix({ + mintingFee: 100, + commercialRevShare: 10, + royaltyPolicy: address(royaltyPolicyLAP), + currencyToken: address(erc20) + }) + ); + uint256[] memory licenseTermsIds = new uint256[](2); + licenseTermsIds[0] = commUseTermsId; + licenseTermsIds[1] = commRemixTermsId; + address[] memory parentIpIds = new address[](2); + parentIpIds[0] = ipId1; + parentIpIds[1] = ipId3; + assertFalse(pilTemplate.verifyRegisterDerivativeForAllParents(ipId2, parentIpIds, licenseTermsIds, ipOwner2)); + + uint256 defaultTermsId = pilTemplate.registerLicenseTerms(PILFlavors.defaultValuesLicenseTerms()); + uint256 socialRemixTermsId = pilTemplate.registerLicenseTerms(PILFlavors.nonCommercialSocialRemixing()); + licenseTermsIds[0] = defaultTermsId; + licenseTermsIds[1] = socialRemixTermsId; + assertFalse(pilTemplate.verifyRegisterDerivativeForAllParents(ipId2, parentIpIds, licenseTermsIds, ipOwner2)); + + licenseTermsIds[0] = socialRemixTermsId; + licenseTermsIds[1] = commRemixTermsId; + assertFalse(pilTemplate.verifyRegisterDerivativeForAllParents(ipId2, parentIpIds, licenseTermsIds, ipOwner2)); + + licenseTermsIds[0] = socialRemixTermsId; + licenseTermsIds[1] = socialRemixTermsId; + assertTrue(pilTemplate.verifyRegisterDerivativeForAllParents(ipId2, parentIpIds, licenseTermsIds, ipOwner2)); + } + + // test isLicenseTransferable + function test_PILicenseTemplate_isLicenseTransferable() public { + uint256 defaultTermsId = pilTemplate.registerLicenseTerms(PILFlavors.defaultValuesLicenseTerms()); + assertTrue(pilTemplate.isLicenseTransferable(defaultTermsId)); + + uint256 socialRemixTermsId = pilTemplate.registerLicenseTerms(PILFlavors.nonCommercialSocialRemixing()); + assertTrue(pilTemplate.isLicenseTransferable(socialRemixTermsId)); + + uint256 commUseTermsId = pilTemplate.registerLicenseTerms( + PILFlavors.commercialUse({ + mintingFee: 100, + currencyToken: address(erc20), + royaltyPolicy: address(royaltyPolicyLAP) + }) + ); + assertTrue(pilTemplate.isLicenseTransferable(commUseTermsId)); + + uint256 commRemixTermsId = pilTemplate.registerLicenseTerms( + PILFlavors.commercialRemix({ + mintingFee: 100, + commercialRevShare: 10, + royaltyPolicy: address(royaltyPolicyLAP), + currencyToken: address(erc20) + }) + ); + assertTrue(pilTemplate.isLicenseTransferable(commRemixTermsId)); + + PILTerms memory terms = pilTemplate.getLicenseTerms(commRemixTermsId); + terms.transferable = false; + uint256 nonTransferableTermsId = pilTemplate.registerLicenseTerms(terms); + assertFalse(pilTemplate.isLicenseTransferable(nonTransferableTermsId)); + } + + function test_PILicenseTemplate_getEarlierExpiredTime_WithEmptyLicenseTerms() public { + uint256[] memory licenseTermsIds = new uint256[](0); + assertEq(pilTemplate.getEarlierExpireTime(licenseTermsIds, block.timestamp), 0); + } + + function test_PILicenseTemplate_name() public { + assertEq(pilTemplate.name(), "pil"); + } + + function test_PILicenseTemplate_getMetadataURI() public { + assertEq( + pilTemplate.getMetadataURI(), + "https://github.com/storyprotocol/protocol-core/blob/main/PIL-Beta-2024-02.pdf" + ); + } + + function onERC721Received(address, address, uint256, bytes memory) public pure returns (bytes4) { + return this.onERC721Received.selector; + } + + function _DefaultToJson() internal pure returns (string memory) { + /* solhint-disable */ + return + '{"trait_type": "Expiration", "value": "never"},{"trait_type": "Currency", "value": "0x0000000000000000000000000000000000000000"},{"trait_type": "Commercial Use", "value": "false"},{"trait_type": "Commercial Attribution", "value": "false"},{"trait_type": "Commercial Revenue Share", "max_value": 1000, "value": 0},{"trait_type": "Commercial Revenue Celling", "value": 0},{"trait_type": "Commercializer Check", "value": "0x0000000000000000000000000000000000000000"},{"trait_type": "Derivatives Allowed", "value": "false"},{"trait_type": "Derivatives Attribution", "value": "false"},{"trait_type": "Derivatives Revenue Celling", "value": 0},{"trait_type": "Derivatives Approval", "value": "false"},{"trait_type": "Derivatives Reciprocal", "value": "false"},'; + /* solhint-enable */ + } +} diff --git a/test/foundry/registries/LicenseRegistry.t.sol b/test/foundry/registries/LicenseRegistry.t.sol new file mode 100644 index 000000000..11f6f55d9 --- /dev/null +++ b/test/foundry/registries/LicenseRegistry.t.sol @@ -0,0 +1,268 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.23; + +// external +import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; + +// contracts +import { IIPAccount } from "../../../contracts/interfaces/IIPAccount.sol"; +import { Errors } from "../../../contracts/lib/Errors.sol"; +import { PILFlavors } from "../../../contracts/lib/PILFlavors.sol"; +import { MockLicenseTemplate } from "../mocks/module/MockLicenseTemplate.sol"; +import { IPAccountStorageOps } from "../../../contracts/lib/IPAccountStorageOps.sol"; +import { Licensing } from "../../../contracts/lib/Licensing.sol"; + +// test +import { MockERC721 } from "../mocks/token/MockERC721.sol"; +import { BaseTest } from "../utils/BaseTest.t.sol"; + +contract LicenseRegistryTest is BaseTest { + using Strings for *; + using IPAccountStorageOps for IIPAccount; + + error ERC721NonexistentToken(uint256 tokenId); + + MockERC721 internal mockNft = new MockERC721("MockERC721"); + MockERC721 internal gatedNftFoo = new MockERC721{ salt: bytes32(uint256(1)) }("GatedNftFoo"); + MockERC721 internal gatedNftBar = new MockERC721{ salt: bytes32(uint256(2)) }("GatedNftBar"); + + address public ipId1; + address public ipId2; + address public ipId3; + address public ipId5; + address public ipOwner1 = address(0x111); + address public ipOwner2 = address(0x222); + address public ipOwner3 = address(0x333); + address public ipOwner5 = address(0x444); + uint256 public tokenId1 = 1; + uint256 public tokenId2 = 2; + uint256 public tokenId3 = 3; + uint256 public tokenId5 = 5; + + address public licenseHolder = address(0x101); + + function setUp() public override { + super.setUp(); + // Create IPAccounts + mockNft.mintId(ipOwner1, tokenId1); + mockNft.mintId(ipOwner2, tokenId2); + mockNft.mintId(ipOwner3, tokenId3); + mockNft.mintId(ipOwner5, tokenId5); + + ipId1 = ipAssetRegistry.register(address(mockNft), tokenId1); + ipId2 = ipAssetRegistry.register(address(mockNft), tokenId2); + ipId3 = ipAssetRegistry.register(address(mockNft), tokenId3); + ipId5 = ipAssetRegistry.register(address(mockNft), tokenId5); + + vm.label(ipId1, "IPAccount1"); + vm.label(ipId2, "IPAccount2"); + vm.label(ipId3, "IPAccount3"); + vm.label(ipId5, "IPAccount5"); + } + + function test_LicenseRegistry_setDisputeModule() public { + vm.prank(admin); + licenseRegistry.setDisputeModule(address(123)); + assertEq(address(licenseRegistry.disputeModule()), address(123)); + } + + function test_LicenseRegistry_setLicensingModule() public { + vm.prank(admin); + licenseRegistry.setLicensingModule(address(123)); + assertEq(address(licenseRegistry.licensingModule()), address(123)); + } + + function test_LicenseRegistry_setDisputeModule_revert_ZeroAddress() public { + vm.expectRevert(Errors.LicenseRegistry__ZeroDisputeModule.selector); + vm.prank(admin); + licenseRegistry.setDisputeModule(address(0)); + } + + function test_LicenseRegistry_setLicensingModule_revert_ZeroAddress() public { + vm.expectRevert(Errors.LicenseRegistry__ZeroLicensingModule.selector); + vm.prank(admin); + licenseRegistry.setLicensingModule(address(0)); + } + + function test_LicenseRegistry_setDefaultLicenseTerms() public { + uint256 socialRemixTermsId = pilTemplate.registerLicenseTerms(PILFlavors.nonCommercialSocialRemixing()); + vm.prank(admin); + licenseRegistry.setDefaultLicenseTerms(address(pilTemplate), socialRemixTermsId); + (address defaultLicenseTemplate, uint256 defaultLicenseTermsId) = licenseRegistry.getDefaultLicenseTerms(); + assertEq(defaultLicenseTemplate, address(pilTemplate)); + assertEq(defaultLicenseTermsId, socialRemixTermsId); + } + + // test registerLicenseTemplate + function test_LicenseRegistry_registerLicenseTemplate() public { + MockLicenseTemplate pilTemplate2 = new MockLicenseTemplate(); + vm.prank(admin); + licenseRegistry.registerLicenseTemplate(address(pilTemplate2)); + assertTrue(licenseRegistry.isRegisteredLicenseTemplate(address(pilTemplate2))); + } + + function test_LicenseRegistry_registerLicenseTemplate_revert_NotImplementedInterface() public { + vm.expectRevert(abi.encodeWithSelector(Errors.LicenseRegistry__NotLicenseTemplate.selector, address(0x123))); + vm.prank(admin); + licenseRegistry.registerLicenseTemplate(address(0x123)); + } + + function test_LicenseRegistry_setExpireTime() public { + vm.prank(address(licensingModule)); + licenseRegistry.setExpireTime(ipId1, block.timestamp + 100); + assertEq(licenseRegistry.getExpireTime(ipId1), block.timestamp + 100); + assertEq( + IIPAccount(payable(ipId1)).getUint256(address(licenseRegistry), licenseRegistry.EXPIRATION_TIME()), + block.timestamp + 100 + ); + } + + function test_LicenseRegistry_setMintingLicenseConfigForLicense() public { + uint256 defaultTermsId = pilTemplate.registerLicenseTerms(PILFlavors.defaultValuesLicenseTerms()); + Licensing.MintingLicenseConfig memory mintingLicenseConfig = Licensing.MintingLicenseConfig({ + isSet: true, + mintingFee: 100, + mintingFeeModule: address(0), + receiverCheckModule: address(0), + receiverCheckData: "" + }); + + vm.prank(address(licensingModule)); + licenseRegistry.setMintingLicenseConfigForLicense( + ipId1, + address(pilTemplate), + defaultTermsId, + mintingLicenseConfig + ); + Licensing.MintingLicenseConfig memory returnedMintingLicenseConfig = licenseRegistry.getMintingLicenseConfig( + ipId1, + address(pilTemplate), + defaultTermsId + ); + assertEq(returnedMintingLicenseConfig.mintingFee, 100); + assertEq(returnedMintingLicenseConfig.mintingFeeModule, address(0)); + assertEq(returnedMintingLicenseConfig.receiverCheckModule, address(0)); + assertEq(returnedMintingLicenseConfig.receiverCheckData, ""); + } + + function test_LicenseRegistry_setMintingLicenseConfigForLicense_revert_UnregisteredTemplate() public { + MockLicenseTemplate pilTemplate2 = new MockLicenseTemplate(); + uint256 termsId = pilTemplate2.registerLicenseTerms(); + Licensing.MintingLicenseConfig memory mintingLicenseConfig = Licensing.MintingLicenseConfig({ + isSet: true, + mintingFee: 100, + mintingFeeModule: address(0), + receiverCheckModule: address(0), + receiverCheckData: "" + }); + + vm.expectRevert( + abi.encodeWithSelector(Errors.LicenseRegistry__UnregisteredLicenseTemplate.selector, address(pilTemplate2)) + ); + vm.prank(address(licensingModule)); + licenseRegistry.setMintingLicenseConfigForLicense(ipId1, address(pilTemplate2), termsId, mintingLicenseConfig); + } + + function test_LicenseRegistry_setMintingLicenseConfigForIp() public { + uint256 defaultTermsId = pilTemplate.registerLicenseTerms(PILFlavors.defaultValuesLicenseTerms()); + Licensing.MintingLicenseConfig memory mintingLicenseConfig = Licensing.MintingLicenseConfig({ + isSet: true, + mintingFee: 100, + mintingFeeModule: address(0), + receiverCheckModule: address(0), + receiverCheckData: "" + }); + + vm.prank(address(licensingModule)); + licenseRegistry.setMintingLicenseConfigForIp(ipId1, mintingLicenseConfig); + + Licensing.MintingLicenseConfig memory returnedMintingLicenseConfig = licenseRegistry.getMintingLicenseConfig( + ipId1, + address(pilTemplate), + defaultTermsId + ); + assertEq(returnedMintingLicenseConfig.mintingFee, 100); + assertEq(returnedMintingLicenseConfig.mintingFeeModule, address(0)); + assertEq(returnedMintingLicenseConfig.receiverCheckModule, address(0)); + assertEq(returnedMintingLicenseConfig.receiverCheckData, ""); + } + + // test attachLicenseTermsToIp + function test_LicenseRegistry_attachLicenseTermsToIp_revert_CannotAttachToDerivativeIP() public { + uint256 socialRemixTermsId = pilTemplate.registerLicenseTerms(PILFlavors.nonCommercialSocialRemixing()); + vm.prank(admin); + licenseRegistry.setDefaultLicenseTerms(address(pilTemplate), socialRemixTermsId); + + address[] memory parentIpIds = new address[](1); + parentIpIds[0] = ipId1; + uint256[] memory licenseTermsIds = new uint256[](1); + licenseTermsIds[0] = socialRemixTermsId; + vm.prank(ipOwner2); + licensingModule.registerDerivative(ipId2, parentIpIds, licenseTermsIds, address(pilTemplate), ""); + + uint256 defaultTermsId = pilTemplate.registerLicenseTerms(PILFlavors.defaultValuesLicenseTerms()); + vm.expectRevert(Errors.LicensingModule__DerivativesCannotAddLicenseTerms.selector); + vm.prank(address(licensingModule)); + licenseRegistry.attachLicenseTermsToIp(ipId2, address(pilTemplate), defaultTermsId); + } + + function test_LicenseRegistry_registerDerivativeIp_revert_parentsArrayEmpty() public { + uint256 socialRemixTermsId = pilTemplate.registerLicenseTerms(PILFlavors.nonCommercialSocialRemixing()); + vm.prank(admin); + licenseRegistry.setDefaultLicenseTerms(address(pilTemplate), socialRemixTermsId); + + address[] memory parentIpIds = new address[](0); + uint256[] memory licenseTermsIds = new uint256[](1); + licenseTermsIds[0] = socialRemixTermsId; + vm.expectRevert(Errors.LicenseRegistry__NoParentIp.selector); + vm.prank(address(licensingModule)); + licenseRegistry.registerDerivativeIp(ipId2, parentIpIds, address(pilTemplate), licenseTermsIds); + } + + // test getAttachedLicenseTerms + function test_LicenseRegistry_getAttachedLicenseTerms_revert_OutOfIndex() public { + uint256 socialRemixTermsId = pilTemplate.registerLicenseTerms(PILFlavors.nonCommercialSocialRemixing()); + + vm.prank(ipOwner1); + licensingModule.attachLicenseTerms(ipId1, address(pilTemplate), socialRemixTermsId); + vm.expectRevert(abi.encodeWithSelector(Errors.LicenseRegistry__IndexOutOfBounds.selector, ipId1, 1, 1)); + licenseRegistry.getAttachedLicenseTerms(ipId1, 1); + } + + // test getDerivativeIp revert IndexOutOfBounds( + function test_LicenseRegistry_getDerivativeIp_revert_IndexOutOfBounds() public { + uint256 socialRemixTermsId = pilTemplate.registerLicenseTerms(PILFlavors.nonCommercialSocialRemixing()); + vm.prank(admin); + licenseRegistry.setDefaultLicenseTerms(address(pilTemplate), socialRemixTermsId); + + address[] memory parentIpIds = new address[](1); + parentIpIds[0] = ipId1; + uint256[] memory licenseTermsIds = new uint256[](1); + licenseTermsIds[0] = socialRemixTermsId; + vm.prank(ipOwner2); + licensingModule.registerDerivative(ipId2, parentIpIds, licenseTermsIds, address(pilTemplate), ""); + + vm.expectRevert(abi.encodeWithSelector(Errors.LicenseRegistry__IndexOutOfBounds.selector, ipId1, 1, 1)); + licenseRegistry.getDerivativeIp(ipId1, 1); + } + + function test_LicenseRegistry_getParentIp_revert_IndexOutOfBounds() public { + uint256 socialRemixTermsId = pilTemplate.registerLicenseTerms(PILFlavors.nonCommercialSocialRemixing()); + vm.prank(admin); + licenseRegistry.setDefaultLicenseTerms(address(pilTemplate), socialRemixTermsId); + + address[] memory parentIpIds = new address[](1); + parentIpIds[0] = ipId1; + uint256[] memory licenseTermsIds = new uint256[](1); + licenseTermsIds[0] = socialRemixTermsId; + vm.prank(ipOwner2); + licensingModule.registerDerivative(ipId2, parentIpIds, licenseTermsIds, address(pilTemplate), ""); + + vm.expectRevert(abi.encodeWithSelector(Errors.LicenseRegistry__IndexOutOfBounds.selector, ipId2, 1, 1)); + licenseRegistry.getParentIp(ipId2, 1); + } + + function onERC721Received(address, address, uint256, bytes memory) public pure returns (bytes4) { + return this.onERC721Received.selector; + } +}