From 22ce9c0cb78d5ffd82a9f37cbe3f3bcb1ae65ff3 Mon Sep 17 00:00:00 2001 From: Ramarti Date: Sat, 18 Nov 2023 03:01:21 -0300 Subject: [PATCH] License activation (#176) * hookity hook * hook ouuuut * lol * wip testing * simplify licensing * WIP * fixes * more fixes * wip * fix tests * typo * separated termrepository from accesscontrol * Update contracts/modules/licensing/LicensingModule.sol Co-authored-by: kingster-will <83567446+kingster-will@users.noreply.github.com> --------- Co-authored-by: Raul Co-authored-by: kingster-will <83567446+kingster-will@users.noreply.github.com> --- contracts/IPAssetRegistry.sol | 5 +- contracts/StoryProtocol.sol | 67 +- contracts/hooks/base/AsyncBaseHook.sol | 23 +- contracts/ip-org/IPOrg.sol | 4 +- contracts/lib/AccessControl.sol | 3 + contracts/lib/Errors.sol | 35 +- contracts/lib/modules/Licensing.sol | 22 +- .../lib/modules/ProtocolLicensingTerms.sol | 15 +- .../licensing/LicenseCreatorModule.sol | 389 ------------ .../modules/licensing/LicenseRegistry.sol | 142 +++-- .../modules/licensing/LicensingModule.sol | 581 ++++++++++++++++++ .../modules/licensing/ProtocolTermsHelper.sol | 34 - .../modules/licensing/TermsRepository.sol | 50 +- .../registration/RegistrationModule.sol | 2 + test/foundry/mocks/MockAsyncHook.sol | 16 +- .../modules/licensing/BaseLicensingTest.sol | 536 +++++++--------- .../licensing/LicenseCreatorModule.Terms.sol | 110 ---- .../LicensingCreatorModule.Config.t.sol | 108 ---- .../LicensingCreatorModule.Licensing.sol | 91 --- .../licensing/LicensingModule.Config.t.sol | 118 ++++ .../licensing/LicensingModule.Licensing.sol | 216 +++++++ .../modules/registration/RegistrationTest.sol | 32 +- test/foundry/utils/BaseTest.sol | 15 +- 23 files changed, 1449 insertions(+), 1165 deletions(-) delete mode 100644 contracts/modules/licensing/LicenseCreatorModule.sol create mode 100644 contracts/modules/licensing/LicensingModule.sol delete mode 100644 contracts/modules/licensing/ProtocolTermsHelper.sol delete mode 100644 test/foundry/modules/licensing/LicenseCreatorModule.Terms.sol delete mode 100644 test/foundry/modules/licensing/LicensingCreatorModule.Config.t.sol delete mode 100644 test/foundry/modules/licensing/LicensingCreatorModule.Licensing.sol create mode 100644 test/foundry/modules/licensing/LicensingModule.Config.t.sol create mode 100644 test/foundry/modules/licensing/LicensingModule.Licensing.sol diff --git a/contracts/IPAssetRegistry.sol b/contracts/IPAssetRegistry.sol index 199df923..898bbe12 100644 --- a/contracts/IPAssetRegistry.sol +++ b/contracts/IPAssetRegistry.sol @@ -72,12 +72,13 @@ contract IPAssetRegistry is IIPAssetRegistry { } // Crate a new IP asset with the provided IP attributes. - ipAssetId = totalSupply++; + ipAssetId = ++totalSupply; uint64 registrationDate = uint64(block.timestamp); _ipAssets[ipAssetId] = IPA({ name: name_, ipAssetType: ipAssetType_, - status: 0, // TODO(ramarti): Define status types. + // For now, let's assume 0 == unset, 1 is OK. TODO: Add status enum and synch with License status + status: 1, registrant: registrant_, ipOrg: ipOrg_, hash: hash_, diff --git a/contracts/StoryProtocol.sol b/contracts/StoryProtocol.sol index 3c75a551..12b9e36e 100644 --- a/contracts/StoryProtocol.sol +++ b/contracts/StoryProtocol.sol @@ -11,6 +11,7 @@ import { LibRelationship } from "contracts/lib/modules/LibRelationship.sol"; import { Registration } from "contracts/lib/modules/Registration.sol"; import { ModuleRegistryKeys } from "contracts/lib/modules/ModuleRegistryKeys.sol"; import { Licensing } from "contracts/lib/modules/Licensing.sol"; +import { FixedSet } from "contracts/utils/FixedSet.sol"; contract StoryProtocol { @@ -228,17 +229,18 @@ contract StoryProtocol { Licensing.LicenseeType.LNFTHolder, abi.encode(licensee_) ); - return abi.decode( - MODULE_REGISTRY.execute( - IIPOrg(ipOrg_), - msg.sender, - ModuleRegistryKeys.LICENSING_MODULE, - params, - preHooksData_, - postHooksData_ + bytes memory result = MODULE_REGISTRY.execute( + IIPOrg(ipOrg_), + msg.sender, + ModuleRegistryKeys.LICENSING_MODULE, + abi.encode( + Licensing.CREATE_LICENSE, + params ), - (uint256) + preHooksData_, + postHooksData_ ); + return abi.decode(result, (uint256)); } /// Creates a License bound to a certain IPA. It's not an NFT, the licensee will be the owner of the IPA. @@ -260,14 +262,53 @@ contract StoryProtocol { Licensing.LicenseeType.BoundToIpa, abi.encode(ipaId_) ); - return abi.decode( MODULE_REGISTRY.execute( + bytes memory result = MODULE_REGISTRY.execute( IIPOrg(ipOrg_), msg.sender, ModuleRegistryKeys.LICENSING_MODULE, - params, + abi.encode( + Licensing.CREATE_LICENSE, + params + ), preHooksData_, postHooksData_ - ), - (uint256)); + ); + return abi.decode(result, (uint256)); + } + + function activateLicense( + address ipOrg_, + uint256 licenseId_ + ) external { + MODULE_REGISTRY.execute( + IIPOrg(ipOrg_), + msg.sender, + ModuleRegistryKeys.LICENSING_MODULE, + abi.encode( + Licensing.ACTIVATE_LICENSE, + abi.encode(licenseId_) + ), + new bytes[](0), + new bytes[](0) + ); } + + function bindLnftToIpa( + address ipOrg_, + uint256 licenseId_, + uint256 ipaId_ + ) external { + MODULE_REGISTRY.execute( + IIPOrg(ipOrg_), + msg.sender, + ModuleRegistryKeys.LICENSING_MODULE, + abi.encode( + Licensing.BOND_LNFT_TO_IPA, + abi.encode(licenseId_, ipaId_) + ), + new bytes[](0), + new bytes[](0) + ); + } + } diff --git a/contracts/hooks/base/AsyncBaseHook.sol b/contracts/hooks/base/AsyncBaseHook.sol index 3fed74f6..9468761b 100644 --- a/contracts/hooks/base/AsyncBaseHook.sol +++ b/contracts/hooks/base/AsyncBaseHook.sol @@ -16,7 +16,6 @@ import { Hook } from "contracts/lib/hooks/Hook.sol"; abstract contract AsyncBaseHook is BaseHook { using ERC165Checker for address; - address private immutable CALLBACK_CALLER; /// @dev requestId => callback handler mapping(bytes32 => ICallbackHandler) public callbackHandlers; @@ -40,15 +39,10 @@ abstract contract AsyncBaseHook is BaseHook { /// @notice Constructs the AsyncBaseHook contract. /// @param accessControl_ The address of the access control contract. - /// @param callbackCaller_ The address of the callback caller contract. /// @dev The constructor sets the access control and callback caller addresses. constructor( - address accessControl_, - address callbackCaller_ - ) BaseHook(accessControl_) { - if (callbackCaller_ == address(0)) revert Errors.ZeroAddress(); - CALLBACK_CALLER = callbackCaller_; - } + address accessControl_ + ) BaseHook(accessControl_) {} /// @notice Executes an asynchronous hook. /// @dev Modules would utilize the function to make an async call. @@ -108,21 +102,28 @@ abstract contract AsyncBaseHook is BaseHook { bytes memory hookParams_ ) internal virtual returns (bytes memory hookData, bytes32 requestId); + /// @dev Internal function to get the address of the callback caller. + /// concrete hoot implementation should override the function. + /// @param requestId_ The ID of the request. + /// @return The address of the callback caller. + function _callbackCaller(bytes32 requestId_) internal view virtual returns (address); /// @dev Internal function to handle a callback from an asynchronous call. /// @param requestId_ The ID of the request. /// @param callbackData_ The data returned by the callback. function _handleCallback( bytes32 requestId_, - bytes calldata callbackData_ + bytes memory callbackData_ ) internal virtual { // Only designated callback caller can make a callback - if (msg.sender != CALLBACK_CALLER) { + address caller = _callbackCaller(requestId_); + if (msg.sender != caller) { revert Errors.Hook_OnlyCallbackCallerCanCallback( msg.sender, - CALLBACK_CALLER + caller ); } + // Checking if a callback handler exists for the given request ID if (address(callbackHandlers[requestId_]) == address(0)) { revert Errors.Hook_InvalidAsyncRequestId(requestId_); diff --git a/contracts/ip-org/IPOrg.sol b/contracts/ip-org/IPOrg.sol index 58a1aaa1..de426cf8 100644 --- a/contracts/ip-org/IPOrg.sol +++ b/contracts/ip-org/IPOrg.sol @@ -58,7 +58,7 @@ contract IPOrg is /// @notice Gets the current owner of an IP asset within the IP Org. function ownerOf(uint256 id) public view override(IIPOrg, ERC721Upgradeable) returns (address) { - return ERC721Upgradeable.ownerOf(id); + return super.ownerOf(id); } /// @notice Retrieves the token URI for an IP Asset within the IP Asset Org. @@ -102,7 +102,7 @@ contract IPOrg is /// @notice Registers a new IP Asset wrapper for the IP Org. function mint(address owner_) public onlyRegistrationModule returns (uint256 id) { totalSupply++; - id = lastIndex++; + id = ++lastIndex; _mint(owner_, id); } diff --git a/contracts/lib/AccessControl.sol b/contracts/lib/AccessControl.sol index 9e3d2554..4fce2ba5 100644 --- a/contracts/lib/AccessControl.sol +++ b/contracts/lib/AccessControl.sol @@ -30,4 +30,7 @@ library AccessControl { // Role that can execute Hooks bytes32 constant HOOK_CALLER_ROLE = keccak256("HOOK_CALLER_ROLE"); + // Role to set legal terms in TermsRepository + bytes32 constant TERMS_SETTER_ROLE = keccak256("TERMS_SETTER_ROLE"); + } diff --git a/contracts/lib/Errors.sol b/contracts/lib/Errors.sol index df9cb475..05bae324 100644 --- a/contracts/lib/Errors.sol +++ b/contracts/lib/Errors.sol @@ -250,39 +250,42 @@ library Errors { error TermsRegistry_CommercialStatusUnset(); //////////////////////////////////////////////////////////////////////////// - // LicenseCreatorModule // + // LicensingModule // //////////////////////////////////////////////////////////////////////////// /// @notice The franchise does not exist. - error LicensingModule_NonExistentIPOrg(); error LicensingModule_CallerNotIpOrgOwner(); error LicensingModule_InvalidConfigType(); error LicensingModule_InvalidTermCommercialStatus(); error LicensingModule_IpOrgFrameworkAlreadySet(); error LicensingModule_DuplicateTermId(); - error LicensingModule_InvalidIntent(); - error LicensingModule_IpaNotActive(); - error LicensingModule_IpaIdRequired(); error LicensingModule_CommercialLicenseNotAllowed(); error LicensingModule_NonCommercialTermsRequired(); error LicensingModule_IpOrgNotConfigured(); error LicensingModule_ipOrgTermNotFound(); error LicensingModule_ShareAlikeDisabled(); - + error LicensingModule_InvalidAction(); + error LicensingModule_CallerNotLicensor(); + error LicensingModule_ParentLicenseNotActive(); + error LicensingModule_InvalidIpa(); + error LicensingModule_CallerNotLicenseOwner(); + error LicensingModule_CantFindParentLicenseOrRelatedIpa(); + error LicensingModule_InvalidLicenseeType(); + error LicensingModule_InvalidLicensorType(); //////////////////////////////////////////////////////////////////////////// // LicenseRegistry // //////////////////////////////////////////////////////////////////////////// - error LicensingModule_InvalidLicenseeType(); error LicenseRegistry_ZeroIpaRegistryAddress(); - error LicenseRegistry_LNFTShouldNotHaveIpaId(); - error LicenseRegistry_BoundToIpaShouldHaveIpaId(); error LicenseRegistry_UnknownLicenseId(); error LicenseRegistry_NotLicenseNFT(); error LicenseRegistry_InvalidIpa(); error LicenseRegistry_ZeroModuleRegistryAddress(); error LicenseRegistry_CallerNotLicensingModule(); - + error LicenseRegistry_CallerNotRevoker(); + error LicenseRegistry_LicenseNotPending(); + error LicenseRegistry_InvalidLicenseStatus(); + //////////////////////////////////////////////////////////////////////////// // RegistrationModule // //////////////////////////////////////////////////////////////////////////// @@ -331,7 +334,7 @@ library Errors { error RelationshipModule_UnsupportedRelationshipDst(); error RelationshipModule_InvalidConfigOperation(); - + error RelationshipModule_CallerNotIpOrgOwner(); error RelationshipModule_InvalidRelatable(); error RelationshipModule_RelTypeNotSet(string relType); @@ -371,4 +374,14 @@ library Errors { /// @notice The address is not the owner of the token. error TokenGatedHook_NotTokenOwner(address tokenAddress, address ownerAddress); + + //////////////////////////////////////////////////////////////////////////// + // LicensorApprovalHook // + //////////////////////////////////////////////////////////////////////////// + + error LicensorApprovalHook_ApprovalAlreadyRequested(); + error LicensorApprovalHook_InvalidLicensor(); + error LicensorApprovalHook_InvalidLicenseId(); + error LicensorApprovalHook_NoApprovalRequested(); + error LicensorApprovalHook_InvalidResponseStatus(); } diff --git a/contracts/lib/modules/Licensing.sol b/contracts/lib/modules/Licensing.sol index adda8a24..21a02d95 100644 --- a/contracts/lib/modules/Licensing.sol +++ b/contracts/lib/modules/Licensing.sol @@ -13,6 +13,8 @@ library Licensing { struct License { /// States the commercial nature of the license. All terms will follow. bool isCommercial; + /// License status. // TODO: IPA status should follow + LicenseStatus status; /// address granting the license address licensor; /// address that could make a license invalid @@ -30,8 +32,13 @@ library Licensing { ShortString[] termIds; /// The data configuring each term. May be empty bytes. May be passed to the term hook bytes[] termsData; - /// Future use - bytes data; + } + + enum LicenseStatus { + Unset, + Active, + Revoked, + Pending } /// User facing parameters for creating a license @@ -47,6 +54,8 @@ library Licensing { struct RegistryAddition { /// States the commercial nature of the license. All terms will follow. bool isCommercial; + /// Only Active or Pending will be accepted here + LicenseStatus status; /// address granting the license address licensor; /// address that could make a license invalid @@ -59,8 +68,6 @@ library Licensing { ShortString[] termIds; /// The data configuring each term. May be empty bytes. May be passed to the term hook bytes[] termsData; - /// Future use - bytes data; } enum LicenseeType { @@ -110,12 +117,15 @@ library Licensing { bytes[] termData; } - /// Input for IpOrg legal terms configuration in LicenseCreatorModule + /// Input for IpOrg legal terms configuration in LicensingModule struct FrameworkConfig { TermsConfig comTermsConfig; TermsConfig nonComTermsConfig; } - /// Input for IpOrg legal terms configuration in LicenseCreatorModule (for now, the only option) + /// Input for IpOrg legal terms configuration in LicensingModule (for now, the only option) bytes32 constant LICENSING_FRAMEWORK_CONFIG = keccak256("LICENSING_FRAMEWORK_CONFIG"); + bytes32 constant CREATE_LICENSE = keccak256("CREATE_LICENSE"); + bytes32 constant ACTIVATE_LICENSE = keccak256("ACTIVATE_LICENSE"); + bytes32 constant BOND_LNFT_TO_IPA = keccak256("BOND_LNFT_TO_IPA"); } \ No newline at end of file diff --git a/contracts/lib/modules/ProtocolLicensingTerms.sol b/contracts/lib/modules/ProtocolLicensingTerms.sol index cac774d6..7d3e2984 100644 --- a/contracts/lib/modules/ProtocolLicensingTerms.sol +++ b/contracts/lib/modules/ProtocolLicensingTerms.sol @@ -3,8 +3,10 @@ pragma solidity ^0.8.19; /// List of Licensing Term categories library TermCategories { - string constant FORMAT_CATEGORIES = "FORMAT_CATEGORIES"; + string constant CATEGORIZATION = "CATEGORIZATION"; string constant SHARE_ALIKE = "SHARE_ALIKE"; + string constant ACTIVATION = "ACTIVATION"; + string constant LICENSOR = "LICENSOR"; } /// List of Protocol Term Ids (meaning the Licensing Module will have specific instructions @@ -13,4 +15,15 @@ library TermCategories { /// see https://docs.openzeppelin.com/contracts/4.x/api/utils#ShortStrings library TermIds { string constant NFT_SHARE_ALIKE = "NFT_SHARE_ALIKE"; + string constant LICENSOR_APPROVAL = "LICENSOR_APPROVAL"; + string constant FORMAT_CATEGORY = "FORMAT_CATEGORY"; + string constant LICENSOR_IPORG_OR_PARENT = "LICENSOR_IPORG_OR_PARENT"; } + +library TermsData { + enum LicensorConfig { + Unset, + IpOrg, + ParentLicensee + } +} \ No newline at end of file diff --git a/contracts/modules/licensing/LicenseCreatorModule.sol b/contracts/modules/licensing/LicenseCreatorModule.sol deleted file mode 100644 index d5da5ab7..00000000 --- a/contracts/modules/licensing/LicenseCreatorModule.sol +++ /dev/null @@ -1,389 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.19; - -import { Licensing } from "contracts/lib/modules/Licensing.sol"; -import { Errors } from "contracts/lib/Errors.sol"; -import { ModuleRegistryKeys } from "contracts/lib/modules/ModuleRegistryKeys.sol"; -import { LibRelationship } from "contracts/lib/modules/LibRelationship.sol"; -import { BaseModule } from "contracts/modules/base/BaseModule.sol"; -import { IIPOrg } from "contracts/interfaces/ip-org/IIPOrg.sol"; -import { RelationshipModule } from "../relationships/RelationshipModule.sol"; -import { TermsRepository } from "./TermsRepository.sol"; -import { ShortString, ShortStrings } from "@openzeppelin/contracts/utils/ShortStrings.sol"; -import { FixedSet } from "contracts/utils/FixedSet.sol"; -import { IPAsset } from "contracts/lib/IPAsset.sol"; -import { TermIds } from "contracts/lib/modules/ProtocolLicensingTerms.sol"; - -/// @title License Creator module -/// @notice Story Protocol module that: -/// - Enables each IP Org to select a collection of terms from the TermsRepository to form -/// their licensing framework. -/// - Enables Other modules to attach licensing terms to IPAs -/// - Enables license holders to create derivative licenses -contract LicenseCreatorModule is BaseModule, TermsRepository { - using ShortStrings for *; - using FixedSet for FixedSet.ShortStringSet; - - // NOTE: emitting this event can be very expensive, check if terms can be indexed some - // other way - event IpOrgTermsSet(address indexed ipOrg, bool commercial, ShortString[] termIds, bytes[] termData); - - /// Per ipOrg commercial licensing term Ids. - mapping(address => FixedSet.ShortStringSet) private _comIpOrgTermIds; - /// Per ipOrg data to configure commercial licensing terms, corresponding to the ids. - mapping(address => bytes[]) private _comIpOrgTermData; - /// Per ipOrg non-commercial licensing term Ids. - mapping(address => FixedSet.ShortStringSet) private _nonComIpOrgTermIds; - /// Per ipOrg data to configure non-commercial licensing terms, corresponding to the ids. - mapping(address => bytes[]) private _nonComIpOrgTermData; - - constructor(ModuleConstruction memory params_) BaseModule(params_) {} - - /// Returns true if the ipOrg has commercial terms configured, false otherwise - function ipOrgAllowsCommercial(address ipOrg_) public view returns (bool) { - return _comIpOrgTermIds[ipOrg_].length() > 0; - } - - /// Get all term ids configured for an ipOrg, along the config data - /// @dev WARNING: this will copy all terms to memory, it can be expensive - /// @param commercial true for commercial terms, false for non-commercial terms - function getIpOrgTerms(bool commercial, address ipOrg_) public view returns (ShortString[] memory, bytes[] memory) { - if (commercial) { - return ( - _comIpOrgTermIds[ipOrg_].values(), - _comIpOrgTermData[ipOrg_] - ); - } else { - return ( - _nonComIpOrgTermIds[ipOrg_].values(), - _nonComIpOrgTermData[ipOrg_] - ); - } - } - - /// Get the number of terms configured for an ipOrg - /// @param commercial_ true for commercial terms, false for non-commercial terms - /// @param ipOrg_ the ipOrg address - /// @return the number of terms configured for the ipOrg - function getTotalIpOrgTerms(bool commercial_, address ipOrg_) public view returns (uint256) { - if (commercial_) { - return _comIpOrgTermIds[ipOrg_].length(); - } else { - return _nonComIpOrgTermIds[ipOrg_].length(); - } - } - - /// Check if an ipOrg has a term configured - /// @param commercial_ true for commercial terms, false for non-commercial terms - /// @param ipOrg_ the ipOrg address - /// @param termId_ the term id - /// @return true if the term is configured, false otherwise - function ipOrgTermsContains(bool commercial_, address ipOrg_, ShortString termId_) public view returns (bool) { - if (commercial_) { - return _comIpOrgTermIds[ipOrg_].contains(termId_); - } else { - return _nonComIpOrgTermIds[ipOrg_].contains(termId_); - } - } - - /// Get the data for a term configured for an ipOrg - /// @dev method will revert if the term is not configured - /// @param commercial_ true for commercial terms, false for non-commercial terms - /// @param ipOrg_ the ipOrg address - /// @param termId_ the term id - /// @return the term data - function ipOrgTermData(bool commercial_, address ipOrg_, ShortString termId_) public view returns (bytes memory) { - if (commercial_) { - uint256 index = _comIpOrgTermIds[ipOrg_].indexOf(termId_); - if (index == type(uint256).max) { - revert Errors.LicensingModule_ipOrgTermNotFound(); - } - return _comIpOrgTermData[ipOrg_][index]; - } else { - uint256 index = _nonComIpOrgTermIds[ipOrg_].indexOf(termId_); - if (index == type(uint256).max) { - revert Errors.LicensingModule_ipOrgTermNotFound(); - } - return _nonComIpOrgTermData[ipOrg_][index]; - } - } - - /// Gets the pair of ipOrg term Id and data at a certain index - /// @param commercial_ true for commercial terms, false for non-commercial terms - /// @param ipOrg_ the ipOrg address - /// @param index_ the index - /// @return termId term Id - /// @return data the term data - function ipOrgTermsAt(bool commercial_, address ipOrg_, uint index_) public view returns (ShortString termId, bytes memory data) { - if (commercial_) { - return (_comIpOrgTermIds[ipOrg_].at(index_), _comIpOrgTermData[ipOrg_][index_]); - } else { - return (_nonComIpOrgTermIds[ipOrg_].at(index_), _nonComIpOrgTermData[ipOrg_][index_]); - } - } - - //////////////////////////////////////////////////////////////////////////// - // Create License // - //////////////////////////////////////////////////////////////////////////// - - /// Module entrypoing to verify execution call - function _verifyExecution( - IIPOrg ipOrg_, - address caller_, - bytes calldata params_ - ) virtual internal override { - ( - Licensing.LicenseCreation memory lParams, - Licensing.LicenseeType licenseeType, - bytes memory data - ) = abi.decode( - params_, - ( - Licensing.LicenseCreation, - Licensing.LicenseeType, - bytes - ) - ); - // At least non commercial terms must be set - if (_nonComIpOrgTermData[address(ipOrg_)].length == 0) { - revert Errors.LicensingModule_IpOrgNotConfigured(); - } - // ------ Commercial status checks ------ - if (!ipOrgAllowsCommercial(address(ipOrg_)) && lParams.isCommercial) { - revert Errors.LicensingModule_CommercialLicenseNotAllowed(); - } - if (licenseeType == Licensing.LicenseeType.Unset) { - revert Errors.LicensingModule_InvalidLicenseeType(); - } - // ------ Root Ipa license checks ------ - if (lParams.parentLicenseId == 0) { - if (ipOrg_.owner() != caller_) { - revert Errors.LicensingModule_CallerNotIpOrgOwner(); - } - } else { - // ------ Derivative license checks: Terms ------ - FixedSet.ShortStringSet storage termIds = _getIpOrgTermIds(lParams.isCommercial, address(ipOrg_)); - bytes[] storage termData = _getIpOrgTermData(lParams.isCommercial, address(ipOrg_)); - - // Share Alike ---- - uint256 nftShareAlikeIndex = termIds.indexOf(TermIds.NFT_SHARE_ALIKE.toShortString()); - // If there is no NFT_SHARE_ALIKE term, or if it is false then we cannot have - // a derivative license unless caller owns the parent license - if (nftShareAlikeIndex == FixedSet.INDEX_NOT_FOUND || - !abi.decode(termData[nftShareAlikeIndex], (bool)) - ) { - address parentLicensor = LICENSE_REGISTRY.getLicensor(lParams.parentLicenseId); - if (parentLicensor != caller_) { - revert Errors.LicensingModule_ShareAlikeDisabled(); - } - } - } - } - - /// Module entrypoint to create licenses - function _performAction( - IIPOrg ipOrg_, - address caller_, - bytes calldata params_ - ) virtual internal override returns (bytes memory result) { - ( - Licensing.LicenseCreation memory lParams, - Licensing.LicenseeType licenseeType, - bytes memory data - ) = abi.decode( - params_, - ( - Licensing.LicenseCreation, - Licensing.LicenseeType, - bytes - ) - ); - (ShortString[] memory termIds, bytes[] memory termsData) = getIpOrgTerms( - lParams.isCommercial, - address(ipOrg_) - ); - uint256 ipaId = Licensing.LicenseeType.BoundToIpa == licenseeType ? abi.decode(data, (uint256)) : 0; - // TODO: compose IpOrg terms with user provider Terms - Licensing.RegistryAddition memory rParams = Licensing.RegistryAddition({ - isCommercial: lParams.isCommercial, - licensor: _getLicensor( - ipaId, - LICENSE_REGISTRY.getLicensor(lParams.parentLicenseId) - ), - revoker: _getRevoker(ipOrg_), - ipOrg: address(ipOrg_), - parentLicenseId: lParams.parentLicenseId, - termIds: termIds, - termsData: termsData, - data: data - }); - uint256 licenseId; - if (licenseeType == Licensing.LicenseeType.BoundToIpa) { - licenseId = LICENSE_REGISTRY.addBoundToIpaLicense( - rParams, - abi.decode(data, (uint256)) - ); - } else { - licenseId = LICENSE_REGISTRY.addTradeableLicense( - rParams, - abi.decode(data, (address)) - ); - } - - return abi.encode(licenseId); - } - - /// Gets the licensor address for this IPA. - function _getLicensor( - uint256 ipaId, - address parentLicenseOwner - ) private view returns (address) { - // TODO: Check for Licensor term in terms registry. - if (parentLicenseOwner != address(0) || ipaId == 0) { - return parentLicenseOwner; - } - return IPA_REGISTRY.ipAssetOwner(ipaId); - } - - /// Gets the revoker address for this IPOrg. - function _getRevoker(IIPOrg ipOrg) private view returns (address) { - // TODO: Check Revoker term in terms registry to chose disputer - // For now, ipOrgOwner - return ipOrg.owner(); - } - - /// Helper method to get a storage pointer to term Ids - function _getIpOrgTermIds(bool commercial, address ipOrg_) private view returns (FixedSet.ShortStringSet storage) { - if (commercial) { - return _comIpOrgTermIds[ipOrg_]; - } else { - return _nonComIpOrgTermIds[ipOrg_]; - } - } - - /// Helper method to get a storage pointer to term data - function _getIpOrgTermData(bool commercial, address ipOrg_) private view returns (bytes[] storage) { - if (commercial) { - return _comIpOrgTermData[ipOrg_]; - } else { - return _nonComIpOrgTermData[ipOrg_]; - } - } - - //////////////////////////////////////////////////////////////////////////// - // Config // - //////////////////////////////////////////////////////////////////////////// - - /// Module entrypoint for configuration. It allows an IPOrg to set licensing term - function _configure( - IIPOrg ipOrg_, - address caller_, - bytes calldata params_ - ) virtual override internal returns (bytes memory) { - // TODO: Revert if terms already exist - (bytes32 configType, bytes memory configData) = abi.decode(params_, (bytes32, bytes)); - if (configType == Licensing.LICENSING_FRAMEWORK_CONFIG) { - return _setIpOrgFramework(ipOrg_, caller_, configData); - } else { - // TODO: We need to define if a license holder can modify the terms of a license - } - revert Errors.LicensingModule_InvalidConfigType(); - } - - //////////////////////////////////////////////////////////////////////////// - // ipOrgConfig // - //////////////////////////////////////////////////////////////////////////// - - /// Gets commercial and non-commercial terms, and checks for misconfigurations in them before - // setting them - /// @param ipOrg_ the ipOrg contract interface - /// @param caller_ address requesting execution - /// @param params_ encoded Licensing.FrameworkConfig struct - function _setIpOrgFramework( - IIPOrg ipOrg_, - address caller_, - bytes memory params_ - ) virtual internal returns (bytes memory) { - if (ipOrg_.owner() != caller_) { - revert Errors.LicensingModule_CallerNotIpOrgOwner(); - } - address ipOrgAddress = address(ipOrg_); - - Licensing.FrameworkConfig memory framework = abi.decode(params_, (Licensing.FrameworkConfig)); - - // Set non-commercial terms - Licensing.TermsConfig memory nonComTermsConfig = framework.nonComTermsConfig; - // IP Org has to have non-commercial terms - if (nonComTermsConfig.termIds.length == 0) { - revert Errors.LicensingModule_NonCommercialTermsRequired(); - } - bytes[] storage nonComTermData = _nonComIpOrgTermData[ipOrgAddress]; - FixedSet.ShortStringSet storage nonComTermIds = _nonComIpOrgTermIds[ipOrgAddress]; - if (nonComTermIds.length() > 0) { - // We assume an ipOrg licensing framework cannot change, so if the terms are not empty - // we revert - revert Errors.LicensingModule_IpOrgFrameworkAlreadySet(); - } - _setTerms(false, nonComTermsConfig, nonComTermIds, nonComTermData); - emit IpOrgTermsSet(ipOrgAddress, false, nonComTermIds.values(), nonComTermData); - - Licensing.TermsConfig memory comTermsConfig = framework.comTermsConfig; - // Set commercial terms - bytes[] storage comTermData = _comIpOrgTermData[ipOrgAddress]; - FixedSet.ShortStringSet storage comTermIds = _comIpOrgTermIds[ipOrgAddress]; - _setTerms(true, comTermsConfig, comTermIds, comTermData); - emit IpOrgTermsSet(ipOrgAddress, true, comTermIds.values(), comTermData); - - return ""; - } - - /// Validate input licensing terms and populate ipOrg licensing framework - /// @param commercial true for commercial terms, false for non-commercial terms - /// @param termsConfig_ arrays for termIds and their ipOrg level config data - /// @param termIds_ ipOrg terms set, where the termIds will be added - /// @param ipOrgTermData_ ipOrg config data for terms, where the term data will be added - function _setTerms( - bool commercial, - Licensing.TermsConfig memory termsConfig_, - FixedSet.ShortStringSet storage termIds_, - bytes[] storage ipOrgTermData_ - ) internal { - uint256 termsLength = termsConfig_.termIds.length; - for (uint256 i = 0; i < termsLength; i++) { - ShortString termId = termsConfig_.termIds[i]; - if (termIds_.contains(termId)) { - revert Errors.LicensingModule_DuplicateTermId(); - } - Licensing.LicensingTerm memory term = getTerm(termId); - // Since there is CommercialStatus.Both, we need to be specific here - if ( - commercial && term.comStatus == Licensing.CommercialStatus.NonCommercial || - !commercial && term.comStatus == Licensing.CommercialStatus.Commercial - ) { - // We assume that CommercialStatus.Unset is not possible, since - // TermsRepository checks for that - revert Errors.LicensingModule_InvalidTermCommercialStatus(); - } - bytes memory data = termsConfig_.termData[i]; - if (address(term.hook) != address(0)) { - // Reverts if decoding fails - term.hook.validateConfig(abi.encode(termId, data)); - } - termIds_.add(termId); - ipOrgTermData_.push(data); - } - } - - //////////////////////////////////////////////////////////////////////////// - // Hooks // - //////////////////////////////////////////////////////////////////////////// - - function _hookRegistryKey( - IIPOrg ipOrg_, - address caller_, - bytes calldata params_ - ) internal view virtual override returns(bytes32) { - return keccak256("TODO"); - } - -} diff --git a/contracts/modules/licensing/LicenseRegistry.sol b/contracts/modules/licensing/LicenseRegistry.sol index 91bf43cb..6d695f74 100644 --- a/contracts/modules/licensing/LicenseRegistry.sol +++ b/contracts/modules/licensing/LicenseRegistry.sol @@ -8,6 +8,8 @@ import { ERC721 } from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import { ModuleRegistry } from "contracts/modules/ModuleRegistry.sol"; import { ModuleRegistryKeys } from "contracts/lib/modules/ModuleRegistryKeys.sol"; +import "forge-std/console2.sol"; + /// @title LicenseRegistry /// @notice This contract is the source of truth for all licenses that are registered in the protocol. /// It will only be called by licensing modules. @@ -15,29 +17,44 @@ import { ModuleRegistryKeys } from "contracts/lib/modules/ModuleRegistryKeys.sol /// Licenses can be made invalid by the revoker, according to the terms of the license. contract LicenseRegistry is ERC721 { // TODO: Figure out data needed for indexing - event LicenseRegistered( - uint256 indexed id - ); + event LicenseRegistered(uint256 indexed id); event LicenseNftBoundedToIpa( uint256 indexed licenseId, uint256 indexed ipaId ); + event LicenseActivated(uint256 indexed licenseId); + event LicenseRevoked(uint256 indexed licenseId); /// license Id => License mapping(uint256 => Licensing.License) private _licenses; - /// counder for license Ids + /// counter for license Ids uint256 private _licenseCount; IPAssetRegistry public immutable IPA_REGISTRY; ModuleRegistry public immutable MODULE_REGISTRY; modifier onlyLicensingModule() { - if (!MODULE_REGISTRY.isModule(ModuleRegistryKeys.LICENSING_MODULE, msg.sender)) { + if ( + !MODULE_REGISTRY.isModule( + ModuleRegistryKeys.LICENSING_MODULE, + msg.sender + ) + ) { revert Errors.LicenseRegistry_CallerNotLicensingModule(); } _; } + modifier onlyActiveOrPending(Licensing.LicenseStatus status_) { + if ( + status_ != Licensing.LicenseStatus.Active && + status_ != Licensing.LicenseStatus.Pending + ) { + revert Errors.LicenseRegistry_InvalidLicenseStatus(); + } + _; + } + constructor( address ipaRegistry_, address moduleRegistry_ @@ -59,25 +76,30 @@ contract LicenseRegistry is ERC721 { function addBoundToIpaLicense( Licensing.RegistryAddition memory params_, uint256 ipaId_ - ) external onlyLicensingModule returns (uint256) { - // TODO statuses + ) + external + onlyLicensingModule + onlyActiveOrPending(params_.status) + returns (uint256) + { if (IPA_REGISTRY.status(ipaId_) == 0) { revert Errors.LicenseRegistry_InvalidIpa(); } - return _addLicense( - Licensing.License({ - isCommercial: params_.isCommercial, - licenseeType: Licensing.LicenseeType.BoundToIpa, - licensor: params_.licensor, - revoker: params_.revoker, - ipOrg: params_.ipOrg, - termIds: params_.termIds, - termsData: params_.termsData, - ipaId: ipaId_, - parentLicenseId: params_.parentLicenseId, - data: params_.data - }) - ); + return + _addLicense( + Licensing.License({ + isCommercial: params_.isCommercial, + status: params_.status, + licenseeType: Licensing.LicenseeType.BoundToIpa, + licensor: params_.licensor, + revoker: params_.revoker, + ipOrg: params_.ipOrg, + termIds: params_.termIds, + termsData: params_.termsData, + ipaId: ipaId_, + parentLicenseId: params_.parentLicenseId + }) + ); } /// Creates a tradeable License NFT. @@ -88,10 +110,16 @@ contract LicenseRegistry is ERC721 { function addTradeableLicense( Licensing.RegistryAddition memory params_, address licensee_ - ) external onlyLicensingModule returns (uint256) { + ) + external + onlyLicensingModule + onlyActiveOrPending(params_.status) + returns (uint256) + { _addLicense( Licensing.License({ isCommercial: params_.isCommercial, + status: params_.status, licenseeType: Licensing.LicenseeType.LNFTHolder, licensor: params_.licensor, revoker: params_.revoker, @@ -99,25 +127,26 @@ contract LicenseRegistry is ERC721 { termIds: params_.termIds, termsData: params_.termsData, ipaId: 0, - parentLicenseId: params_.parentLicenseId, - data: params_.data + parentLicenseId: params_.parentLicenseId }) ); _mint(licensee_, _licenseCount); return _licenseCount; } - - function _addLicense(Licensing.License memory license_) private returns (uint256) { - // TODO: Check valid parent license - _licenseCount++; - _licenses[_licenseCount] = license_; + function _addLicense( + Licensing.License memory license_ + ) private returns (uint256) { + // Note: Valid parent license must be checked in Licensing module + _licenses[++_licenseCount] = license_; emit LicenseRegistered(_licenseCount); return _licenseCount; } /// Gets License struct for input id - function getLicense(uint256 id_) external view returns (Licensing.License memory) { + function getLicense( + uint256 id_ + ) external view returns (Licensing.License memory) { return _licenses[id_]; } @@ -125,17 +154,17 @@ contract LicenseRegistry is ERC721 { function getLicensor(uint256 id_) external view returns (address) { return _licenses[id_].licensor; } + /// Gets the address a license is granted to /// @param id_ of the license /// @return licensee address, NFT owner if the license is tradeable, or IPA owner if bound to IPA function getLicensee(uint256 id_) external view returns (address) { - Licensing.LicenseeType licenseeType_ = _licenses[id_].licenseeType; - if (licenseeType_ == Licensing.LicenseeType.Unset) { + Licensing.License storage license = _licenses[id_]; + if (license.licenseeType == Licensing.LicenseeType.Unset) { revert Errors.LicenseRegistry_UnknownLicenseId(); - } - if (_licenses[id_].licenseeType == Licensing.LicenseeType.BoundToIpa) { - return IPA_REGISTRY.ipAssetOwner(id_); - } else { + } else if (license.licenseeType == Licensing.LicenseeType.BoundToIpa) { + return IPA_REGISTRY.ipAssetOwner(license.ipaId); + } else { return ownerOf(id_); } } @@ -143,7 +172,10 @@ contract LicenseRegistry is ERC721 { /// Burns a license NFT and binds the license to an IPA /// @param licenseId_ id of the license NFT /// @param ipaId_ id of the IPA - function boundLnftToIpa(uint256 licenseId_, uint256 ipaId_) external onlyLicensingModule { + function bindLnftToIpa( + uint256 licenseId_, + uint256 ipaId_ + ) external onlyLicensingModule { Licensing.License memory license_ = _licenses[licenseId_]; if (license_.licenseeType != Licensing.LicenseeType.LNFTHolder) { revert Errors.LicenseRegistry_NotLicenseNFT(); @@ -153,4 +185,40 @@ contract LicenseRegistry is ERC721 { _burn(licenseId_); emit LicenseNftBoundedToIpa(licenseId_, ipaId_); } + + /// Checks if a license is active. If an ancestor is not active, the license is not active + /// NOTE: this method needs to be optimized, or moved to a merkle tree/scalability solution + function isLicenseActive(uint256 licenseId_) public view returns (bool) { + // NOTE: should IPA status check this? + if (licenseId_ == 0) return false; + while (licenseId_ != 0) { + if (_licenses[licenseId_].status != Licensing.LicenseStatus.Active) + return false; + licenseId_ = _licenses[licenseId_].parentLicenseId; + } + return true; + } + + /// Called by the licensing module to activate a license, after all the activation terms pass + /// @param licenseId_ id of the license + function activateLicense(uint256 licenseId_) external onlyLicensingModule { + if (_licenses[licenseId_].status != Licensing.LicenseStatus.Pending) { + revert Errors.LicenseRegistry_LicenseNotPending(); + } + _licenses[licenseId_].status = Licensing.LicenseStatus.Active; + // TODO: change IPA status + emit LicenseActivated(licenseId_); + } + + /// Revokes a license, making it incactive. Only the revoker can do this. + /// NOTE: revoking licenses in an already inactive chain should be incentivized, since it + /// reduces the while loop iterations. + function revokeLicense(uint256 licenseId_) external { + if (msg.sender != _licenses[licenseId_].revoker) { + revert Errors.LicenseRegistry_CallerNotRevoker(); + } + _licenses[licenseId_].status = Licensing.LicenseStatus.Revoked; + // TODO: change IPA status + emit LicenseRevoked(licenseId_); + } } diff --git a/contracts/modules/licensing/LicensingModule.sol b/contracts/modules/licensing/LicensingModule.sol new file mode 100644 index 00000000..b26c5e2d --- /dev/null +++ b/contracts/modules/licensing/LicensingModule.sol @@ -0,0 +1,581 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.19; + +import { Licensing } from "contracts/lib/modules/Licensing.sol"; +import { Errors } from "contracts/lib/Errors.sol"; +import { ModuleRegistryKeys } from "contracts/lib/modules/ModuleRegistryKeys.sol"; +import { LibRelationship } from "contracts/lib/modules/LibRelationship.sol"; +import { BaseModule } from "contracts/modules/base/BaseModule.sol"; +import { IIPOrg } from "contracts/interfaces/ip-org/IIPOrg.sol"; +import { RelationshipModule } from "../relationships/RelationshipModule.sol"; +import { TermsRepository } from "./TermsRepository.sol"; +import { ShortString, ShortStrings } from "@openzeppelin/contracts/utils/ShortStrings.sol"; +import { FixedSet } from "contracts/utils/FixedSet.sol"; +import { IPAsset } from "contracts/lib/IPAsset.sol"; +import { TermIds, TermsData } from "contracts/lib/modules/ProtocolLicensingTerms.sol"; +import { ShortStringOps } from "contracts/utils/ShortStringOps.sol"; + + +/// @title Licensing module +/// @notice Story Protocol module that: +/// - Enables each IP Org to select a collection of terms from the TermsRepository to form +/// their licensing framework. +/// - Enables Other modules to attach licensing terms to IPAs +/// - Enables license holders to create derivative licenses +/// Thanks to ERC-5218 authors for inspiration (see https://eips.ethereum.org/EIPS/eip-5218) +contract LicensingModule is BaseModule { + using ShortStrings for *; + using FixedSet for FixedSet.ShortStringSet; + + // NOTE: emitting this event can be very expensive, check if terms can be indexed some + // other way + event IpOrgTermsSet( + address indexed ipOrg, + bool commercial, + ShortString[] termIds, + bytes[] termData + ); + + /// Per ipOrg licensing term Ids. + mapping(bytes32 => FixedSet.ShortStringSet) private _ipOrgTermIds; + /// Per ipOrg data to configure licensing terms, corresponding to the ids. + mapping(bytes32 => bytes[]) private _ipOrgTermData; + mapping(bytes32 => bool) private _shareAlike; + mapping(bytes32 => TermsData.LicensorConfig) private _licensorConfig; + // TODO: support different activation terms on chain + mapping(bytes32 => bool) private _licensorApprovalNeeded; + + TermsRepository public immutable TERMS_REPOSITORY; + + constructor(ModuleConstruction memory params_, address termRepository_) BaseModule(params_) { + if (termRepository_ == address(0)) { + revert Errors.ZeroAddress(); + } + TERMS_REPOSITORY = TermsRepository(termRepository_); + } + + /// Returns true if the share alike term is on for this ipOrg and commercial status, + function isShareAlikeOn( + bool commercial_, + address ipOrg_ + ) external view returns (bool) { + return _shareAlike[_getTermsKey(commercial_, ipOrg_)]; + } + + /// Returns the licensor config for an ipOrg and commercial status + function getLicensorConfig( + bool commercial_, + address ipOrg_ + ) external view returns (TermsData.LicensorConfig) { + return _licensorConfig[_getTermsKey(commercial_, ipOrg_)]; + } + + /// Returns true if the licensor approval is needed for this ipOrg and commercial + /// status, false otherwise + function isLicensorAppovalOn( + bool commercial_, + address ipOrg_ + ) external view returns (bool) { + return _licensorApprovalNeeded[_getTermsKey(commercial_, ipOrg_)]; + } + + /// Returns true if the ipOrg has commercial terms configured, false otherwise + function ipOrgAllowsCommercial(address ipOrg_) public view returns (bool) { + return _ipOrgTermIds[_getTermsKey(true, ipOrg_)].length() > 0; + } + + /// Get all term ids configured for an ipOrg, along the config data + /// @dev WARNING: this will copy all term ids to memory, it can be expensive + function getIpOrgTerms( + bool commercial_, + address ipOrg_ + ) public view returns (ShortString[] memory, bytes[] memory) { + return ( + _ipOrgTermIds[_getTermsKey(commercial_, ipOrg_)].values(), + _ipOrgTermData[_getTermsKey(commercial_, ipOrg_)] + ); + } + + /// Get the number of terms configured for an ipOrg + /// @param commercial_ true for commercial terms, false for non-commercial terms + /// @param ipOrg_ the ipOrg address + /// @return the number of terms configured for the ipOrg + function getTotalIpOrgTerms( + bool commercial_, + address ipOrg_ + ) public view returns (uint256) { + return _ipOrgTermIds[_getTermsKey(commercial_, ipOrg_)].length(); + } + + /// Check if an ipOrg has a term configured + /// @param commercial_ true for commercial terms, false for non-commercial terms + /// @param ipOrg_ the ipOrg address + /// @param termId_ the term id + /// @return true if the term is configured, false otherwise + function ipOrgTermsContains( + bool commercial_, + address ipOrg_, + ShortString termId_ + ) public view returns (bool) { + return + _ipOrgTermIds[_getTermsKey(commercial_, ipOrg_)].contains(termId_); + } + + /// Get the data for a term configured for an ipOrg + /// @dev method will revert if the term is not configured + /// @param commercial_ true for commercial terms, false for non-commercial terms + /// @param ipOrg_ the ipOrg address + /// @param termId_ the term id + /// @return the term data + function ipOrgTermData( + bool commercial_, + address ipOrg_, + ShortString termId_ + ) public view returns (bytes memory) { + bytes32 key = _getTermsKey(commercial_, ipOrg_); + FixedSet.ShortStringSet storage termIds = _ipOrgTermIds[key]; + bytes[] storage termData = _ipOrgTermData[key]; + uint256 index = termIds.indexOf(termId_); + if (index == FixedSet.INDEX_NOT_FOUND) { + revert Errors.LicensingModule_ipOrgTermNotFound(); + } + return termData[index]; + } + + /// Gets the pair of ipOrg term Id and data at a certain index + /// @param commercial_ true for commercial terms, false for non-commercial terms + /// @param ipOrg_ the ipOrg address + /// @param index_ the index + /// @return termId term Id + /// @return data the term data + function ipOrgTermsAt( + bool commercial_, + address ipOrg_, + uint index_ + ) public view returns (ShortString termId, bytes memory data) { + bytes32 key = _getTermsKey(commercial_, ipOrg_); + return (_ipOrgTermIds[key].at(index_), _ipOrgTermData[key][index_]); + } + + //////////////////////////////////////////////////////////////////////////// + // Create License // + //////////////////////////////////////////////////////////////////////////// + + /// Module entrypoing to verify execution call + function _verifyExecution( + IIPOrg ipOrg_, + address caller_, + bytes calldata params_ + ) internal virtual override { + // At least non commercial terms must be set + if (getTotalIpOrgTerms(false, address(ipOrg_)) == 0) { + revert Errors.LicensingModule_IpOrgNotConfigured(); + } + (bytes32 action, bytes memory params) = abi.decode( + params_, + (bytes32, bytes) + ); + if (action == Licensing.CREATE_LICENSE) { + _verifyCreateLicense(ipOrg_, caller_, params); + } else if (action == Licensing.ACTIVATE_LICENSE) { + _verifyActivateLicense(ipOrg_, caller_, params); + } else if (action == Licensing.BOND_LNFT_TO_IPA) { + _verifyBondNftToIpa(ipOrg_, caller_, params); + } else { + revert Errors.LicensingModule_InvalidAction(); + } + } + + function _verifyCreateLicense( + IIPOrg ipOrg_, + address caller_, + bytes memory params_ + ) private view { + ( + Licensing.LicenseCreation memory lParams, + Licensing.LicenseeType licenseeType, + bytes memory data + ) = abi.decode( + params_, + (Licensing.LicenseCreation, Licensing.LicenseeType, bytes) + ); + // ------ Commercial status checks ------ + if (!ipOrgAllowsCommercial(address(ipOrg_)) && lParams.isCommercial) { + revert Errors.LicensingModule_CommercialLicenseNotAllowed(); + } + // ------ Misconfiguration ------ + if (licenseeType == Licensing.LicenseeType.Unset) { + revert Errors.LicensingModule_InvalidLicenseeType(); + } + // ------ Derivative license checks ------ + if (lParams.parentLicenseId != 0) { + if (!LICENSE_REGISTRY.isLicenseActive(lParams.parentLicenseId)) { + revert Errors.LicensingModule_ParentLicenseNotActive(); + } + // If no share alike, only the parent licensee can create a derivative license + if ( + !_shareAlike[ + _getTermsKey(lParams.isCommercial, address(ipOrg_)) + ] + ) { + if ( + caller_ != + LICENSE_REGISTRY.getLicensee(lParams.parentLicenseId) + ) { + revert Errors.LicensingModule_ShareAlikeDisabled(); + } + } + } + } + + function _verifyActivateLicense( + IIPOrg ipOrg_, + address caller_, + bytes memory params_ + ) private view { + uint256 licenseId = abi.decode(params_, (uint256)); + Licensing.License memory license = LICENSE_REGISTRY.getLicense( + licenseId + ); + if (caller_ != license.licensor) { + revert Errors.LicensingModule_CallerNotLicensor(); + } + if ( + license.parentLicenseId != 0 && + !LICENSE_REGISTRY.isLicenseActive(license.parentLicenseId) + ) { + revert Errors.LicensingModule_ParentLicenseNotActive(); + } + } + + function _verifyBondNftToIpa( + IIPOrg ipOrg_, + address caller_, + bytes memory params_ + ) private view { + (uint256 licenseId, uint256 ipaId) = abi.decode( + params_, + (uint256, uint256) + ); + if (caller_ != LICENSE_REGISTRY.ownerOf(licenseId)) { + revert Errors.LicensingModule_CallerNotLicenseOwner(); + } + if (!LICENSE_REGISTRY.isLicenseActive(licenseId)) { + revert Errors.LicensingModule_ParentLicenseNotActive(); + } + if (IPA_REGISTRY.status(ipaId) == 0) { + revert Errors.LicensingModule_InvalidIpa(); + } + } + + /// Module entrypoint to create licenses + function _performAction( + IIPOrg ipOrg_, + address caller_, + bytes calldata params_ + ) internal virtual override returns (bytes memory result) { + (bytes32 action, bytes memory actionParams) = abi.decode( + params_, + (bytes32, bytes) + ); + if (action == Licensing.CREATE_LICENSE) { + return _createLicense(ipOrg_, caller_, actionParams); + } else if (action == Licensing.ACTIVATE_LICENSE) { + return _activateLicense(ipOrg_, caller_, actionParams); + } else if (action == Licensing.BOND_LNFT_TO_IPA) { + (uint256 licenseId, uint256 ipaId) = abi.decode( + actionParams, + (uint256, uint256) + ); + LICENSE_REGISTRY.bindLnftToIpa(licenseId, ipaId); + return bytes(""); + } else { + revert Errors.LicensingModule_InvalidAction(); + } + } + + function _createLicense( + IIPOrg ipOrg_, + address caller_, + bytes memory params_ + ) private returns (bytes memory result) { + ( + Licensing.LicenseCreation memory lParams, + Licensing.LicenseeType licenseeType, + bytes memory data + ) = abi.decode( + params_, + (Licensing.LicenseCreation, Licensing.LicenseeType, bytes) + ); + + uint256 ipaId = Licensing.LicenseeType.BoundToIpa == licenseeType + ? abi.decode(data, (uint256)) + : 0; + // TODO: compose IpOrg terms with user provider Terms + Licensing.RegistryAddition memory rParams = _getRegistryAddition( + lParams, + address(ipOrg_), + ipaId + ); + uint256 licenseId; + // Create the licenses + if (licenseeType == Licensing.LicenseeType.BoundToIpa) { + licenseId = LICENSE_REGISTRY.addBoundToIpaLicense( + rParams, + abi.decode(data, (uint256)) + ); + } else { + licenseId = LICENSE_REGISTRY.addTradeableLicense( + rParams, + abi.decode(data, (address)) + ); + } + return abi.encode(licenseId); + } + + function _getRegistryAddition( + Licensing.LicenseCreation memory lParams_, + address ipOrg_, + uint256 ipaId_ + ) private view returns (Licensing.RegistryAddition memory) { + bytes32 termsKey = _getTermsKey(lParams_.isCommercial, ipOrg_); + ShortString[] memory termIds = _ipOrgTermIds[termsKey].values(); + bytes[] memory termsData = _ipOrgTermData[termsKey]; + Licensing.LicenseStatus status = Licensing.LicenseStatus.Active; + if (_licensorApprovalNeeded[termsKey]) { + status = Licensing.LicenseStatus.Pending; + } + return + Licensing.RegistryAddition({ + isCommercial: lParams_.isCommercial, + status: status, + licensor: _getLicensor( + lParams_.parentLicenseId, + ipaId_, + lParams_.isCommercial, + ipOrg_ + ), + revoker: _getRevoker(ipOrg_), + ipOrg: ipOrg_, + parentLicenseId: lParams_.parentLicenseId, + termIds: termIds, + termsData: termsData + }); + } + + function _activateLicense( + IIPOrg ipOrg_, + address caller_, + bytes memory params_ + ) private returns (bytes memory result) { + uint256 licenseId = abi.decode(params_, (uint256)); + Licensing.License memory license = LICENSE_REGISTRY.getLicense( + licenseId + ); + // For now, we just support activating license with an explicit approval from + // Licensor. TODO: support more activation terms + LICENSE_REGISTRY.activateLicense(licenseId); + } + + /// Gets the licensor address for this IPA. + function _getLicensor( + uint256 parentLicenseId_, + uint256 ipa_, + bool commercial_, + address ipOrg_ + ) private view returns (address) { + // TODO: Check for Licensor term in terms registry. + TermsData.LicensorConfig licensorConfig = _licensorConfig[ + _getTermsKey(commercial_, ipOrg_) + ]; + if (licensorConfig == TermsData.LicensorConfig.IpOrg) { + return IIPOrg(ipOrg_).owner(); + } else if (licensorConfig == TermsData.LicensorConfig.ParentLicensee) { + if (parentLicenseId_ == 0) { + if (ipa_ == 0) { + revert Errors + .LicensingModule_CantFindParentLicenseOrRelatedIpa(); + } + return IPA_REGISTRY.ipAssetOwner(ipa_); + } else { + return LICENSE_REGISTRY.getLicensee(parentLicenseId_); + } + } else { + revert Errors.LicensingModule_InvalidLicensorType(); + } + } + + /// Gets the revoker address for this IPOrg. + function _getRevoker(address ipOrg) private view returns (address) { + // TODO: Check Revoker term in terms registry to chose disputer + // For now, ipOrgOwner + return IIPOrg(ipOrg).owner(); + } + + //////////////////////////////////////////////////////////////////////////// + // Config // + //////////////////////////////////////////////////////////////////////////// + + /// Module entrypoint for configuration. It allows an IPOrg to set licensing term + function _configure( + IIPOrg ipOrg_, + address caller_, + bytes calldata params_ + ) internal virtual override returns (bytes memory) { + // TODO: Revert if terms already exist + (bytes32 configType, bytes memory configData) = abi.decode( + params_, + (bytes32, bytes) + ); + if (configType == Licensing.LICENSING_FRAMEWORK_CONFIG) { + return _setIpOrgFramework(ipOrg_, caller_, configData); + } else { + // TODO: We need to define if a license holder can modify the terms of a license + } + revert Errors.LicensingModule_InvalidConfigType(); + } + + //////////////////////////////////////////////////////////////////////////// + // ipOrgConfig // + //////////////////////////////////////////////////////////////////////////// + + /// Gets commercial and non-commercial terms, and checks for misconfigurations in them before + // setting them + /// @param ipOrg_ the ipOrg contract interface + /// @param caller_ address requesting execution + /// @param params_ encoded Licensing.FrameworkConfig struct + function _setIpOrgFramework( + IIPOrg ipOrg_, + address caller_, + bytes memory params_ + ) internal virtual returns (bytes memory) { + if (ipOrg_.owner() != caller_) { + revert Errors.LicensingModule_CallerNotIpOrgOwner(); + } + address ipOrgAddress = address(ipOrg_); + + Licensing.FrameworkConfig memory framework = abi.decode( + params_, + (Licensing.FrameworkConfig) + ); + + // Set non-commercial terms + Licensing.TermsConfig memory nonComTermsConfig = framework + .nonComTermsConfig; + // IP Org has to have non-commercial terms + if (nonComTermsConfig.termIds.length == 0) { + revert Errors.LicensingModule_NonCommercialTermsRequired(); + } + bytes32 nonComKey = _getTermsKey(false, ipOrgAddress); + bytes[] storage nonComTermData = _ipOrgTermData[nonComKey]; + FixedSet.ShortStringSet storage nonComTermIds = _ipOrgTermIds[ + nonComKey + ]; + if (nonComTermIds.length() > 0) { + // We assume an ipOrg licensing framework cannot change, so if the terms are not empty + // we revert + revert Errors.LicensingModule_IpOrgFrameworkAlreadySet(); + } + _setTerms( + false, + nonComKey, + nonComTermsConfig, + nonComTermIds, + nonComTermData + ); + emit IpOrgTermsSet( + ipOrgAddress, + false, + nonComTermIds.values(), + nonComTermData + ); + + Licensing.TermsConfig memory comTermsConfig = framework.comTermsConfig; + // Set commercial terms + bytes32 comKey = _getTermsKey(true, ipOrgAddress); + bytes[] storage comTermData = _ipOrgTermData[comKey]; + FixedSet.ShortStringSet storage comTermIds = _ipOrgTermIds[comKey]; + _setTerms(true, comKey, comTermsConfig, comTermIds, comTermData); + emit IpOrgTermsSet( + ipOrgAddress, + true, + comTermIds.values(), + comTermData + ); + + return ""; + } + + /// Validate input licensing terms and populate ipOrg licensing framework + /// @param commercial_ true for commercial terms, false for non-commercial terms + /// @param termsKey_ key to the ipOrg terms + /// @param termsConfig_ arrays for termIds and their ipOrg level config data + /// @param ipOrgTermIds_ ipOrg terms set, where the termIds will be added + /// @param ipOrgTermData_ ipOrg config data for terms, where the term data will be added + function _setTerms( + bool commercial_, + bytes32 termsKey_, + Licensing.TermsConfig memory termsConfig_, + FixedSet.ShortStringSet storage ipOrgTermIds_, + bytes[] storage ipOrgTermData_ + ) internal { + uint256 termsLength = termsConfig_.termIds.length; + for (uint256 i = 0; i < termsLength; i++) { + ShortString termId = termsConfig_.termIds[i]; + if (ipOrgTermIds_.contains(termId)) { + revert Errors.LicensingModule_DuplicateTermId(); + } + Licensing.LicensingTerm memory term = TERMS_REPOSITORY.getTerm(termId); + // Since there is CommercialStatus.Both, we need to be specific here + if ( + (commercial_ && + term.comStatus == + Licensing.CommercialStatus.NonCommercial) || + (!commercial_ && + term.comStatus == Licensing.CommercialStatus.Commercial) + ) { + // We assume that CommercialStatus.Unset is not possible, since + // TermsRepository checks for that + revert Errors.LicensingModule_InvalidTermCommercialStatus(); + } + bytes memory data = termsConfig_.termData[i]; + if (ShortStringOps._equal(termId, TermIds.NFT_SHARE_ALIKE)) { + _shareAlike[termsKey_] = abi.decode(data, (bool)); + } else if ( + ShortStringOps._equal(termId, TermIds.LICENSOR_IPORG_OR_PARENT) + ) { + _licensorConfig[termsKey_] = abi.decode( + data, + (TermsData.LicensorConfig) + ); + } else if ( + ShortStringOps._equal(termId, TermIds.LICENSOR_APPROVAL) + ) { + _licensorApprovalNeeded[termsKey_] = abi.decode(data, (bool)); + } + + // TODO: support hooks + ipOrgTermIds_.add(termId); + ipOrgTermData_.push(data); + } + } + + function _getTermsKey( + bool commercial_, + address ipOrg_ + ) private pure returns (bytes32) { + return keccak256(abi.encodePacked(ipOrg_, commercial_)); + } + + //////////////////////////////////////////////////////////////////////////// + // Hooks // + //////////////////////////////////////////////////////////////////////////// + + function _hookRegistryKey( + IIPOrg ipOrg_, + address caller_, + bytes calldata params_ + ) internal view virtual override returns (bytes32) { + return keccak256("TODO"); + } +} diff --git a/contracts/modules/licensing/ProtocolTermsHelper.sol b/contracts/modules/licensing/ProtocolTermsHelper.sol deleted file mode 100644 index d991e65e..00000000 --- a/contracts/modules/licensing/ProtocolTermsHelper.sol +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.19; - -import { Licensing } from "contracts/lib/modules/Licensing.sol"; -import { IHook } from "contracts/interfaces/hooks/base/IHook.sol"; - -library ProtocolTermsHelper { - - function _getExcludedCategoriesTerm( - Licensing.CommercialStatus comStatus_, - IHook hook - ) internal pure returns (Licensing.LicensingTerm memory) { - return Licensing.LicensingTerm({ - comStatus: comStatus_, - url: "https://excluded.com", - hash: "qwertyu", - algorithm: "sha256", - hook: hook - }); - } - - function _getNftShareAlikeTerm( - Licensing.CommercialStatus comStatus_ - ) internal pure returns (Licensing.LicensingTerm memory) { - return Licensing.LicensingTerm({ - comStatus: comStatus_, - url: "https://sharealike.com", - hash: "qwertyu", - algorithm: "sha256", - hook: IHook(address(0)) - }); - } - -} \ No newline at end of file diff --git a/contracts/modules/licensing/TermsRepository.sol b/contracts/modules/licensing/TermsRepository.sol index f171c70e..eacffdc8 100644 --- a/contracts/modules/licensing/TermsRepository.sol +++ b/contracts/modules/licensing/TermsRepository.sol @@ -7,10 +7,15 @@ import { ShortString, ShortStrings } from "@openzeppelin/contracts/utils/ShortSt import { Multicall } from "@openzeppelin/contracts/utils/Multicall.sol"; import { Errors } from "contracts/lib/Errors.sol"; import { IHook } from "contracts/interfaces/hooks/base/IHook.sol"; - -import "forge-std/console.sol"; - -contract TermsRepository is Multicall { +import { AccessControlled } from "contracts/access-control/AccessControlled.sol"; +import { AccessControl } from "contracts/lib/AccessControl.sol"; + +/// @title TermsRepository +/// @notice Protocol repository for terms that can be used by Licensing Modules to compose +/// licenses. Terms are grouped by categories, and each term has a unique id within its category. +/// Terms are added by the protocol. +/// The text of the terms is not stored in the contract, but in external storage. +contract TermsRepository is AccessControlled, Multicall { using EnumerableSet for EnumerableSet.Bytes32Set; using ShortStrings for *; @@ -42,41 +47,49 @@ contract TermsRepository is Multicall { _; } + constructor(address accessControl_) AccessControlled(accessControl_) { } + + /// Adds a new category of terms function addCategory(string calldata category_) public { _termCategories.add(ShortString.unwrap(category_.toShortString())); emit TermCategoryAdded(category_); } - function removeTermCategory(string calldata category_) public { + /// Removes a category of terms + function removeCategory(string calldata category_) public { _termCategories.remove(ShortString.unwrap(category_.toShortString())); emit TermCategoryRemoved(category_); } + /// Returns the total number of term categories function totalTermCategories() public view returns (uint256) { return _termCategories.length(); } + /// Returns the term category at the given index function termCategoryAt( uint256 index_ ) public view returns (string memory) { return ShortString.wrap(_termCategories.at(index_)).toString(); } - // TODO: access control + /// Adds a new term to a category + /// @param category_ The category to add the term to + /// @param termId_ The unique id of the term within the category + /// @param term_ The term definition function addTerm( string calldata category_, string calldata termId_, Licensing.LicensingTerm calldata term_ - ) public { + ) public onlyRole(AccessControl.TERMS_SETTER_ROLE) { + // TODO: access control ShortString category = category_.toShortString(); _verifyCategoryExists(category); if (term_.comStatus == Licensing.CommercialStatus.Unset) { - console.log("TermsRegistry_CommercialStatusUnset"); revert Errors.TermsRegistry_CommercialStatusUnset(); } ShortString termId = termId_.toShortString(); if (_terms[termId].comStatus != Licensing.CommercialStatus.Unset) { - console.log("TermsRegistry_TermAlreadyExists"); revert Errors.TermsRegistry_TermAlreadyExists(); } _terms[termId] = term_; @@ -84,24 +97,18 @@ contract TermsRepository is Multicall { emit TermAdded(category_, termId_); } - // TODO: access control - function disableTerm( - string calldata category_, - string calldata termId_ - ) public { - ShortString category = category_.toShortString(); - _verifyCategoryExists(category); - ShortString termId = termId_.toShortString(); - _termIdsByCategory[category].add(ShortString.unwrap(termId)); - emit TermDisabled(category_, termId_); - } - function categoryForTerm( string calldata termId_ ) public view returns (string memory) { return _termCategoryByTermId[termId_.toShortString()].toString(); } + function shortStringCategoryForTerm( + ShortString termId_ + ) public view returns (ShortString) { + return _termCategoryByTermId[termId_]; + } + function getTerm( ShortString termId_ ) public view onlyValidTerm(termId_) returns (Licensing.LicensingTerm memory) { @@ -143,7 +150,6 @@ contract TermsRepository is Multicall { function _verifyCategoryExists(ShortString category_) private view { if (!_termCategories.contains(ShortString.unwrap(category_))) { - console.log("TermsRegistry_UnsupportedTermCategory"); revert Errors.TermsRegistry_UnsupportedTermCategory(); } } diff --git a/contracts/modules/registration/RegistrationModule.sol b/contracts/modules/registration/RegistrationModule.sol index 09f9d7a7..e9709bde 100644 --- a/contracts/modules/registration/RegistrationModule.sol +++ b/contracts/modules/registration/RegistrationModule.sol @@ -15,6 +15,8 @@ import { LibUintArrayMask } from "contracts/lib/LibUintArrayMask.sol"; import { Errors } from "contracts/lib/Errors.sol"; import { IPAsset } from "contracts/lib/IPAsset.sol"; +import "forge-std/console2.sol"; + /// @title Registration Module /// @notice Handles registration and transferring of IP assets.. contract RegistrationModule is BaseModule, IRegistrationModule, AccessControlled { diff --git a/test/foundry/mocks/MockAsyncHook.sol b/test/foundry/mocks/MockAsyncHook.sol index c9f561e0..10e99f13 100644 --- a/test/foundry/mocks/MockAsyncHook.sol +++ b/test/foundry/mocks/MockAsyncHook.sol @@ -7,6 +7,7 @@ import { AsyncBaseHook } from "contracts/hooks/base/AsyncBaseHook.sol"; /// @notice This contract is a mock for testing the AsyncBaseHook contract. /// @dev It overrides the _requestAsyncCall and handleCallback functions for testing purposes. contract MockAsyncHook is AsyncBaseHook { + address immutable CALLBACK_CALLER; /// @notice Constructs the MockAsyncHook contract. /// @param accessControl_ The address of the access control contract. @@ -15,7 +16,9 @@ contract MockAsyncHook is AsyncBaseHook { constructor( address accessControl_, address callbackCaller_ - ) AsyncBaseHook(accessControl_, callbackCaller_) {} + ) AsyncBaseHook(accessControl_) { + CALLBACK_CALLER = callbackCaller_; + } /// @notice Requests an asynchronous call. /// @dev This function is overridden for testing purposes. @@ -34,7 +37,10 @@ contract MockAsyncHook is AsyncBaseHook { returns (bytes memory hookData, bytes32 requestId) { // Simply return the input parameters - return (abi.encode(hookConfig_, hookParams_), bytes32(uint256(keccak256(hookParams_)))); + return ( + abi.encode(hookConfig_, hookParams_), + bytes32(uint256(keccak256(hookParams_))) + ); } /// @notice Handles a callback. @@ -51,4 +57,10 @@ contract MockAsyncHook is AsyncBaseHook { } function _validateConfig(bytes memory) internal view override {} + + function _callbackCaller( + bytes32 + ) internal view virtual override returns (address) { + return CALLBACK_CALLER; + } } diff --git a/test/foundry/modules/licensing/BaseLicensingTest.sol b/test/foundry/modules/licensing/BaseLicensingTest.sol index 844d7acf..5fb667c6 100644 --- a/test/foundry/modules/licensing/BaseLicensingTest.sol +++ b/test/foundry/modules/licensing/BaseLicensingTest.sol @@ -1,306 +1,230 @@ -// // SPDX-License-Identifier: BUSDL-1.1 -// pragma solidity ^0.8.13; - -// import "forge-std/Test.sol"; -// import { Licensing } from "contracts/lib/modules/Licensing.sol"; -// import { ShortStrings, ShortString } from "@openzeppelin/contracts/utils/ShortStrings.sol"; -// import 'test/foundry/utils/BaseTest.sol'; -// import { IHook } from "contracts/interfaces/hooks/base/IHook.sol"; -// import { TermCategories, TermIds } from "contracts/lib/modules/ProtocolLicensingTerms.sol"; -// import { ProtocolTermsHelper } from "contracts/modules/licensing/ProtocolTermsHelper.sol"; - -// contract BaseLicensingTest is BaseTest { -// using ShortStrings for *; - -// ShortString public textTermId = "text_term_id".toShortString(); -// ShortString public nonCommTextTermId = "non_comm_text_term_id".toShortString(); -// ShortString public commTextTermId = "comm_text_term_id".toShortString(); - -// uint256 public rootIpaId; -// address public ipaOwner = address(0x13333); - -// uint256 public commRootLicenseId; -// uint256 public nonCommRootLicenseId; - -// ShortString[] public nonCommTermIds; -// bytes[] public nonCommTermData; -// ShortString[] public commTermIds; -// bytes[] public commTermData; - -// modifier withNonCommFrameworkShareAlike() { -// vm.prank(ipOrg.owner()); -// spg.configureIpOrgLicensing( -// address(ipOrg), -// getNonCommFramework(true) -// ); -// _; -// } - -// modifier withNonCommFrameworkNoShareAlike() { -// vm.prank(ipOrg.owner()); -// spg.configureIpOrgLicensing( -// address(ipOrg), -// getNonCommFramework(false) -// ); -// _; -// } - -// modifier withNonCommFrameworkShareAlikeAnd( -// ShortString termId, -// bytes memory data -// ) { -// vm.prank(ipOrg.owner()); -// spg.configureIpOrgLicensing( -// address(ipOrg), -// getNonCommFrameworkAndPush(true, termId, data) -// ); -// _; -// } - -// modifier withNonCommFrameworkNoShareAlikeAnd( -// ShortString termId, -// bytes memory data -// ) { -// vm.prank(ipOrg.owner()); -// spg.configureIpOrgLicensing( -// address(ipOrg), -// getNonCommFrameworkAndPush(false, termId, data) -// ); -// _; -// } - -// modifier withCommFrameworkShareAlike() { -// vm.prank(ipOrg.owner()); -// spg.configureIpOrgLicensing( -// address(ipOrg), -// getCommFramework(true, true) -// ); -// _; -// } - -// modifier withCommFrameworkNoShareAlike() { -// vm.prank(ipOrg.owner()); -// spg.configureIpOrgLicensing( -// address(ipOrg), -// getCommFramework(false, false) -// ); -// _; -// } - -// modifier withCommFrameworkShareAlikeAnd( -// ShortString ncTermId, -// bytes memory ncData, -// ShortString cTermId, -// bytes memory cData -// ) { -// vm.prank(ipOrg.owner()); -// spg.configureIpOrgLicensing( -// address(ipOrg), -// getCommFrameworkAndPush(true, ncTermId, ncData, true, cTermId, cData) -// ); -// _; -// } - -// modifier withRootLicense(bool commercial) { -// vm.prank(ipOrg.owner()); -// uint256 lId = spg.createIpaBoundLicense( -// address(ipOrg), -// Licensing.LicenseCreation({ -// parentLicenseId: 0, -// isCommercial: commercial -// }), -// rootIpaId, -// new bytes[](0), -// new bytes[](0) -// ); -// if (commercial) { -// commRootLicenseId = lId; -// } else { -// nonCommRootLicenseId = lId; -// } -// _; -// } - -// function setUp() virtual override public { -// super.setUp(); -// _addShareAlike(Licensing.CommercialStatus.Both); -// _addTextTerms(); -// nonCommTermIds = [textTermId, nonCommTextTermId]; -// nonCommTermData = [bytes(""), bytes("")]; -// commTermIds = [commTextTermId]; -// commTermData = [bytes("")]; -// (uint256 rootIpaId, uint256 ignored) = spg.registerIPAsset( -// address(ipOrg), -// Registration.RegisterIPAssetParams({ -// owner: ipaOwner, -// name: "bob", -// ipAssetType: 2, -// hash: keccak256("test") -// }), -// new bytes[](0), -// new bytes[](0) -// ); -// } - -// function getEmptyFramework() public pure returns (Licensing.FrameworkConfig memory) { -// return -// Licensing.FrameworkConfig({ -// comTermsConfig: Licensing.TermsConfig({ -// termIds: new ShortString[](0), -// termData: new bytes[](0) -// }), -// nonComTermsConfig: Licensing.TermsConfig({ -// termIds: new ShortString[](0), -// termData: new bytes[](0) -// }) -// }); -// } - -// function getCommFramework(bool comShareAlike, bool nonComShareAlike) public returns (Licensing.FrameworkConfig memory) { -// commTermIds.push(TermIds.NFT_SHARE_ALIKE.toShortString()); -// commTermData.push(abi.encode(comShareAlike)); -// nonCommTermIds.push(TermIds.NFT_SHARE_ALIKE.toShortString()); -// nonCommTermData.push(abi.encode(nonComShareAlike)); -// return -// Licensing.FrameworkConfig({ -// comTermsConfig: Licensing.TermsConfig({ -// termIds: commTermIds, -// termData: commTermData -// }), -// nonComTermsConfig: Licensing.TermsConfig({ -// termIds: nonCommTermIds, -// termData: nonCommTermData -// }) -// }); -// } - -// function getNonCommFramework(bool shareAlike) public returns (Licensing.FrameworkConfig memory) { -// nonCommTermIds.push(TermIds.NFT_SHARE_ALIKE.toShortString()); -// nonCommTermData.push(abi.encode(shareAlike)); -// return -// Licensing.FrameworkConfig({ -// comTermsConfig: Licensing.TermsConfig({ -// termIds: new ShortString[](0), -// termData: new bytes[](0) -// }), -// nonComTermsConfig: Licensing.TermsConfig({ -// termIds: nonCommTermIds, -// termData: nonCommTermData -// }) -// }); -// } - -// function getNonCommFrameworkAndPush( -// bool shareAlike, -// ShortString termId, -// bytes memory data -// ) public returns (Licensing.FrameworkConfig memory) { -// nonCommTermIds.push(termId); -// nonCommTermData.push(data); -// return getNonCommFramework(shareAlike); -// } - -// function getCommFrameworkAndPush( -// bool cShareAlike, -// ShortString ncTermId, -// bytes memory ncData, -// bool ncShareAlike, -// ShortString cTermId, -// bytes memory cData -// ) public returns (Licensing.FrameworkConfig memory) { -// nonCommTermIds.push(ncTermId); -// nonCommTermData.push(ncData); - -// commTermIds.push(cTermId); -// commTermData.push(cData); - -// return getCommFramework(cShareAlike, ncShareAlike); -// } - - -// function assertTerms(Licensing.License memory license) public { -// (ShortString[] memory ipOrgTermsId, bytes[] memory ipOrgTermsData) = licensingModule.getIpOrgTerms( -// license.isCommercial, address(ipOrg) -// ); -// assertEq(license.termIds.length, ipOrgTermsId.length); -// assertEq(license.termsData.length, ipOrgTermsData.length); -// for (uint256 i = 0; i < license.termIds.length; i++) { -// assertTrue(ShortStringOps._equal(license.termIds[i], ipOrgTermsId[i])); -// assertTrue(keccak256(license.termsData[i]) == keccak256(ipOrgTermsData[i])); -// Licensing.LicensingTerm memory term = licensingModule.getTerm(ipOrgTermsId[i]); -// if (license.isCommercial) { -// assertTrue( -// term.comStatus == Licensing.CommercialStatus.Commercial || -// term.comStatus == Licensing.CommercialStatus.Both -// ); -// } else { -// assertTrue( -// term.comStatus == Licensing.CommercialStatus.NonCommercial || -// term.comStatus == Licensing.CommercialStatus.Both -// ); -// } -// } -// } - -// function assertTermsSetInIpOrg(bool commercial) public { -// (ShortString[] memory ipOrgTermsId, bytes[] memory ipOrgTermsData) = licensingModule.getIpOrgTerms( -// commercial, address(ipOrg) -// ); -// ShortString[] memory termIds = commercial ? commTermIds : nonCommTermIds; -// bytes[] memory termData = commercial ? commTermData : nonCommTermData; -// assertEq(termIds.length, ipOrgTermsId.length); -// assertEq(termData.length, ipOrgTermsData.length); -// for (uint256 i = 0; i < termIds.length; i++) { -// assertTrue(ShortStringOps._equal(termIds[i], ipOrgTermsId[i])); -// assertTrue(keccak256(termData[i]) == keccak256(ipOrgTermsData[i])); -// } -// } - -// function _addShareAlike(Licensing.CommercialStatus comStatus) private { -// licensingModule.addCategory(TermCategories.SHARE_ALIKE); -// Licensing.LicensingTerm memory term = ProtocolTermsHelper._getNftShareAlikeTerm(comStatus); -// licensingModule.addTerm( -// TermCategories.SHARE_ALIKE, -// TermIds.NFT_SHARE_ALIKE, -// term -// ); -// } - -// function _addTextTerms() private { -// licensingModule.addCategory("test_category"); -// licensingModule.addTerm( -// "test_category", -// "text_term_id", -// Licensing.LicensingTerm({ -// comStatus: Licensing.CommercialStatus.Both, -// url: "https://text_term_id.com", -// hash: "qwertyu", -// algorithm: "sha256", -// hook: IHook(address(0)) -// } -// )); -// licensingModule.addTerm( -// "test_category", -// "non_comm_text_term_id", -// Licensing.LicensingTerm({ -// comStatus: Licensing.CommercialStatus.NonCommercial, -// url: "https://non_comm_text_term_id.com", -// hash: "qwertyu", -// algorithm: "sha256", -// hook: IHook(address(0)) -// } -// )); -// licensingModule.addTerm( -// "test_category", -// "comm_text_term_id", -// Licensing.LicensingTerm({ -// comStatus: Licensing.CommercialStatus.Commercial, -// url: "https://comm_text_term_id.com", -// hash: "qwertyu", -// algorithm: "sha256", -// hook: IHook(address(0)) -// } -// )); -// } - -// } +// SPDX-License-Identifier: BUSDL-1.1 +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; +import { Licensing } from "contracts/lib/modules/Licensing.sol"; +import { ShortStrings, ShortString } from "@openzeppelin/contracts/utils/ShortStrings.sol"; +import 'test/foundry/utils/BaseTest.sol'; +import { IHook } from "contracts/interfaces/hooks/base/IHook.sol"; +import { TermCategories, TermIds } from "contracts/lib/modules/ProtocolLicensingTerms.sol"; + +struct LicTestConfig { + bool shareAlike; + TermsData.LicensorConfig licConfig; + bool needsActivation; +} + +struct AddTermConfig { + ShortString termId; + bytes data; +} + +contract BaseLicensingTest is BaseTest { + using ShortStrings for *; + + ShortString public textTermId = "text_term_id".toShortString(); + ShortString public nonCommTextTermId = "non_comm_text_term_id".toShortString(); + ShortString public commTextTermId = "comm_text_term_id".toShortString(); + + address public ipaOwner = address(0x13333); + + mapping(bool => ShortString[]) public termIds; + mapping(bool => bytes[]) public termData; + + modifier withNonCommFramework(LicTestConfig memory config) { + _addTerms(false, config); + vm.prank(ipOrg.owner()); + spg.configureIpOrgLicensing( + address(ipOrg), + _getFramework(false) + ); + _; + } + + modifier withCommFramework(LicTestConfig memory config) { + _addTerms(false, config); + _addTerms(true, config); + vm.prank(ipOrg.owner()); + spg.configureIpOrgLicensing( + address(ipOrg), + _getFramework(true) + ); + _; + } + + + function setUp() virtual override public { + super.setUp(); + _addProtocolTerms(); + _addTextTerms(); + termIds[true].push(textTermId); + termIds[true].push(commTextTermId); + termData[true].push(bytes("")); + termData[true].push(bytes("")); + + termIds[false].push(textTermId); + termIds[false].push(nonCommTextTermId); + termData[false].push(bytes("")); + termData[false].push(bytes("")); + } + + function _addTerms(bool commercial, LicTestConfig memory config) internal { + termIds[commercial].push(TermIds.NFT_SHARE_ALIKE.toShortString()); + termData[commercial].push(abi.encode(config.shareAlike)); + termIds[commercial].push(TermIds.LICENSOR_APPROVAL.toShortString()); + termData[commercial].push(abi.encode(config.needsActivation)); + termIds[commercial].push(TermIds.LICENSOR_IPORG_OR_PARENT.toShortString()); + termData[commercial].push(abi.encode(config.licConfig)); + } + + function _getFramework(bool commercial) internal view returns (Licensing.FrameworkConfig memory) { + if (commercial) { + return Licensing.FrameworkConfig({ + comTermsConfig: Licensing.TermsConfig({ + termIds: termIds[commercial], + termData: termData[commercial] + }), + nonComTermsConfig: Licensing.TermsConfig({ + termIds: termIds[!commercial], + termData: termData[!commercial] + }) + }); + } else { + return Licensing.FrameworkConfig({ + comTermsConfig: Licensing.TermsConfig({ + termIds: new ShortString[](0), + termData: new bytes[](0) + }), + nonComTermsConfig: Licensing.TermsConfig({ + termIds: termIds[commercial], + termData: termData[commercial] + }) + }); + } + } + + function _getEmptyFramework() internal pure returns (Licensing.FrameworkConfig memory) { + return Licensing.FrameworkConfig({ + comTermsConfig: Licensing.TermsConfig({ + termIds: new ShortString[](0), + termData: new bytes[](0) + }), + nonComTermsConfig: Licensing.TermsConfig({ + termIds: new ShortString[](0), + termData: new bytes[](0) + }) + }); + } + + function _addProtocolTerms() private { + Licensing.CommercialStatus comStatus = Licensing.CommercialStatus.Both; + vm.startPrank(termSetter); + termsRepository.addCategory(TermCategories.SHARE_ALIKE); + Licensing.LicensingTerm memory term = _getTerm(TermIds.NFT_SHARE_ALIKE, comStatus); + termsRepository.addTerm(TermCategories.SHARE_ALIKE, TermIds.NFT_SHARE_ALIKE, term); + + termsRepository.addCategory(TermCategories.LICENSOR); + term = _getTerm(TermIds.LICENSOR_APPROVAL, comStatus); + termsRepository.addTerm(TermCategories.LICENSOR, TermIds.LICENSOR_APPROVAL, term); + + termsRepository.addCategory(TermCategories.CATEGORIZATION); + term = _getTerm(TermIds.FORMAT_CATEGORY, comStatus); + termsRepository.addTerm(TermCategories.CATEGORIZATION, TermIds.FORMAT_CATEGORY, term); + + termsRepository.addCategory(TermCategories.ACTIVATION); + term = _getTerm(TermIds.LICENSOR_IPORG_OR_PARENT, comStatus); + termsRepository.addTerm(TermCategories.ACTIVATION, TermIds.LICENSOR_IPORG_OR_PARENT, term); + vm.stopPrank(); + } + + function _getTerm( + string memory termId, + Licensing.CommercialStatus comStatus_ + ) internal pure returns (Licensing.LicensingTerm memory) { + return Licensing.LicensingTerm({ + comStatus: comStatus_, + url: string(abi.encodePacked("https://", termId,".com")), + hash: "qwertyu", + algorithm: "sha256", + hook: IHook(address(0)) + }); + } + + function _addTextTerms() private { + vm.startPrank(termSetter); + termsRepository.addCategory("test_category"); + termsRepository.addTerm( + "test_category", + "text_term_id", + Licensing.LicensingTerm({ + comStatus: Licensing.CommercialStatus.Both, + url: "https://text_term_id.com", + hash: "qwertyu", + algorithm: "sha256", + hook: IHook(address(0)) + } + )); + termsRepository.addTerm( + "test_category", + "non_comm_text_term_id", + Licensing.LicensingTerm({ + comStatus: Licensing.CommercialStatus.NonCommercial, + url: "https://non_comm_text_term_id.com", + hash: "qwertyu", + algorithm: "sha256", + hook: IHook(address(0)) + } + )); + termsRepository.addTerm( + "test_category", + "comm_text_term_id", + Licensing.LicensingTerm({ + comStatus: Licensing.CommercialStatus.Commercial, + url: "https://comm_text_term_id.com", + hash: "qwertyu", + algorithm: "sha256", + hook: IHook(address(0)) + } + )); + vm.stopPrank(); + } + + function assertTerms(Licensing.License memory license) public { + (ShortString[] memory ipOrgTermsId, bytes[] memory ipOrgTermsData) = licensingModule.getIpOrgTerms( + license.isCommercial, address(ipOrg) + ); + assertEq(license.termIds.length, ipOrgTermsId.length); + assertEq(license.termsData.length, ipOrgTermsData.length); + for (uint256 i = 0; i < license.termIds.length; i++) { + assertTrue(ShortStringOps._equal(license.termIds[i], ipOrgTermsId[i])); + assertTrue(keccak256(license.termsData[i]) == keccak256(ipOrgTermsData[i])); + Licensing.LicensingTerm memory term = termsRepository.getTerm(ipOrgTermsId[i]); + if (license.isCommercial) { + assertTrue( + term.comStatus == Licensing.CommercialStatus.Commercial || + term.comStatus == Licensing.CommercialStatus.Both + ); + } else { + assertTrue( + term.comStatus == Licensing.CommercialStatus.NonCommercial || + term.comStatus == Licensing.CommercialStatus.Both + ); + } + } + } + + function assertTermsSetInIpOrg(bool commercial) public { + (ShortString[] memory ipOrgTermsId, bytes[] memory ipOrgTermsData) = licensingModule.getIpOrgTerms( + commercial, address(ipOrg) + ); + ShortString[] memory tIds = termIds[commercial]; + bytes[] memory tData = termData[commercial]; + assertEq(tIds.length, ipOrgTermsId.length); + assertEq(tData.length, ipOrgTermsData.length); + uint256 length = termIds[commercial].length; + for (uint256 i = 0; i < length; i++) { + assertTrue(ShortStringOps._equal(tIds[i], ipOrgTermsId[i])); + assertTrue(keccak256(tData[i]) == keccak256(ipOrgTermsData[i])); + } + } +} diff --git a/test/foundry/modules/licensing/LicenseCreatorModule.Terms.sol b/test/foundry/modules/licensing/LicenseCreatorModule.Terms.sol deleted file mode 100644 index aaaf3dbb..00000000 --- a/test/foundry/modules/licensing/LicenseCreatorModule.Terms.sol +++ /dev/null @@ -1,110 +0,0 @@ -// // SPDX-License-Identifier: BUSDL-1.1 -// pragma solidity ^0.8.13; - -// import "forge-std/Test.sol"; -// import "test/foundry/utils/BaseTest.sol"; -// import "contracts/modules/relationships/RelationshipModule.sol"; -// import "contracts/lib/modules/LibRelationship.sol"; -// import { AccessControl } from "contracts/lib/AccessControl.sol"; -// import { Licensing } from "contracts/lib/modules/Licensing.sol"; -// import { TermIds, TermCategories } from "contracts/lib/modules/ProtocolLicensingTerms.sol"; -// import { IHook } from "contracts/interfaces/hooks/base/IHook.sol"; -// import { IPAsset } from "contracts/lib/IPAsset.sol"; -// import { BaseLicensingTest } from "./BaseLicensingTest.sol"; -// import { ProtocolTermsHelper } from "contracts/modules/licensing/ProtocolTermsHelper.sol"; - -// contract LicensingCreatorModuleTermsTest is BaseLicensingTest { -// using ShortStrings for *; - -// address licensee = address(0x22222); -// address ipaOwner2 = address(0x33333); - -// function setUp() public override { -// super.setUp(); -// } - -// function test_LicensingModule_terms_shareAlikeOn() -// public -// withNonCommFrameworkShareAlike -// withRootLicense(false) -// { -// // TODO: This should be just creating an derivative IPA -// (uint256 ipaId2, uint256 ignored) = spg.registerIPAsset( -// address(ipOrg), -// Registration.RegisterIPAssetParams({ -// owner: ipaOwner, -// name: "bob", -// ipAssetType: 2, -// hash: keccak256("test") -// }), -// new bytes[](0), -// new bytes[](0) -// ); -// vm.prank(ipaOwner2); -// uint256 lId = spg.createIpaBoundLicense( -// address(ipOrg), -// Licensing.LicenseCreation({ -// parentLicenseId: nonCommRootLicenseId, -// isCommercial: false -// }), -// ipaId2, -// new bytes[](0), -// new bytes[](0) -// ); -// Licensing.License memory license = licenseRegistry.getLicense(lId); -// assertTerms(license); -// vm.expectRevert(); -// licenseRegistry.ownerOf(lId); -// assertEq(license.ipaId, ipaId2); -// assertEq(license.parentLicenseId, nonCommRootLicenseId); -// } - -// function test_LicensingModule_terms_revert_shareAlikeOff() -// public -// withNonCommFrameworkNoShareAlike -// withRootLicense(false) { -// // TODO: this should be create derivative IPA -// // expect revert if share alike is off -// vm.startPrank(ipaOwner2); -// vm.expectRevert(Errors.LicensingModule_ShareAlikeDisabled.selector); -// spg.createIpaBoundLicense( -// address(ipOrg), -// Licensing.LicenseCreation({ -// parentLicenseId: nonCommRootLicenseId, -// isCommercial: false -// }), -// 1, -// new bytes[](0), -// new bytes[](0) -// ); -// vm.stopPrank(); -// // have licensor create a license -// console.log("nonCommRootLicenseId", nonCommRootLicenseId); - -// vm.prank(ipaOwner); -// uint256 lId = spg.createLicenseNft( -// address(ipOrg), -// Licensing.LicenseCreation({ -// parentLicenseId: nonCommRootLicenseId, -// isCommercial: false -// }), -// ipaOwner, -// new bytes[](0), -// new bytes[](0) -// ); -// Licensing.License memory license = licenseRegistry.getLicense(lId); -// console.log("lId", lId); -// console.log("licenseeType", uint8(license.licenseeType)); -// // Non Commercial -// assertEq(licenseRegistry.ownerOf(lId), ipaOwner); -// assertEq(licenseRegistry.getLicensee(lId), ipaOwner); -// // transfer license to other guy -// vm.prank(ipaOwner); -// licenseRegistry.transferFrom(ipaOwner, ipaOwner2, lId); -// // have other guy activate license - -// // have other guy mint ipa, burn LNFT and tie it to IPA - -// } - -// } diff --git a/test/foundry/modules/licensing/LicensingCreatorModule.Config.t.sol b/test/foundry/modules/licensing/LicensingCreatorModule.Config.t.sol deleted file mode 100644 index 88b7b027..00000000 --- a/test/foundry/modules/licensing/LicensingCreatorModule.Config.t.sol +++ /dev/null @@ -1,108 +0,0 @@ -// // SPDX-License-Identifier: BUSDL-1.1 -// pragma solidity ^0.8.13; - -// import "forge-std/Test.sol"; -// import "contracts/modules/relationships/RelationshipModule.sol"; -// import "contracts/lib/modules/LibRelationship.sol"; -// import { AccessControl } from "contracts/lib/AccessControl.sol"; -// import { Licensing } from "contracts/lib/modules/Licensing.sol"; -// import { TermCategories, TermIds } from "contracts/lib/modules/ProtocolLicensingTerms.sol"; -// import { IHook } from "contracts/interfaces/hooks/base/IHook.sol"; -// import { BaseLicensingTest } from "./BaseLicensingTest.sol"; -// import { ShortStringOps } from "contracts/utils/ShortStringOps.sol"; -// import { ShortString } from "@openzeppelin/contracts/utils/ShortStrings.sol"; - -// contract LicensingCreatorModuleConfigTest is BaseLicensingTest { -// function setUp() public override { -// super.setUp(); -// } - -// function test_LicensingModule_configIpOrg_revertIfNotIpOrgOwner() public { -// vm.expectRevert(Errors.LicensingModule_CallerNotIpOrgOwner.selector); -// spg.configureIpOrgLicensing( -// address(ipOrg), -// getNonCommFramework(true) -// ); -// } - -// function test_LicensingModule_configIpOrg_ipOrgWithoutCommercialTermsIsNonCommercial() -// public -// withNonCommFrameworkShareAlike -// { -// assertFalse(licensingModule.ipOrgAllowsCommercial(address(ipOrg)), "should be non commercial"); -// ( -// ShortString[] memory comTermIds, -// bytes[] memory comTermData -// ) = licensingModule.getIpOrgTerms(true, address(ipOrg)); -// assertTrue(comTermIds.length == 0, "commercial terms should be empty"); -// assertTermsSetInIpOrg(false); // non commercial terms -// } - -// function test_LicensingModule_configIpOrg_ipOrgWithCommercialTermsIsCommercial() -// public -// withCommFrameworkShareAlike -// { -// assertTrue(licensingModule.ipOrgAllowsCommercial(address(ipOrg)), "not commercial"); -// assertTermsSetInIpOrg(true); // commercial terms -// assertTermsSetInIpOrg(false); // non commercial terms too -// } - -// function test_LicensingModule_configIpOrg_revert_noEmptyNonCommercialTerms() -// public -// { -// vm.startPrank(ipOrg.owner()); -// vm.expectRevert( -// Errors.LicensingModule_NonCommercialTermsRequired.selector -// ); -// spg.configureIpOrgLicensing(address(ipOrg), getEmptyFramework()); -// vm.stopPrank(); -// } - -// function test_LicensingModule_configIpOrg_revert_IfWrongTermCommercialStatus() -// public -// { -// vm.startPrank(ipOrg.owner()); -// vm.expectRevert( -// Errors.LicensingModule_InvalidTermCommercialStatus.selector -// ); -// spg.configureIpOrgLicensing( -// address(ipOrg), -// getNonCommFrameworkAndPush( -// false, -// commTextTermId, -// bytes("") -// ) -// ); -// vm.stopPrank(); -// } - -// function test_LicensingModule_configIpOrg_revertIfIpOrgAlreadyConfigured() -// public -// { -// // Todo -// } - -// function test_LicensingModule_configIpOrg_setsHooksForCreatingCommercialLicenses() -// public -// { -// // Todo -// } - -// function test_LicensingModule_configIpOrg_setsHooksForCreatingNonCommercialLicenses() -// public -// { -// // Todo -// } - -// function test_LicensingModule_configIpOrg_commercialLicenseActivationHooksCanBeSet() -// public -// { -// // TODO -// } - -// function test_LicensingModule_configIpOrg_nonCommercialLicenseActivationHooksCanBeSet() -// public -// { -// // TODO -// } -// } diff --git a/test/foundry/modules/licensing/LicensingCreatorModule.Licensing.sol b/test/foundry/modules/licensing/LicensingCreatorModule.Licensing.sol deleted file mode 100644 index a75ba5b5..00000000 --- a/test/foundry/modules/licensing/LicensingCreatorModule.Licensing.sol +++ /dev/null @@ -1,91 +0,0 @@ -// // SPDX-License-Identifier: BUSDL-1.1 -// pragma solidity ^0.8.13; - -// import "forge-std/Test.sol"; -// import "test/foundry/utils/BaseTest.sol"; -// import "contracts/modules/relationships/RelationshipModule.sol"; -// import "contracts/lib/modules/LibRelationship.sol"; -// import { AccessControl } from "contracts/lib/AccessControl.sol"; -// import { Licensing } from "contracts/lib/modules/Licensing.sol"; -// import { TermCategories, TermIds } from "contracts/lib/modules/ProtocolLicensingTerms.sol"; -// import { IHook } from "contracts/interfaces/hooks/base/IHook.sol"; -// import { IPAsset } from "contracts/lib/IPAsset.sol"; -// import { BaseLicensingTest } from "./BaseLicensingTest.sol"; - -// contract LicensingCreatorLicensingTest is BaseLicensingTest { -// using ShortStrings for *; - -// address lnftOwner = address(0x13334); - -// function setUp() public override { -// super.setUp(); - -// } - -// function test_LicensingModule_configIpOrg_commercialLicenseActivationHooksCanBeSet() -// public -// { -// // TODO -// } - -// function test_LicensingModule_configIpOrg_nonCommercialLicenseActivationHooksCanBeSet() -// public -// { -// // TODO -// } - -// function test_LicensingModule_licensing_createNonCommercialRootLicense() -// public -// withNonCommFrameworkShareAlike -// { -// // TODO: this should be create root IPA -// vm.prank(ipOrg.owner()); -// uint256 lId = spg.createIpaBoundLicense( -// address(ipOrg), -// Licensing.LicenseCreation({ -// parentLicenseId: 0, -// isCommercial: false -// }), -// rootIpaId, -// new bytes[](0), -// new bytes[](0) -// ); -// // Non Commercial -// Licensing.License memory license = licenseRegistry.getLicense(lId); -// assertFalse(license.isCommercial); -// assertEq(license.revoker, ipOrg.owner()); -// assertEq(license.licensor, ipaOwner, "licensor"); - -// assertTerms(license); -// assertEq(license.ipaId, rootIpaId); -// } - -// function test_LicensingModule_licensing_createsCommercialSubLicense_noDestIpa() -// public -// withCommFrameworkShareAlike -// withRootLicense(false) -// withRootLicense(true) -// { -// vm.prank(lnftOwner); -// uint256 lId = spg.createLicenseNft( -// address(ipOrg), -// Licensing.LicenseCreation({ -// parentLicenseId: commRootLicenseId, -// isCommercial: true -// }), -// lnftOwner, -// new bytes[](0), -// new bytes[](0) -// ); -// // Non Commercial -// Licensing.License memory license = licenseRegistry.getLicense(lId); -// assertTrue(license.isCommercial); -// assertEq(licenseRegistry.ownerOf(lId), lnftOwner); -// assertEq(licenseRegistry.getLicensee(lId), lnftOwner); -// assertEq(license.revoker, ipOrg.owner()); -// assertEq(license.licensor, ipaOwner, "licensor"); -// assertEq(license.ipaId, 0); -// assertEq(license.parentLicenseId, commRootLicenseId); -// } - -// } diff --git a/test/foundry/modules/licensing/LicensingModule.Config.t.sol b/test/foundry/modules/licensing/LicensingModule.Config.t.sol new file mode 100644 index 00000000..2e70d31f --- /dev/null +++ b/test/foundry/modules/licensing/LicensingModule.Config.t.sol @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: BUSDL-1.1 +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; +import "contracts/modules/relationships/RelationshipModule.sol"; +import "contracts/lib/modules/LibRelationship.sol"; +import { AccessControl } from "contracts/lib/AccessControl.sol"; +import { Licensing } from "contracts/lib/modules/Licensing.sol"; +import { TermCategories, TermIds, TermsData } from "contracts/lib/modules/ProtocolLicensingTerms.sol"; +import { IHook } from "contracts/interfaces/hooks/base/IHook.sol"; +import { BaseLicensingTest, LicTestConfig } from "./BaseLicensingTest.sol"; +import { ShortStringOps } from "contracts/utils/ShortStringOps.sol"; +import { ShortString } from "@openzeppelin/contracts/utils/ShortStrings.sol"; + +contract LicensingModuleConfigTest is BaseLicensingTest { + function setUp() public override { + super.setUp(); + } + + function test_LicensingModule_configIpOrg_revertIfNotIpOrgOwner() public { + vm.expectRevert(Errors.LicensingModule_CallerNotIpOrgOwner.selector); + spg.configureIpOrgLicensing( + address(ipOrg), + _getFramework(false) + ); + } + + function test_LicensingModule_configIpOrg_ipOrgWithoutCommercialTermsIsNonCommercial() + public + withNonCommFramework(LicTestConfig({ + shareAlike: true, + licConfig: TermsData.LicensorConfig.IpOrg, + needsActivation: false + })) + { + assertFalse(licensingModule.ipOrgAllowsCommercial(address(ipOrg)), "should be non commercial"); + ( + ShortString[] memory comTermIds, + bytes[] memory comTermData + ) = licensingModule.getIpOrgTerms(true, address(ipOrg)); + assertTrue(comTermIds.length == 0, "commercial terms should be empty"); + assertTermsSetInIpOrg(false);// non commercial terms + } + + function test_LicensingModule_configIpOrg_ipOrgWithCommercialTermsIsCommercial() + public + withCommFramework(LicTestConfig({ + shareAlike: true, + licConfig: TermsData.LicensorConfig.IpOrg, + needsActivation: false + })) + { + assertTrue(licensingModule.ipOrgAllowsCommercial(address(ipOrg)), "not commercial"); + assertTermsSetInIpOrg(true);// commercial terms + assertTermsSetInIpOrg(false);// non commercial terms too + } + + function test_LicensingModule_configIpOrg_revert_noEmptyNonCommercialTerms() + public + { + vm.startPrank(ipOrg.owner()); + vm.expectRevert( + Errors.LicensingModule_NonCommercialTermsRequired.selector + ); + spg.configureIpOrgLicensing(address(ipOrg), _getEmptyFramework()); + vm.stopPrank(); + } + + function test_LicensingModule_configIpOrg_revert_IfWrongTermCommercialStatus() + public + { + vm.startPrank(ipOrg.owner()); + vm.expectRevert( + Errors.LicensingModule_InvalidTermCommercialStatus.selector + ); + termIds[false].push(commTextTermId); + termData[false].push(bytes("")); + spg.configureIpOrgLicensing( + address(ipOrg), + _getFramework(false) + ); + vm.stopPrank(); + } + + function test_LicensingModule_configIpOrg_revert_ipOrgAlreadySet() + public + withNonCommFramework(LicTestConfig({ + shareAlike: true, + licConfig: TermsData.LicensorConfig.IpOrg, + needsActivation: false + })) { + vm.startPrank(ipOrg.owner()); + vm.expectRevert( + Errors.LicensingModule_IpOrgFrameworkAlreadySet.selector + ); + spg.configureIpOrgLicensing( + address(ipOrg), + _getFramework(false) + ); + vm.stopPrank(); + } + + function test_LicensingModule_configIpOrg_protocolTermsMustBeSet() + public + withNonCommFramework(LicTestConfig({ + shareAlike: true, + licConfig: TermsData.LicensorConfig.ParentLicensee, + needsActivation: true + })) { + assertEq(licensingModule.isShareAlikeOn(false, address(ipOrg)), true); + assertEq( + uint8(licensingModule.getLicensorConfig(false, address(ipOrg))), + uint8(TermsData.LicensorConfig.ParentLicensee) + ); + assertEq(licensingModule.isLicensorAppovalOn(false, address(ipOrg)), true); + } + +} diff --git a/test/foundry/modules/licensing/LicensingModule.Licensing.sol b/test/foundry/modules/licensing/LicensingModule.Licensing.sol new file mode 100644 index 00000000..d6f50f21 --- /dev/null +++ b/test/foundry/modules/licensing/LicensingModule.Licensing.sol @@ -0,0 +1,216 @@ +// SPDX-License-Identifier: BUSDL-1.1 +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; +import "test/foundry/utils/BaseTest.sol"; +import "contracts/modules/relationships/RelationshipModule.sol"; +import "contracts/lib/modules/LibRelationship.sol"; +import { AccessControl } from "contracts/lib/AccessControl.sol"; +import { Licensing } from "contracts/lib/modules/Licensing.sol"; +import { TermCategories, TermIds } from "contracts/lib/modules/ProtocolLicensingTerms.sol"; +import { IHook } from "contracts/interfaces/hooks/base/IHook.sol"; +import { IPAsset } from "contracts/lib/IPAsset.sol"; +import { BaseLicensingTest, LicTestConfig } from "./BaseLicensingTest.sol"; + +contract LicensingModuleLicensingTest is BaseLicensingTest { + using ShortStrings for *; + + address lnftOwner = address(0x13334); + address ipaOwner2 = address(0x13336); + + uint256 rootIpaId; + + function setUp() public override { + super.setUp(); + rootIpaId = _createIpAsset(ipaOwner, 2, bytes("")); + } + + function test_LicensingModule_createNonCommercialIpaBoundLicense_licensorIpOrg() + public + withNonCommFramework(LicTestConfig({ + shareAlike: true, + licConfig: TermsData.LicensorConfig.IpOrg, + needsActivation: false + })) + { + vm.prank(ipOrg.owner()); + uint256 lId = spg.createIpaBoundLicense( + address(ipOrg), + Licensing.LicenseCreation({ + parentLicenseId: 0, + isCommercial: false + }), + rootIpaId, + new bytes[](0), + new bytes[](0) + ); + Licensing.License memory license = licenseRegistry.getLicense(lId); + assertFalse(license.isCommercial, "commercial"); + assertEq(license.revoker, ipOrg.owner(), "revoker is iporg"); + assertEq(license.licensor, ipOrg.owner(), "licensor is owner is iporg"); + assertEq(licenseRegistry.isLicenseActive(lId), true, "license is active"); + assertEq(licenseRegistry.getLicensee(lId), ipaOwner, "licensee is ipa owner"); + assertTerms(license); + assertEq(license.ipaId, rootIpaId); + } + + function test_LicensingModule_createCommercialLicense_licensorPrevious() + public + withCommFramework(LicTestConfig({ + shareAlike: true, + licConfig: TermsData.LicensorConfig.ParentLicensee, + needsActivation: false + })) + { + vm.prank(ipOrg.owner()); + uint256 lId = spg.createIpaBoundLicense( + address(ipOrg), + Licensing.LicenseCreation({ + parentLicenseId: 0, + isCommercial: true + }), + rootIpaId, + new bytes[](0), + new bytes[](0) + ); + Licensing.License memory license = licenseRegistry.getLicense(lId); + assertTrue(license.isCommercial, "is commercial"); + assertEq(license.licensor, ipaOwner, "licensor is ipaOwner"); + + uint256 lId2 = spg.createLicenseNft( + address(ipOrg), + Licensing.LicenseCreation({ + parentLicenseId: lId, + isCommercial: true + }), + lnftOwner, + new bytes[](0), + new bytes[](0) + ); + Licensing.License memory license2 = licenseRegistry.getLicense(lId2); + assertEq(license2.licensor, licenseRegistry.getLicensee(lId), "licensor is parent licensee"); + assertEq(license2.parentLicenseId, lId, "parent is first license"); + assertEq(license2.ipaId, 0, "no ipa id"); + assertEq(licenseRegistry.getLicensee(lId2), lnftOwner, "licensee is lnft owner"); + + } + + + function test_LicensingModule_terms_revert_shareAlikeOff_LicensorApproval_ActivateAndBound() + public + withNonCommFramework(LicTestConfig({ + shareAlike: false, + licConfig: TermsData.LicensorConfig.ParentLicensee, + needsActivation: true + })) + { + + // First derivative should work + console2.log("First derivative should work"); + vm.startPrank(ipaOwner); + uint256 lid1 = spg.createIpaBoundLicense( + address(ipOrg), + Licensing.LicenseCreation({ + parentLicenseId: 0, + isCommercial: false + }), + 1, + new bytes[](0), + new bytes[](0) + ); + assertEq(lid1, 1); + assertEq(licenseRegistry.isLicenseActive(lid1), false); + spg.activateLicense(address(ipOrg), lid1); + assertEq(licenseRegistry.isLicenseActive(lid1), true); + vm.stopPrank(); + + // Second derivative should fail + console2.log("Second derivative should fail"); + vm.startPrank(ipaOwner2); + vm.expectRevert(Errors.LicensingModule_ShareAlikeDisabled.selector); + uint256 lid2 = spg.createIpaBoundLicense( + address(ipOrg), + Licensing.LicenseCreation({ + parentLicenseId: lid1, + isCommercial: false + }), + 1, + new bytes[](0), + new bytes[](0) + ); + vm.stopPrank(); + + // But original ipa owner can emit a license + console2.log("But original ipa owner can emit a license"); + vm.prank(ipaOwner); + lid2 = spg.createLicenseNft( + address(ipOrg), + Licensing.LicenseCreation({ + parentLicenseId: lid1, + isCommercial: false + }), + ipaOwner, + new bytes[](0), + new bytes[](0) + ); + // License is not active + console2.log("License is not active"); + Licensing.License memory license2 = licenseRegistry.getLicense(lid2); + assertEq(uint8(license2.status), uint8(Licensing.LicenseStatus.Pending)); + assertFalse(licenseRegistry.isLicenseActive(lid2)); + assertEq(licenseRegistry.ownerOf(lid2), ipaOwner); + assertEq(licenseRegistry.getLicensee(lid2), ipaOwner); + assertEq(uint8(license2.licenseeType), uint8(Licensing.LicenseeType.LNFTHolder)); + + // transfer license to other guy + console2.log("transfer license to other guy"); + vm.prank(ipaOwner); + licenseRegistry.transferFrom(ipaOwner, ipaOwner2, lid2); + assertEq(licenseRegistry.ownerOf(lid2), ipaOwner2); + assertEq(licenseRegistry.getLicensee(lid2), ipaOwner2); + // Fail to bound if not active + console2.log("Fail to bound if not active"); + vm.expectRevert(); + spg.bindLnftToIpa( + address(ipOrg), + lid2, + 1 + ); + // have other guy activate license + console2.log("licensee fails to activate license"); + vm.expectRevert(Errors.LicensingModule_CallerNotLicensor.selector); + vm.prank(ipaOwner2); + spg.activateLicense(address(ipOrg), lid2); + + // Licensor must activate + console2.log("Licensor must activate"); + vm.prank(ipaOwner); + spg.activateLicense(address(ipOrg), lid2); + + license2 = licenseRegistry.getLicense(lid2); + assertEq(uint8(license2.status), uint8(Licensing.LicenseStatus.Active)); + assertTrue(licenseRegistry.isLicenseActive(lid2)); + + // Bond if active + console2.log("Bond if active"); + vm.prank(ipaOwner2); + spg.bindLnftToIpa( + address(ipOrg), + lid2, + 1 + ); + license2 = licenseRegistry.getLicense(lid2); + assertEq(uint8(license2.licenseeType), uint8(Licensing.LicenseeType.BoundToIpa)); + vm.expectRevert("ERC721: invalid token ID"); + assertEq(licenseRegistry.ownerOf(lid2), address(0)); + // This looks weird because: + // IpOwner2 is owner of the lnft + // Bounds to ipa1, owned by ipaOwner + // Licenseee type is bound to ipa + // So ipaOwner is the licensee + // In a normal case, ipaOwner2 would bound to an ipa he owns + assertEq(licenseRegistry.getLicensee(lid2), ipaOwner); + } + + +} diff --git a/test/foundry/modules/registration/RegistrationTest.sol b/test/foundry/modules/registration/RegistrationTest.sol index af62e47c..e974e434 100644 --- a/test/foundry/modules/registration/RegistrationTest.sol +++ b/test/foundry/modules/registration/RegistrationTest.sol @@ -65,17 +65,17 @@ contract RegistrationModuleTest is BaseTest { "https://storyprotocol.xyz/", "https://storyprotocol.xyz" ); - assertEq(registrationModule.tokenURI(address(ipOrg), 0), "https://storyprotocol.xyz/0"); + assertEq(registrationModule.tokenURI(address(ipOrg), 1), "https://storyprotocol.xyz/1"); } /// @notice Tests the default token URI for IPAs. function test_RegistrationModuleDefaultIPOrgMetadata() public virtual createIpAsset(registrant, 0) { - IPAssetRegistry.IPA memory ipa = registry.ipAsset(0); + IPAssetRegistry.IPA memory ipa = registry.ipAsset(1); string memory ipOrgStr = Strings.toHexString(uint160(address(ipOrg)), 20); string memory registrantStr = Strings.toHexString(uint160(address(registrant)), 20); string memory part1 = string(abi.encodePacked( - '{"name": "Global IP Asset #0", "description": "IP Org Asset Registration Details", "attributes": [', + '{"name": "Global IP Asset #1", "description": "IP Org Asset Registration Details", "attributes": [', '{"trait_type": "Name", "value": "TestIPAsset"},', '{"trait_type": "IP Org", "value": "', ipOrgStr, '"},', '{"trait_type": "Current IP Owner", "value": "', registrantStr, '"},', @@ -84,7 +84,7 @@ contract RegistrationModuleTest is BaseTest { string memory part2 = string(abi.encodePacked( '{"trait_type": "IP Asset Type", "value": "0"},', - '{"trait_type": "Status", "value": "0"},', + '{"trait_type": "Status", "value": "1"},', '{"trait_type": "Hash", "value": "0x0000000000000000000000000000000000000000000000000000000000000000"},', '{"trait_type": "Registration Date", "value": "', Strings.toString(ipa.registrationDate), '"}' ']}' @@ -93,7 +93,7 @@ contract RegistrationModuleTest is BaseTest { "data:application/json;base64,", Base64.encode(bytes(string(abi.encodePacked(part1, part2)))) )); - assertEq(expectedURI, registrationModule.tokenURI(address(ipOrg), 0)); + assertEq(expectedURI, registrationModule.tokenURI(address(ipOrg), 1)); } @@ -102,7 +102,7 @@ contract RegistrationModuleTest is BaseTest { vm.prank(cal); vm.expectEmit(true, true, true, true, address(registry)); emit Registered( - 0, + 1, "TestIPA", 0, address(ipOrg), @@ -111,9 +111,9 @@ contract RegistrationModuleTest is BaseTest { ); vm.expectEmit(true, true, true, true, address(registrationModule)); emit IPAssetRegistered( - 0, + 1, address(ipOrg), - 0, + 1, cal, "TestIPA", 0, @@ -121,8 +121,8 @@ contract RegistrationModuleTest is BaseTest { "" ); _register(address(ipOrg), cal, "TestIPA", 0, "", ""); - assertEq(registry.ipAssetOwner(0), cal); - assertEq(ipOrg.ownerOf(0), cal); + assertEq(registry.ipAssetOwner(1), cal); + assertEq(ipOrg.ownerOf(1), cal); } /// @notice Tests IP Asset registration with media URL. @@ -131,7 +131,7 @@ contract RegistrationModuleTest is BaseTest { vm.prank(cal); vm.expectEmit(true, true, true, true, address(registry)); emit Registered( - 0, + 1, "TestIPA", 0, address(ipOrg), @@ -140,9 +140,9 @@ contract RegistrationModuleTest is BaseTest { ); vm.expectEmit(true, true, true, true, address(registrationModule)); emit IPAssetRegistered( - 0, + 1, address(ipOrg), - 0, + 1, cal, "TestIPA", 0, @@ -150,9 +150,9 @@ contract RegistrationModuleTest is BaseTest { mediaUrl ); _register(address(ipOrg), cal, "TestIPA", 0, "", mediaUrl); - assertEq(registry.ipAssetOwner(0), cal); - assertEq(ipOrg.ownerOf(0), cal); - assertEq(mediaUrl, registrationModule.tokenURI(address(ipOrg), 0)); + assertEq(registry.ipAssetOwner(1), cal); + assertEq(ipOrg.ownerOf(1), cal); + assertEq(mediaUrl, registrationModule.tokenURI(address(ipOrg), 1)); } /// @dev Helper function that performs registration. diff --git a/test/foundry/utils/BaseTest.sol b/test/foundry/utils/BaseTest.sol index e6c531e7..8e9e845e 100644 --- a/test/foundry/utils/BaseTest.sol +++ b/test/foundry/utils/BaseTest.sol @@ -17,12 +17,13 @@ import "contracts/IPAssetRegistry.sol"; import "contracts/interfaces/modules/collect/ICollectModule.sol"; import "contracts/modules/relationships/RelationshipModule.sol"; import "contracts/modules/licensing/LicenseRegistry.sol"; -import "contracts/modules/licensing/LicenseCreatorModule.sol"; +import "contracts/modules/licensing/LicensingModule.sol"; import { ShortString, ShortStrings } from "@openzeppelin/contracts/utils/ShortStrings.sol"; import { ShortStringOps } from "contracts/utils/ShortStringOps.sol"; import { AccessControl } from "contracts/lib/AccessControl.sol"; import { ModuleRegistryKeys } from "contracts/lib/modules/ModuleRegistryKeys.sol"; import { RegistrationModule } from "contracts/modules/registration/RegistrationModule.sol"; +import { TermsRepository } from "contracts/modules/licensing/TermsRepository.sol"; contract BaseTest is BaseTestUtils, ProxyHelper, AccessControlHelper { using ShortStrings for *; @@ -34,9 +35,10 @@ contract BaseTest is BaseTestUtils, ProxyHelper, AccessControlHelper { RelationshipModule public relationshipModule; IPAssetRegistry public registry; StoryProtocol public spg; - LicenseCreatorModule public licensingModule; + LicensingModule public licensingModule; LicenseRegistry public licenseRegistry; RegistrationModule public registrationModule; + TermsRepository public termsRepository; address public defaultCollectNftImpl; address public collectModuleImpl; @@ -44,6 +46,7 @@ contract BaseTest is BaseTestUtils, ProxyHelper, AccessControlHelper { address constant upgrader = address(6969); address constant ipAssetOrgOwner = address(456); address constant relManager = address(9999); + address constant termSetter = address(444); function setUp() virtual override(BaseTestUtils) public { super.setUp(); @@ -77,14 +80,18 @@ contract BaseTest is BaseTestUtils, ProxyHelper, AccessControlHelper { _grantRole(vm, AccessControl.MODULE_REGISTRAR_ROLE, address(this)); // Create Licensing contracts + termsRepository = new TermsRepository(address(accessControl)); + _grantRole(vm, AccessControl.TERMS_SETTER_ROLE, termSetter); + licenseRegistry = new LicenseRegistry(address(registry), address(moduleRegistry)); - licensingModule = new LicenseCreatorModule( + licensingModule = new LicensingModule( BaseModule.ModuleConstruction({ ipaRegistry: registry, moduleRegistry: moduleRegistry, licenseRegistry: licenseRegistry, ipOrgController: ipOrgController - }) + }), + address(termsRepository) ); moduleRegistry.registerProtocolModule(ModuleRegistryKeys.LICENSING_MODULE, licensingModule);