From b739da80e2ce683f433748372a2527b256120ff3 Mon Sep 17 00:00:00 2001 From: Jongwon Park Date: Thu, 7 Dec 2023 23:48:45 -0800 Subject: [PATCH] Improved Unit & Integration Test (#231) * Remove 'indexed' from string relType events * Module registry events test * Add make coverage script * Add more access control tests * Add more IPOrg (controller) tests * fix: IPOrgTransferred event emit, cache record.owner for prevOwner event arg * Add more test for IPOrg controller Coverage except internal functions and `initialize` for upgradable. Need to use harness for internal functions. * Add basic tests for FixedSet util * fix: FixedSet.UintSet indexOf argument signature * Add integration tests to full line coverage * Add coverage instruction * Updated tests for e2e and modules * Contract bug fix & comment nit * lcov update * Fix licensing module test cases * Separate tests & remove lcov file * Update integration tests * Small fixes for contracts & interfaces * Update e2e and module tests & comment out unready fn * Updated tests * More integration tests * Fix param comment * Add integration tests, fix unit tests, fix contract bugs * Remove commented out function * Update LicensingModule.Licensing.t.sol * Update LicensingModule.Config.t.sol --- contracts/interfaces/ip-org/IIPOrg.sol | 5 +- contracts/ip-org/IPOrg.sol | 6 +- contracts/ip-org/IPOrgController.sol | 9 - contracts/lib/Errors.sol | 1 + .../modules/licensing/LicenseRegistry.sol | 4 + .../modules/licensing/LicensingModule.sol | 54 +- .../registration/RegistrationModule.sol | 48 +- test/foundry/e2e/e2e.t.sol | 1566 ++++++++++++++--- test/foundry/interfaces/IE2ETest.sol | 63 +- test/foundry/mocks/MockIPOrg.sol | 4 + test/foundry/modules/ModuleRegistry.t.sol | 4 +- .../licensing/LicensingModule.Config.t.sol | 7 +- .../licensing/LicensingModule.Licensing.t.sol | 15 + .../RelationshipModule.Setting.t.sol | 38 +- 14 files changed, 1468 insertions(+), 356 deletions(-) diff --git a/contracts/interfaces/ip-org/IIPOrg.sol b/contracts/interfaces/ip-org/IIPOrg.sol index 1bcfbdaa..129b8435 100644 --- a/contracts/interfaces/ip-org/IIPOrg.sol +++ b/contracts/interfaces/ip-org/IIPOrg.sol @@ -6,7 +6,6 @@ import { IPAsset } from "contracts/lib/IPAsset.sol"; /// @notice IP Org Interface interface IIPOrg { - /// @notice Returns the current owner of the IP asset within th IP Org. function ownerOf(uint256 id) external view returns (address); @@ -37,4 +36,8 @@ interface IIPOrg { /// @notice Returns the Ip Org asset type for a given IP Org asset. function ipOrgAssetType(uint256 id_) external view returns (uint8); + /// @notice Gets the global IP asset id associated with this IP Org asset. + /// @param id_ The local id of the IP Org wrapped IP asset. + /// @return The global identifier of the IP asset. + function ipAssetId(uint256 id_) external returns (uint256); } diff --git a/contracts/ip-org/IPOrg.sol b/contracts/ip-org/IPOrg.sol index 705ec3a6..8ed84977 100644 --- a/contracts/ip-org/IPOrg.sol +++ b/contracts/ip-org/IPOrg.sol @@ -85,11 +85,11 @@ contract IPOrg is } /// @notice Gets the global IP asset id associated with this IP Org asset. - /// @param id The local id of the IP Org wrapped IP asset. + /// @param id_ The local id of the IP Org wrapped IP asset. /// @return The global identifier of the IP asset. - function ipAssetId(uint256 id) public returns (uint256) { + function ipAssetId(uint256 id_) public returns (uint256) { address registrationModule = address(MODULE_REGISTRY.protocolModule(REGISTRATION_MODULE_KEY)); - return IRegistrationModule(registrationModule).ipAssetId(address(this), id); + return IRegistrationModule(registrationModule).ipAssetId(address(this), id_); } /// @notice Initializes an IP Org. diff --git a/contracts/ip-org/IPOrgController.sol b/contracts/ip-org/IPOrgController.sol index 31744ec7..9a3fc6dc 100644 --- a/contracts/ip-org/IPOrgController.sol +++ b/contracts/ip-org/IPOrgController.sol @@ -208,15 +208,6 @@ contract IPOrgController is } } - /// @dev Checks whether an IP Org exists, throwing if not. - /// @param ipOrg_ The address of the IP Org being queried. - function _assertIPOrgExists(address ipOrg_) internal view { - IPOrgControllerStorage storage $ = _getIpOrgControllerStorage(); - if (!$.ipOrgs[ipOrg_].registered) { - revert Errors.IPOrgController_IPOrgNonExistent(); - } - } - /// @dev Authorizes upgrade to a new contract address via UUPS. function _authorizeUpgrade(address) internal diff --git a/contracts/lib/Errors.sol b/contracts/lib/Errors.sol index ed83c832..2c702e35 100644 --- a/contracts/lib/Errors.sol +++ b/contracts/lib/Errors.sol @@ -293,6 +293,7 @@ library Errors { error LicensingModule_InvalidAction(); error LicensingModule_CallerNotLicensor(); error LicensingModule_ParentLicenseNotActive(); + error LicensingModule_DerivativeNotAllowed(); error LicensingModule_InvalidIpa(); error LicensingModule_CallerNotLicenseOwner(); error LicensingModule_CantFindParentLicenseOrRelatedIpa(); diff --git a/contracts/modules/licensing/LicenseRegistry.sol b/contracts/modules/licensing/LicenseRegistry.sol index afa9e5aa..e7ea0f77 100644 --- a/contracts/modules/licensing/LicenseRegistry.sol +++ b/contracts/modules/licensing/LicenseRegistry.sol @@ -202,6 +202,10 @@ contract LicenseRegistry is ERC721 { return _licenses[id_].isReciprocal; } + function isDerivativeAllowed(uint256 id_) external view returns (bool) { + return _licenses[id_].derivativesAllowed; + } + function derivativeNeedsApproval(uint256 id_) external view returns (bool) { return _licenses[id_].derivativeNeedsApproval; } diff --git a/contracts/modules/licensing/LicensingModule.sol b/contracts/modules/licensing/LicensingModule.sol index 9101a6b8..7068e6ee 100644 --- a/contracts/modules/licensing/LicensingModule.sol +++ b/contracts/modules/licensing/LicensingModule.sol @@ -139,6 +139,9 @@ contract LicensingModule is BaseModule, ILicensingModule { if (!LICENSE_REGISTRY.isLicenseActive(input.parentLicenseId)) { revert Errors.LicensingModule_ParentLicenseNotActive(); } + if (!LICENSE_REGISTRY.isDerivativeAllowed(input.parentLicenseId)) { + revert Errors.LicensingModule_DerivativeNotAllowed(); + } } // If this is a derivative and parent is reciprocal, license parameters // cannot be changed in the new license @@ -159,8 +162,7 @@ contract LicensingModule is BaseModule, ILicensingModule { ) ); } else { - // If this is not a derivative, or parent is not reciprocal, caller must be - // the licensor + // If this is not a derivative, or parent is not reciprocal, caller must be the licensor if (licensor != caller_) { revert Errors.LicensingModule_CallerNotLicensor(); } @@ -196,14 +198,23 @@ contract LicensingModule is BaseModule, ILicensingModule { bool derivativesAllowed, bool isReciprocal, bool derivativeNeedsApproval - ) = _parseLicenseParameters( - ipOrg_, - input.params, - supportedParams - ); + ) = _parseLicenseParameters(ipOrg_, input.params, supportedParams); + + Licensing.LicenseStatus newLicenseStatus; + if ( + input.parentLicenseId != 0 && + LICENSE_REGISTRY.derivativeNeedsApproval(input.parentLicenseId) + ) { + // If parent license ID has `derivativeNeedsApproval` = true, then new license is pending licensor approval. + // This condition is triggered when parent's `isReciprocal` = false but `derivativeNeedsApproval` = true. + newLicenseStatus = Licensing.LicenseStatus.PendingLicensorApproval; + } else { + newLicenseStatus = Licensing.LicenseStatus.Active; + } + // Create license Licensing.LicenseData memory newLicense = Licensing.LicenseData({ - status: Licensing.LicenseStatus.Active, + status: newLicenseStatus, derivativesAllowed: derivativesAllowed, isReciprocal: isReciprocal, derivativeNeedsApproval: derivativeNeedsApproval, @@ -221,17 +232,23 @@ contract LicensingModule is BaseModule, ILicensingModule { address ipOrg_, Licensing.ParamValue[] memory inputParams_, Licensing.ParamDefinition[] memory supportedParams_ - ) private view returns ( - Licensing.ParamValue[] memory licenseParams, - bool derivativesAllowed, - bool derivativeNeedsApproval, - bool isReciprocal - ) { + ) + private + view + returns ( + Licensing.ParamValue[] memory licenseParams, + bool derivativesAllowed, + bool isReciprocal, + bool derivativeNeedsApproval + ) + { uint256 inputLength_ = inputParams_.length; - mapping(ShortString => bytes) storage _ipOrgValues = _ipOrgParamValues[ipOrg_]; + mapping(ShortString => bytes) storage _ipOrgValues = _ipOrgParamValues[ + ipOrg_ + ]; uint256 supportedLength = supportedParams_.length; licenseParams = new Licensing.ParamValue[](supportedLength); - + // First, get ipOrg defaults for (uint256 i = 0; i < supportedLength; i++) { // For every supported parameter @@ -283,7 +300,6 @@ contract LicensingModule is BaseModule, ILicensingModule { derivativeNeedsApproval = false; isReciprocal = false; } - return (licenseParams, derivativesAllowed, derivativeNeedsApproval, isReciprocal); } function _decideValueSource( @@ -313,9 +329,7 @@ contract LicensingModule is BaseModule, ILicensingModule { ShortString tag, bytes memory resultValue, bool derivativesAllowed - ) private pure returns (bool) { - - } + ) private pure returns (bool) {} /// Gets the licensor address for this IPA. function _getLicensor( diff --git a/contracts/modules/registration/RegistrationModule.sol b/contracts/modules/registration/RegistrationModule.sol index 93f25821..0378d465 100644 --- a/contracts/modules/registration/RegistrationModule.sol +++ b/contracts/modules/registration/RegistrationModule.sol @@ -64,6 +64,7 @@ contract RegistrationModule is BaseModule, IRegistrationModule, AccessControlled /// @param ipOrg_ The IP Org for which the hooks are being registered. /// @param hooks_ The addresses of the hooks to register. /// @param hooksConfig_ The configurations for the hooks. + /// @param registerParams_ The parameters for the registration. function registerHooks( HookType hType_, IIPOrg ipOrg_, @@ -297,30 +298,29 @@ contract RegistrationModule is BaseModule, IRegistrationModule, AccessControlled /// @param toIpOrg_ The address of the new governing IP Org. /// @param toIpOrgType_ The type of the IP asset within the new IP Org. /// TODO(leeren) Expose this function to FE once IP Orgs are finalized. - function _transferIPAssetToIPOrg( - address fromIpOrg_, - uint256 fromIpOrgAssetId_, - address toIpOrg_, - uint8 toIpOrgType_, - address from_, - address to_ - ) internal returns (uint256 ipAssetId_, uint256 ipOrgAssetId_) { - uint256 id = ipAssetId[address(fromIpOrg_)][fromIpOrgAssetId_]; - - address owner = IIPOrg(fromIpOrg_).ownerOf(ipOrgAssetId_); - - delete ipAssetId[address(fromIpOrg_)][fromIpOrgAssetId_]; - delete ipOrgAssets[id]; - IIPOrg(fromIpOrg_).burn(ipOrgAssetId_); - IPA_REGISTRY.transferIPOrg( - ipAssetId_, - toIpOrg_ - ); - ipOrgAssetId_ = IIPOrg(toIpOrg_).mint(owner, toIpOrgType_); - IPOrgAsset memory ipOrgAsset = IPOrgAsset(toIpOrg_, ipOrgAssetId_); - ipOrgAssets[id] = ipOrgAsset; - ipAssetId[address(toIpOrg_)][ipOrgAssetId_] = id; - } + /// NOTE: This function is currently not used, but will be used in the future. Commented out for now. + // function _transferIPAssetToIPOrg( + // address fromIpOrg_, + // uint256 fromIpOrgAssetId_, + // address toIpOrg_, + // uint8 toIpOrgType_, + // address from_, + // address to_ + // ) internal returns (uint256 ipAssetId_, uint256 ipOrgAssetId_) { + // uint256 id = ipAssetId[address(fromIpOrg_)][fromIpOrgAssetId_]; + // address owner = IIPOrg(fromIpOrg_).ownerOf(ipOrgAssetId_); + // delete ipAssetId[address(fromIpOrg_)][fromIpOrgAssetId_]; + // delete ipOrgAssets[id]; + // IIPOrg(fromIpOrg_).burn(ipOrgAssetId_); + // IPA_REGISTRY.transferIPOrg( + // ipAssetId_, + // toIpOrg_ + // ); + // ipOrgAssetId_ = IIPOrg(toIpOrg_).mint(owner, toIpOrgType_); + // IPOrgAsset memory ipOrgAsset = IPOrgAsset(toIpOrg_, ipOrgAssetId_); + // ipOrgAssets[id] = ipOrgAsset; + // ipAssetId[address(toIpOrg_)][ipOrgAssetId_] = id; + // } /// @dev Adds new IP asset types to an IP Org. diff --git a/test/foundry/e2e/e2e.t.sol b/test/foundry/e2e/e2e.t.sol index dc66f284..8bd767a9 100644 --- a/test/foundry/e2e/e2e.t.sol +++ b/test/foundry/e2e/e2e.t.sol @@ -1,7 +1,9 @@ +/* solhint-disable contract-name-camelcase, func-name-mixedcase, var-name-mixedcase */ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import { IIPOrg } from "contracts/interfaces/ip-org/IIPOrg.sol"; +import { IPOrg } from "contracts/ip-org/IPOrg.sol"; import { AccessControl } from "contracts/lib/AccessControl.sol"; import { RegistrationModule } from "contracts/modules/registration/RegistrationModule.sol"; import { StoryProtocol } from "contracts/StoryProtocol.sol"; @@ -15,33 +17,70 @@ import { HookResult, IHook } from "contracts/interfaces/hooks/base/IHook.sol"; import { Hook } from "contracts/lib/hooks/Hook.sol"; import { TokenGated } from "contracts/lib/hooks/TokenGated.sol"; import { PolygonToken } from "contracts/lib/hooks/PolygonToken.sol"; +import { MockERC20 } from "test/foundry/mocks/MockERC20.sol"; import { MockERC721 } from "test/foundry/mocks/MockERC721.sol"; import { Licensing } from "contracts/lib/modules/Licensing.sol"; import { BaseTest } from "test/foundry/utils/BaseTest.sol"; import { LibRelationship } from "contracts/lib/modules/LibRelationship.sol"; -import { ShortString, ShortStrings } from "@openzeppelin/contracts/utils/ShortStrings.sol"; +import { SPUMLParams } from "contracts/lib/modules/SPUMLParams.sol"; import { Registration } from "contracts/lib/modules/Registration.sol"; import { IE2ETest } from "test/foundry/interfaces/IE2ETest.sol"; +import { Errors } from "contracts/lib/Errors.sol"; +import { BitMask } from "contracts/lib/BitMask.sol"; +import { ShortString, ShortStrings } from "@openzeppelin/contracts/utils/ShortStrings.sol"; import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; import { Create2 } from "@openzeppelin/contracts/utils/Create2.sol"; -import { Hook } from "contracts/lib/hooks/Hook.sol"; contract E2ETest is IE2ETest, BaseTest { using ShortStrings for *; - address public tokenGatedHook; - address public polygonTokenHook; - MockERC721 public mockNFT; + TokenGatedHook internal tokenGatedHook; + PolygonTokenHook public polygonTokenHook; + MockERC721 internal mockNFT; + MockERC20 internal mockERC20; + + address internal ipOrgOwner1 = address(1234); + address internal ipOrgOwner2 = address(4567); + address internal ipOrgOwner3 = address(6789); + address internal ipAssetOwner1 = address(6789); + address internal ipAssetOwner2 = address(9876); + address internal ipAssetOwner3 = address(8888); + address internal ipAssetOwner4 = address(7777); + address internal ipAssetOwner5 = address(6666); - // create 3 roles: protocol admin, ip org owner, ip asset owner - address public ipOrgOwner1 = address(1234); - address public ipOrgOwner2 = address(4567); - address public ipAssetOwner1 = address(6789); - address public ipAssetOwner2 = address(9876); - address public ipAssetOwner3 = address(9876); + address internal ipOrg1; + address internal ipOrg2; + address internal ipOrg3; - address public ipOrg1; - address public ipOrg2; + string internal FRAMEWORK_ID_DOGnCO = "test_framework_dog_and_co"; + string internal FRAMEWORK_ID_CATnCO = "test_framework_cat_and_co"; + string internal FRAMEWORK_ID_ORG3 = "test_framework_org3"; + + uint256 internal mockPolygonTokenHookNonce; + + // variables defined here to avoid stack too deep error + bytes[] internal hooksTransferIPAsset; + uint256 internal licenseId_1_nonDeriv; + uint256 internal licenseId_2_deriv; + uint256 internal licenseId_3_deriv; + uint256 internal licenseId_4_sub_deriv; + uint256 internal ipAssetId_1; + uint256 internal ipAssetId_2; + uint256 internal ipAssetId_3; + uint256 internal ipAssetId_4; + uint256 internal ipAssetId_5; + uint256 internal ipOrg1_AssetId_1; + uint256 internal ipOrg1_AssetId_2; + uint256 internal ipOrg2_AssetId_1; + uint256 internal ipOrg2_AssetId_2; + uint256 internal ipOrg3_AssetId_1; + uint256 internal relIdProtocolLevel; + string internal ipOrg1_baseUri = "http://iporg1.baseuri.url"; + string internal ipOrg2_baseUri = "http://iporg2.baseuri.url"; + string internal ipOrg3_baseUri = "http://iporg3.baseuri.url"; + string internal ipOrg1_contractUri = "http://iporg1.contracturi.url"; + string internal ipOrg2_contractUri = "http://iporg2.contracturi.url"; + string internal ipOrg3_contractUri = "http://iporg3.contracturi.url"; function setUp() public virtual override { super.setUp(); @@ -65,163 +104,982 @@ contract E2ETest is IE2ETest, BaseTest { /// TOKEN_GATED_HOOK bytes memory tokenGatedHookCode = abi.encodePacked( - type(TokenGatedHook).creationCode, abi.encode(address(accessControl))); - tokenGatedHook = _deployHook(tokenGatedHookCode, Hook.SYNC_FLAG, 0); - moduleRegistry.registerProtocolHook("TokenGatedHook", IHook(tokenGatedHook)); + type(TokenGatedHook).creationCode, + abi.encode(address(accessControl)) + ); + tokenGatedHook = TokenGatedHook( + _deployHook(tokenGatedHookCode, Hook.SYNC_FLAG, 0) + ); + moduleRegistry.registerProtocolHook( + "TokenGatedHook", + IHook(tokenGatedHook) + ); /// POLYGON_TOKEN_HOOK MockPolygonTokenClient mockPolygonTokenClient = new MockPolygonTokenClient(); bytes memory polygonTokenHookCode = abi.encodePacked( type(PolygonTokenHook).creationCode, - abi.encode(address(accessControl), mockPolygonTokenClient, address(this)) + abi.encode( + address(accessControl), + mockPolygonTokenClient, + address(this) + ) + ); + polygonTokenHook = PolygonTokenHook( + _deployHook(polygonTokenHookCode, Hook.ASYNC_FLAG, 0) + ); + moduleRegistry.registerProtocolHook( + "PolygonTokenHook", + IHook(polygonTokenHook) ); - polygonTokenHook = _deployHook(polygonTokenHookCode, Hook.ASYNC_FLAG, 0); - moduleRegistry.registerProtocolHook("PolygonTokenHook", IHook(polygonTokenHook)); - /// MOCK_ERC_721 + /// MOCK_ERC721, for regular token-gated hook + /// In the example, ipAssetOwner1 and ipAssetOwner2 are owners of IPAs that are + /// registered to an IPOrg (IPOrg 1) that uses TokenGated hook as pre-hook. mockNFT = new MockERC721(); mockNFT.mint(ipAssetOwner1, 1); mockNFT.mint(ipAssetOwner2, 2); - // framework + /// MOCK_ERC20, for Polygon token hook + /// In the example, ipAssetOwner3 and ipAssetOwner4 are owners of IPAs that are + /// registered to an IPOrg (IPOrg 2) that uses PolygonToken hook as pre-hook. + mockERC20 = new MockERC20("MockERC20", "MERC20", 18); + mockERC20.mint(2000); + mockERC20.transfer(ipAssetOwner3, 1000); + mockERC20.transfer(ipAssetOwner4, 1000); + + /// From above, you can also stack pre- or post-hooks! + + /// Setups + _setUp_LicensingFramework(); + + vm.label(ipOrgOwner1, "ipOrgOwner1"); + vm.label(ipOrgOwner2, "ipOrgOwner2"); + } + + function _setUp_LicensingFramework() internal { + // + /// Licensing Framework with ID: Dog & Co. (FRAMEWORK_ID_DOGnCO) + // + + uint8[] memory enabledDerivativeIndice = new uint8[](2); + enabledDerivativeIndice[0] = SPUMLParams.ALLOWED_WITH_APPROVAL_INDEX; + // enabledDerivativeIndice[1] = SPUMLParams + // .ALLOWED_WITH_RECIPROCAL_LICENSE_INDEX; + + // Use 4 of SPUMLParams for Dog & Co. Licensing.ParamDefinition[] - memory fParams = new Licensing.ParamDefinition[](3); - fParams[0] = Licensing.ParamDefinition({ - tag: "TEST_TAG_1".toShortString(), + memory paramDefs = new Licensing.ParamDefinition[](4); + ShortString[] memory derivativeChoices = new ShortString[](3); + derivativeChoices[0] = SPUMLParams + .ALLOWED_WITH_APPROVAL + .toShortString(); + derivativeChoices[1] = SPUMLParams + .ALLOWED_WITH_RECIPROCAL_LICENSE + .toShortString(); + derivativeChoices[2] = SPUMLParams + .ALLOWED_WITH_ATTRIBUTION + .toShortString(); + + paramDefs[0] = Licensing.ParamDefinition( + SPUMLParams.CHANNELS_OF_DISTRIBUTION.toShortString(), + Licensing.ParameterType.ShortStringArray, + "", + "" + ); + paramDefs[1] = Licensing.ParamDefinition( + SPUMLParams.ATTRIBUTION.toShortString(), + Licensing.ParameterType.Bool, + abi.encode(true), + "" + ); + paramDefs[2] = Licensing.ParamDefinition( + SPUMLParams.DERIVATIVES_ALLOWED.toShortString(), + Licensing.ParameterType.Bool, + abi.encode(true), + "" + ); + paramDefs[3] = Licensing.ParamDefinition({ + tag: SPUMLParams.DERIVATIVES_ALLOWED_OPTIONS.toShortString(), + paramType: Licensing.ParameterType.MultipleChoice, + // defaultValue: abi.encode( + // BitMask._convertToMask(enabledDerivativeIndice) + // ), + defaultValue: "", + availableChoices: abi.encode(derivativeChoices) + }); + + vm.prank(licensingManager); + licensingFrameworkRepo.addFramework( + Licensing.SetFramework({ + id: FRAMEWORK_ID_DOGnCO, + textUrl: "text_url_dog_and_co", + paramDefs: paramDefs + }) + ); + + // + // Licensing Framework with ID: Cat & Co. (FRAMEWORK_ID_CATnCO) + // + + paramDefs = new Licensing.ParamDefinition[](4); + + ShortString[] memory catColorChoices = new ShortString[](2); + catColorChoices[0] = "cat_is_gold".toShortString(); + catColorChoices[1] = "cat_is_gray".toShortString(); + paramDefs[0] = Licensing.ParamDefinition({ + tag: "TEST_TAG_CAT_COLOR".toShortString(), + paramType: Licensing.ParameterType.MultipleChoice, + defaultValue: abi.encode(0), + availableChoices: abi.encode(catColorChoices) + }); + paramDefs[1] = Licensing.ParamDefinition({ + tag: "TEST_TAG_CAT_IS_CUTE".toShortString(), paramType: Licensing.ParameterType.Bool, defaultValue: abi.encode(true), availableChoices: "" }); - fParams[1] = Licensing.ParamDefinition({ - tag: "TEST_TAG_2".toShortString(), - paramType: Licensing.ParameterType.Number, - defaultValue: abi.encode(123), - availableChoices: "" - }); - ShortString[] memory choices = new ShortString[](2); - choices[0] = "test1".toShortString(); - choices[1] = "test2".toShortString(); - fParams[2] = Licensing.ParamDefinition({ - tag: "TEST_TAG_3".toShortString(), + paramDefs[2] = Licensing.ParamDefinition( + SPUMLParams.DERIVATIVES_ALLOWED.toShortString(), + Licensing.ParameterType.Bool, + abi.encode(true), + "" + ); + paramDefs[3] = Licensing.ParamDefinition({ + tag: SPUMLParams.DERIVATIVES_ALLOWED_OPTIONS.toShortString(), paramType: Licensing.ParameterType.MultipleChoice, - defaultValue: abi.encode(1), - availableChoices: abi.encode(choices) - }); - Licensing.SetFramework memory framework = Licensing.SetFramework({ - id: "test_framework", - textUrl: "text_url", - paramDefs: fParams + defaultValue: "", + availableChoices: abi.encode(derivativeChoices) }); + vm.prank(licensingManager); - licensingFrameworkRepo.addFramework(framework); - } + licensingFrameworkRepo.addFramework( + Licensing.SetFramework({ + id: FRAMEWORK_ID_CATnCO, + textUrl: "text_url_cat_and_co", + paramDefs: paramDefs + }) + ); - function test_e2e() public { // - // IPOrg owner create IPOrgs + // Licensing Framework with ID: Org3 (FRAMEWORK_ID_ORG3) // - string[] memory ipAssetTypes = new string[](2); - ipAssetTypes[0] = "CHARACTER"; - ipAssetTypes[1] = "STORY"; + paramDefs = new Licensing.ParamDefinition[](1); + + paramDefs[0] = Licensing.ParamDefinition( + SPUMLParams.DERIVATIVES_ALLOWED.toShortString(), + Licensing.ParameterType.Bool, + abi.encode(false), + "" + ); + + vm.prank(licensingManager); + licensingFrameworkRepo.addFramework( + Licensing.SetFramework({ + id: FRAMEWORK_ID_ORG3, + textUrl: "text_url_org3", + paramDefs: paramDefs + }) + ); + } + + function test_e2e() public { + /// + /// ========================================= + /// Create IPOrgs + /// ========================================= + /// + + string[] memory ipAssetTypesShared = new string[](2); + string[] memory ipAssetTypesOnly1 = new string[](1); + string[] memory ipAssetTypesOnly2 = new string[](1); + string[] memory ipAssetTypesScratchPad = new string[](3); + ipAssetTypesShared[0] = "CHARACTER"; + ipAssetTypesShared[1] = "STORY"; + ipAssetTypesOnly1[0] = "MOVIE"; + ipAssetTypesOnly2[0] = "MUSIC"; + ipOrg1 = spg.registerIpOrg( ipOrgOwner1, "IPOrgName1", "IPO1", - ipAssetTypes + ipAssetTypesShared ); ipOrg2 = spg.registerIpOrg( ipOrgOwner2, "IPOrgName2", "IPO2", - ipAssetTypes + ipAssetTypesShared + ); + ipOrg3 = spg.registerIpOrg( + ipOrgOwner3, + "IPOrgName3", + "IPO3", + ipAssetTypesShared ); vm.label(ipOrg1, "IPOrg_1"); vm.label(ipOrg2, "IPOrg_2"); - - string[] memory ipAssetTypesMore1 = new string[](1); - string[] memory ipAssetTypesMore2 = new string[](1); - ipAssetTypesMore1[0] = "MOVIE"; - ipAssetTypesMore2[0] = "MUSIC"; + vm.label(ipOrg3, "IPOrg_3"); // TODO: check for event `ModuleConfigured` vm.prank(ipOrgOwner1); - spg.addIPAssetTypes(ipOrg1, ipAssetTypesMore1); + spg.addIPAssetTypes(ipOrg1, ipAssetTypesOnly1); - // TODO: check for event `ModuleConfigured` vm.prank(ipOrgOwner2); - spg.addIPAssetTypes(ipOrg2, ipAssetTypesMore2); + spg.addIPAssetTypes(ipOrg2, ipAssetTypesOnly2); + + ipAssetTypesScratchPad = registrationModule.getIpOrgAssetTypes(ipOrg3); + for (uint256 i = 0; i < ipAssetTypesScratchPad.length; i++) { + assertEq( + ipAssetTypesScratchPad[i], + ipAssetTypesShared[i], + "ipAssetTypesScratchPad[i] should match ipAssetTypesShared[i]" + ); + } + + ipAssetTypesScratchPad = registrationModule.getIpOrgAssetTypes(ipOrg2); + assertEq( + ipAssetTypesScratchPad.length, + ipAssetTypesShared.length + ipAssetTypesOnly1.length, + "length should match" + ); + + /// + /// ========================================= + /// Configure IPOrg modules + /// ========================================= + /// + + vm.expectEmit(address(registrationModule)); + emit MetadataUpdated(ipOrg1, ipOrg1_baseUri, ipOrg1_contractUri); + vm.prank(ipOrgOwner1); + spg.setMetadata(ipOrg1, ipOrg1_baseUri, ipOrg1_contractUri); + assertEq( + registrationModule.contractURI(ipOrg1), + ipOrg1_contractUri, + "contractURI should be ipOrg1_contractUri" + ); + assertEq( + IIPOrg(ipOrg1).contractURI(), + ipOrg1_contractUri, + "contractURI should be ipOrg1_contractUri" + ); + // TODO: tokenURI check + // assertEq(registrationModule.tokenURI(address(ipOrg), 1, 0), ""); + + vm.prank(ipOrgOwner2); + spg.setMetadata(ipOrg2, ipOrg2_baseUri, ipOrg2_contractUri); + assertEq( + registrationModule.contractURI(ipOrg2), + ipOrg2_contractUri, + "contractURI should be ipOrg2_contractUri" + ); + assertEq( + IIPOrg(ipOrg2).contractURI(), + ipOrg2_contractUri, + "contractURI should be ipOrg2_contractUri" + ); + + vm.prank(ipOrgOwner3); + spg.setMetadata(ipOrg3, ipOrg3_baseUri, ipOrg3_contractUri); + + /// + /// ========================================= + /// Register hooks via RegistrationModule + /// ========================================= + /// + + _registerHooksForIPOrgs(); + + /// + /// ========================================= + /// Add Relationship types + /// ========================================= + /// + + LibRelationship.RelatedElements memory allowedElements = LibRelationship + .RelatedElements({ + src: LibRelationship.Relatables.ADDRESS, + dst: LibRelationship.Relatables.ADDRESS + }); + uint8[] memory allowedSrcs = new uint8[](2); + allowedSrcs[0] = 0; + allowedSrcs[1] = 2; + uint8[] memory allowedDsts = new uint8[](1); + allowedDsts[0] = 1; + LibRelationship.AddRelationshipTypeParams + memory relProtocolLevelParams = LibRelationship + .AddRelationshipTypeParams({ + relType: "TEST_RELATIONSHIP_PROTOCOL_LEVEL", + ipOrg: LibRelationship.PROTOCOL_LEVEL_RELATIONSHIP, + allowedElements: allowedElements, + allowedSrcs: allowedSrcs, + allowedDsts: allowedDsts + }); + // Admin needs to add protocol-level relationship types + vm.startPrank(admin); + spg.addRelationshipType(relProtocolLevelParams); + spg.removeRelationshipType( + LibRelationship.PROTOCOL_LEVEL_RELATIONSHIP, + "TEST_RELATIONSHIP_PROTOCOL_LEVEL" + ); + spg.addRelationshipType(relProtocolLevelParams); + vm.stopPrank(); + + allowedElements = LibRelationship.RelatedElements({ + src: LibRelationship.Relatables.ADDRESS, + dst: LibRelationship.Relatables.ADDRESS + }); + allowedSrcs = new uint8[](0); + allowedDsts = new uint8[](0); + LibRelationship.AddRelationshipTypeParams + memory relAppearInParams = LibRelationship + .AddRelationshipTypeParams({ + relType: "APPEAR_IN", + ipOrg: ipOrg1, + allowedElements: allowedElements, + allowedSrcs: allowedSrcs, + allowedDsts: allowedDsts + }); + // TODO: event check for `addRelationshipType` (event `RelationshipTypeSet`) + vm.prank(ipOrgOwner1); + spg.addRelationshipType(relAppearInParams); + + /// + /// ========================================= + /// Create Relationships for IPOrgs + /// based on above Relationship types + /// ========================================= + /// + + // In IPOrg 1, create a relationship from asset ID 1 to asset ID 2 (local to IPOrg 1) + // to indicate that ID 1 appears in ID 2 + LibRelationship.CreateRelationshipParams + memory crParams = LibRelationship.CreateRelationshipParams({ + relType: "APPEAR_IN", + srcAddress: ipOrg1, + srcId: 1, // source, global asset id + dstAddress: ipOrg1, + dstId: 2 // destination, global asset id + }); + + vm.prank(ipOrgOwner1); + relIdProtocolLevel = spg.createRelationship( + ipOrg1, + crParams, + new bytes[](0), // preHooksDataRel + new bytes[](0) // postHooksDataRel + ); + assertEq(relIdProtocolLevel, 1, "relIdProtocolLevel should be 1"); + LibRelationship.Relationship memory rel = relationshipModule + .getRelationship(1); + assertEq(rel.relType, "APPEAR_IN", "relType should be APPEAR_IN"); + assertEq(rel.srcAddress, ipOrg1, "srcAddress should be ipOrg1"); + assertEq(rel.dstAddress, ipOrg1, "dstAddress should be ipOrg1"); + assertEq(rel.srcId, 1, "srcId should be 1"); + assertEq(rel.dstId, 2, "dstId should be 2"); + assertTrue( + relationshipModule.relationshipExists(rel), + "Relationship should exist" + ); + + /// + /// ========================================= + /// Configure IPOrg's org-wide Licensing + /// ========================================= + /// + + // + // NOTE: For each ipOrg, we set IPOrg-wide Licensing terms that get applied to any Licenses under that IPOrg. + // Licenses can modify terms within its IPOrg's assigned Licensing framework, as long as those terms + // aren't specified in IPOrg-wide Licensing terms. + // In other words, you must use IPOrg-wide Licensing terms and modify what's untouched. + // + + // + // Configure licensing for IPOrg1 (Dog & Co.) + // Enforce these license terms to all Licenses under IPOrg1. + // + + Licensing.ParamValue[] memory lParams = new Licensing.ParamValue[](3); + ShortString[] memory channel_distribution = new ShortString[](2); + + channel_distribution[0] = "dog loves hoomans".toShortString(); + channel_distribution[1] = "dog conquers world".toShortString(); + + uint8[] memory enabledDerivativeIndice = new uint8[](1); + enabledDerivativeIndice[0] = SPUMLParams.ALLOWED_WITH_APPROVAL_INDEX; + // enabledDerivativeIndice[1] = SPUMLParams + // .ALLOWED_WITH_ATTRIBUTION_INDEX; + + // Use the list of terms from SPUMLParams + lParams = new Licensing.ParamValue[](4); + lParams[0] = Licensing.ParamValue({ + tag: SPUMLParams.CHANNELS_OF_DISTRIBUTION.toShortString(), + value: abi.encode(channel_distribution) + }); + lParams[1] = Licensing.ParamValue({ + tag: SPUMLParams.ATTRIBUTION.toShortString(), + value: abi.encode(true) // unset + }); + lParams[2] = Licensing.ParamValue({ + tag: SPUMLParams.DERIVATIVES_ALLOWED.toShortString(), + value: abi.encode(true) + }); + lParams[3] = Licensing.ParamValue({ + tag: SPUMLParams.DERIVATIVES_ALLOWED_OPTIONS.toShortString(), + // (active) derivative options are set via bitmask + value: abi.encode(BitMask._convertToMask(enabledDerivativeIndice)) + }); + + Licensing.LicensingConfig memory licensingConfig = Licensing + .LicensingConfig({ + frameworkId: FRAMEWORK_ID_DOGnCO, + params: lParams, + // licensor: Licensing.LicensorConfig.Source + licensor: Licensing.LicensorConfig.IpOrgOwnerAlways + }); + + // TODO: event check for `configureIpOrgLicensing` + vm.startPrank(ipOrgOwner1); + spg.configureIpOrgLicensing(ipOrg1, licensingConfig); + // Two `configureIpOrgLicensing`s are commented out since right now, we allow + // `configureIpOrgLicensing` to be called only once per IPOrg. + // spg.configureIpOrgLicensing(ipOrg2, licensingConfig); // this should get overwritten by Unset + // spg.configureIpOrgLicensing(ipOrg2, Licensing.LicensingConfig({ + // frameworkId: FRAMEWORK_ID_DOGnCO, + // params: lParams, + // licensor: Licensing.LicensorConfig.Unset + // })); + vm.stopPrank(); + + // + // Configure licensing for IPOrg2 (Cat & Co.). + // Enforce these license terms to all Licenses under IPOrg2. + // => TEST_TAG_CAT_COLOR = 1 (cat_is_gray) + // => TEST_TAG_CAT_IS_CUTE = true + // + + lParams = new Licensing.ParamValue[](2); + lParams[0] = Licensing.ParamValue({ + tag: "TEST_TAG_CAT_COLOR".toShortString(), + value: abi.encode(1) // BitMask, or just 1 to indicate index 1 + }); + lParams[1] = Licensing.ParamValue({ + tag: "TEST_TAG_CAT_IS_CUTE".toShortString(), + value: abi.encode(true) + }); + + licensingConfig = Licensing.LicensingConfig({ + frameworkId: FRAMEWORK_ID_CATnCO, + params: lParams, + // licensor: Licensing.LicensorConfig.Source + licensor: Licensing.LicensorConfig.IpOrgOwnerAlways + }); + + // TODO: event check for `configureIpOrgLicensing` + vm.prank(ipOrgOwner2); + spg.configureIpOrgLicensing(ipOrg2, licensingConfig); + + // + // Configure licensing for IPOrg3. + // Enforce these license terms to all Licenses under IPOrg3. + // => DERIVATIVES_ALLOWED = false + // + + lParams = new Licensing.ParamValue[](1); + lParams[0] = Licensing.ParamValue({ + tag: SPUMLParams.DERIVATIVES_ALLOWED.toShortString(), + value: abi.encode(false) + }); + + licensingConfig = Licensing.LicensingConfig({ + frameworkId: FRAMEWORK_ID_ORG3, + params: lParams, + licensor: Licensing.LicensorConfig.Source + // licensor: Licensing.LicensorConfig.IpOrgOwnerAlways + }); + + vm.prank(ipOrgOwner3); + spg.configureIpOrgLicensing(ipOrg3, licensingConfig); // - // IPOrg owner configure modules + // Configure // + /// + /// ========================================= + /// Register IP Assets + /// ========================================= + /// + + _registerIpAssets(); + + /// + /// ========================================= + /// IP Assets Transfers + /// ========================================= + /// + vm.expectEmit(address(registrationModule)); - emit MetadataUpdated( + emit IPAssetTransferred(1, ipOrg1, 1, ipAssetOwner1, ipAssetOwner2); + vm.prank(ipAssetOwner1); + spg.transferIPAsset( + ipOrg1, + ipAssetOwner1, + ipAssetOwner2, + 1, // global asset id + // IPOrg1 has no pre-hooks set for action `TRANSFER_IP_ASSET`, so we pass in empty params + new bytes[](0), + new bytes[](0) + ); + assertEq( + registry.ipAssetOwner(1), + ipAssetOwner2, + "owner should be ipAssetOwner2 after transferIPAsset" + ); + + // Transfer back, `ipAssetOwner2` also has enough balance of mockNFT + vm.prank(ipAssetOwner2); + spg.transferIPAsset( + ipOrg1, + ipAssetOwner2, + ipAssetOwner1, + 1, // global asset id + // IPOrg1 has no pre-hooks set for action `TRANSFER_IP_ASSET`, so we pass in empty params + new bytes[](0), + new bytes[](0) + ); + assertEq( + registry.ipAssetOwner(1), + ipAssetOwner1, + "owner should be ipAssetOwner1 after transferIPAsset" + ); + + // // Since we've configured PolygonToken hook for IPOrg2 on `TRANSFER_IP_ASSET` action, + // // it will get triggered here. The hook will check if the sender has enough token + // // balance (in this case, mockERC20) to transfer the IP asset. If not, it will revert. + // hooksTransferIPAsset = new bytes[](1); + // hooksTransferIPAsset[0] = abi.encode(ipAssetOwner4); + + // // vm.expectEmit(address(registrationModule)); + // // emit IPAssetTransferred(1, ipOrg1, 1, ipAssetOwner1, ipAssetOwner2); + // vm.prank(ipAssetOwner4); + // spg.transferIPAsset( + // ipOrg2, + // ipAssetOwner4, + // ipAssetOwner3, + // 4, // asset id + // // IPOrg2 has 1 pre-hook set for action `TRANSFER_IP_ASSET` + // hooksTransferIPAsset, + // new bytes[](0) + // ); + + // _triggerMockPolygonTokenHook(ipAssetOwner4); + + /// + /// ========================================= + /// Random IP Asset actions + /// ========================================= + /// + + vm.prank(address(registrationModule)); + vm.expectEmit(address(registry)); + emit IPOrgTransferred(ipAssetId_2, ipOrg1, ipOrg2); + registry.transferIPOrg(ipAssetId_2, ipOrg2); + assertEq(registry.ipAssetOrg(ipAssetId_2), ipOrg2, "IPOrg should be 2"); + + // Misc. + + vm.prank(address(0)); // TODO: modify when `onlyDisputer` is complete + emit StatusChanged(ipAssetId_2, 1, 0); // 0 means unset, 1 means set (change when status is converted to ENUM) + registry.setStatus(ipAssetId_2, 0); + assertEq(registry.status(ipAssetId_2), 0, "Status should be unset"); + + registry.setStatus(ipAssetId_2, 1); // reset the status to be active + + /// + /// ========================================= + /// Create License NFTs (1) + /// ========================================= + /// + + // + // NOTE You can only add/use ParamValues that aren't used by the license's IPOrg, + // since IPOrg's license terms are enforced to all Licenses under that IPOrg. + // + + // + // Create a license for Asset ID 1 (Org 1, ID 1) + // Use SPUMLParams license framework, which is attached to Org 2 (cat & co.) + // + + // Only inherit IPOrg's org-wide licensing terms, don't set any params + Licensing.LicenseCreation memory lCreation = Licensing.LicenseCreation({ + params: new Licensing.ParamValue[](0), + parentLicenseId: 0, // no parent + ipaId: ipAssetId_1 + }); + vm.prank(ipOrgOwner1); + licenseId_1_nonDeriv = spg.createLicense( address(ipOrg1), - "http://iporg1.baseuri.url", - "http://iporg1.contracturi.url" + lCreation, + new bytes[](0), + new bytes[](0) + ); + assertEq(licenseId_1_nonDeriv, 1, "License ID should be 1"); + Licensing.LicenseData memory licenseData_1_nonDeriv = licenseRegistry + .getLicenseData(licenseId_1_nonDeriv); + assertEq( + uint8(licenseData_1_nonDeriv.status), + uint8(Licensing.LicenseStatus.Active), + "License 1 (Org 1) should active on creation + not a derivative" + ); + + assertEq( + licenseData_1_nonDeriv.derivativesAllowed, + true, + "License 1 (Org 1) should allow derivatives" + ); + assertEq( + licenseData_1_nonDeriv.isReciprocal, + false, + "License 1 (Org 1) should NOT be reciprocal" + ); + assertEq( + licenseData_1_nonDeriv.derivativeNeedsApproval, + true, + "License 1 (Org 1) should approve derivatives" + ); + assertEq( + licenseData_1_nonDeriv.ipaId, + ipAssetId_1, + "License 1 (Org 1)'s linked IPA ID should be 1" + ); + assertEq( + licenseData_1_nonDeriv.parentLicenseId, + 0, + "License 1 (Org 1) should have no parent license" + ); + + // Since this is a license without a parent license, the license should be activated immediately on + // `createLicense`. This is already checked about via status == LicenseStatus.Active, but again checked here. + // This is just a test that expects revert. + vm.expectRevert( + Errors.LicenseRegistry_LicenseNotPendingApproval.selector ); vm.prank(ipOrgOwner1); - spg.setMetadata( - ipOrg1, - "http://iporg1.baseuri.url", - "http://iporg1.contracturi.url" + spg.activateLicense(address(ipOrg1), licenseId_1_nonDeriv); + + // + // Create two more licenses for Asset ID 3 (Org 2, ID 1), this time with a parent license + // (licenseId_1_nonDeriv created above), so this is a sub-license. + // + // Since `licenseId_1_nonDeriv` is reciprocal (as we've configured for SPUMLParams), + // the two sub-licenses can't modify the params, ie. they inherit the parent's params. + // + // First sub-license is created without a linked IP asset, second sub-license is created with a linked IP asset. + // Both sub-licenses will be pending approval on creation — parent license has specified in its license terms. + // They will need to get approved by the parent license's licensor. + // + // Third sub-sub-license is derived from the second sub-license, and created without a linked IP asset. + // This license will be approved immediately on creation, as the second sub-license didn't specify the + // "require approval from derivatives" (ALLOWED_WITH_APPROVAL) in its terms. + // + // + + // + // First once should have no IP asset linked on creation. + // This license does NOT allow derivatives. + // + + lParams = new Licensing.ParamValue[](3); + // allow channel of distribution + lParams[0] = Licensing.ParamValue({ + tag: SPUMLParams.CHANNELS_OF_DISTRIBUTION.toShortString(), + value: abi.encode(true) + }); + // require attribution + lParams[1] = Licensing.ParamValue({ + tag: SPUMLParams.ATTRIBUTION.toShortString(), + value: abi.encode(true) + }); + // disable derivatives + lParams[2] = Licensing.ParamValue({ + tag: SPUMLParams.DERIVATIVES_ALLOWED.toShortString(), + value: abi.encode(false) + }); + + lCreation = Licensing.LicenseCreation({ + params: lParams, + parentLicenseId: licenseId_1_nonDeriv, + ipaId: 0 // no linked IP asset + }); + vm.prank(ipOrgOwner2); + licenseId_2_deriv = spg.createLicense( + address(ipOrg2), + lCreation, + new bytes[](0), + new bytes[](0) ); + assertEq(licenseId_2_deriv, 2, "License ID should be 2"); + Licensing.LicenseData memory licenseData_2_deriv = licenseRegistry + .getLicenseData(licenseId_2_deriv); assertEq( - registrationModule.contractURI(address(ipOrg1)), - "http://iporg1.contracturi.url" + uint8(licenseData_2_deriv.status), + uint8(Licensing.LicenseStatus.PendingLicensorApproval), + "License 2 (Org 1) should be pending approval on creation" ); - // TODO: tokenURI check - // assertEq(registrationModule.tokenURI(address(ipOrg), 1, 0), ""); - vm.expectEmit(address(registrationModule)); - emit MetadataUpdated( + vm.prank(ipOrgOwner2); + spg.activateLicense(address(ipOrg2), licenseId_2_deriv); + licenseData_2_deriv = licenseRegistry.getLicenseData(licenseId_2_deriv); // refresh license data in mem + assertEq( + uint8(licenseData_2_deriv.status), + uint8(Licensing.LicenseStatus.Active), + "License 2 (Org 1) should be active" + ); + assertEq( + licenseData_2_deriv.derivativesAllowed, + false, + "License 2 (Org 1) should NOT allow derivatives" + ); + assertEq( + licenseData_2_deriv.isReciprocal, + false, + "License 2 (Org 1) should NOT be reciprocal" + ); + assertEq( + licenseData_2_deriv.derivativeNeedsApproval, + false, + "License 2 (Org 1) should not need to approve derivatives" + ); + assertEq( + licenseData_2_deriv.ipaId, + 0, + "License 2 (Org 1) should not be linked to IPA" + ); + assertEq( + licenseData_2_deriv.parentLicenseId, + licenseId_1_nonDeriv, + "License 2 (Org 1) should have parent license" + ); + + // + // Second one should have an IP asset linked on creation + // This license allows derivatives. + // + + lParams = new Licensing.ParamValue[](2); + // allow derivatives without approval, but require reciprocal license + enabledDerivativeIndice[0] = SPUMLParams + .ALLOWED_WITH_RECIPROCAL_LICENSE_INDEX; + + // derivatives allowed + lParams[0] = Licensing.ParamValue({ + tag: SPUMLParams.DERIVATIVES_ALLOWED.toShortString(), + value: abi.encode(true) + }); + // derivative options => derivatives must be of reciprocal + lParams[1] = Licensing.ParamValue({ + tag: SPUMLParams.DERIVATIVES_ALLOWED_OPTIONS.toShortString(), + value: abi.encode(BitMask._convertToMask(enabledDerivativeIndice)) + }); + + lCreation = Licensing.LicenseCreation({ + params: lParams, + parentLicenseId: licenseId_1_nonDeriv, + ipaId: ipAssetId_3 // linked IP asset (owned by IPOrg 2) + }); + vm.prank(ipOrgOwner2); + licenseId_3_deriv = spg.createLicense( address(ipOrg2), - "http://iporg2.baseuri.url", - "http://iporg2.contracturi.url" + lCreation, + new bytes[](0), + new bytes[](0) ); + assertEq(licenseId_3_deriv, 3, "License ID should be 3"); + Licensing.LicenseData memory licenseData_3_deriv = licenseRegistry + .getLicenseData(licenseId_3_deriv); + assertEq( + uint8(licenseData_3_deriv.status), + uint8(Licensing.LicenseStatus.PendingLicensorApproval), + "License 3 (Org 2) should be pending approval on creation" + ); + + // Comment above on the first license applies here as well. vm.prank(ipOrgOwner2); - spg.setMetadata( - ipOrg2, - "http://iporg2.baseuri.url", - "http://iporg2.contracturi.url" + spg.activateLicense(address(ipOrg2), licenseId_3_deriv); + licenseData_3_deriv = licenseRegistry.getLicenseData(licenseId_3_deriv); // refresh license data + assertEq( + uint8(licenseData_3_deriv.status), + uint8(Licensing.LicenseStatus.Active), + "License 3 (Org 2) should be active" ); assertEq( - registrationModule.contractURI(address(ipOrg2)), - "http://iporg2.contracturi.url" + licenseData_3_deriv.derivativesAllowed, + true, + "License 3 (Org 2) should allow derivatives" + ); + assertEq( + licenseData_3_deriv.isReciprocal, + true, + "License 3 (Org 2) should be reciprocal" + ); + assertEq( + licenseData_3_deriv.derivativeNeedsApproval, + false, + "License 3 (Org 2) should allow derivatives without approval" + ); + assertEq( + licenseData_3_deriv.ipaId, + ipAssetId_3, + "License 3 (Org 2) should be linked to IPA" + ); + assertEq( + licenseData_3_deriv.parentLicenseId, + licenseId_1_nonDeriv, + "License 3 (Org 2) should have parent license" + ); + + // + // Check that license 2 (Org 1) doesn't allow derivative + // + + lCreation = Licensing.LicenseCreation({ + params: new Licensing.ParamValue[](0), // no licensing params + parentLicenseId: licenseId_2_deriv, // License ID 2 DOES NOT ALLOW DERIVATIVES + ipaId: 0 // no linked IPA + }); + vm.prank(ipOrgOwner3); + vm.expectRevert(Errors.LicensingModule_DerivativeNotAllowed.selector); + licenseId_4_sub_deriv = spg.createLicense( + address(ipOrg3), + lCreation, + new bytes[](0), + new bytes[](0) ); - // TODO: tokenURI check - // assertEq(registrationModule.tokenURI(address(ipOrg), 1, 0), ""); // - // IPOrg 1 owner register hooks to RegistrationModule + // Create another license that has parent license (licenseId_4_sub_deriv), which is also a sublicense. // - address[] memory hooks = new address[](2); - hooks[0] = tokenGatedHook; - hooks[1] = polygonTokenHook; + lCreation = Licensing.LicenseCreation({ + params: new Licensing.ParamValue[](0), // no licensing params + parentLicenseId: licenseId_3_deriv, // License 3 allows derivative without approval + ipaId: 0 // no linked IPA + }); + vm.prank(ipOrgOwner3); + licenseId_4_sub_deriv = spg.createLicense( + address(ipOrg3), + lCreation, + new bytes[](0), + new bytes[](0) + ); + assertEq(licenseId_4_sub_deriv, 4, "License ID should be 4"); + Licensing.LicenseData memory licenseData_4_sub_deriv = licenseRegistry + .getLicenseData(licenseId_4_sub_deriv); + assertEq( + uint8(licenseData_4_sub_deriv.status), + uint8(Licensing.LicenseStatus.Active), + "License 4 (Org 3) should active on creation" + ); + assertEq( + licenseData_4_sub_deriv.derivativesAllowed, + true, + "License 4 (Org 3) should allow derivatives (parent is reciprocal, parent allows derivative)" + ); + assertEq( + licenseData_4_sub_deriv.isReciprocal, + true, + "License 4 (Org 3) should be reciprocal (parent is reciprocal, parent allows derivative)" + ); + assertEq( + licenseData_4_sub_deriv.derivativeNeedsApproval, + false, + "License 4 (Org 3) should not need to approve derivative (parent is reciprocal)" + ); + assertEq( + licenseData_4_sub_deriv.ipaId, + 0, + "License 4 (Org 3) should not be linked to IPA" + ); + assertEq( + licenseData_4_sub_deriv.parentLicenseId, + licenseId_3_deriv, + "License 4 (Org 3) should have parent license" + ); + + /// + /// ========================================= + /// Link License NFTs (1) + /// ========================================= + /// + // Try to link license ID 1 (non-derivative) to Asset ID 3, which will fail + // because Asset ID 3 is already linked to license ID 3 (derivative) + vm.prank(address(licensingModule)); + vm.expectRevert( + Errors.LicenseRegistry_LicenseAlreadyLinkedToIpa.selector + ); + // One way to link LNFT to IPA + spg.linkLnftToIpa(ipOrg2, licenseId_1_nonDeriv, ipAssetId_3); + + // Link license ID 1 (non-derivative) to Asset ID 2 (Org 2, ID 2) + vm.prank(address(licensingModule)); + vm.expectEmit(address(licenseRegistry)); + emit LicenseNftLinkedToIpa(licenseId_2_deriv, ipAssetId_2); + // Another way to link LNFT to IPA + licenseRegistry.linkLnftToIpa(licenseId_2_deriv, ipAssetId_2); + } + + /// + /// ========================================= + /// + /// Register Hooks for IPOrgs + /// + /// ========================================= + /// + + function _registerHooksForIPOrgs() internal { + // Add token gated hook & polygon token gated hook + // Specify the configuration for the token gated hook, ie. which token to use + address[] memory hooks = new address[](1); + bytes[] memory hooksConfig = new bytes[](1); + + // TokenGated hook that uses MockERC721 TokenGated.Config memory tokenGatedConfig = TokenGated.Config({ tokenAddress: address(mockNFT) }); + + // PolygonToken hook that uses MockERC20 PolygonToken.Config memory polygonTokenConfig = PolygonToken.Config({ - tokenAddress: address(mockNFT), + tokenAddress: address(mockERC20), balanceThreshold: 1 }); - bytes[] memory hooksConfig = new bytes[](2); - hooksConfig[0] = abi.encode(tokenGatedConfig); - hooksConfig[1] = abi.encode(polygonTokenConfig); + // + // Register TokenGated hook for IPOrg1 in pre-action hooks. + // This hook is triggered on `REGISTER_IP_ASSET` action. + // => this means user needs to hold some tokens on Polygon to register IPAs. + // + hooks[0] = address(tokenGatedHook); + hooksConfig[0] = abi.encode(tokenGatedConfig); + vm.prank(ipOrgOwner1); vm.expectEmit(address(registrationModule)); emit HooksRegistered( HookRegistry.HookType.PreAction, - // from _generateRegistryKey(ipOrg_) => registryKey - keccak256(abi.encode(address(ipOrg1), Registration.REGISTER_IP_ASSET, "REGISTRATION")), + keccak256( + abi.encode( + address(ipOrg1), + Registration.REGISTER_IP_ASSET, + "REGISTRATION" + ) + ), hooks ); - vm.prank(ipOrgOwner1); RegistrationModule(registrationModule).registerHooks( HookRegistry.HookType.PreAction, IIPOrg(ipOrg1), @@ -230,71 +1088,87 @@ contract E2ETest is IE2ETest, BaseTest { abi.encode(Registration.REGISTER_IP_ASSET) ); - // protocol admin add relationship type - LibRelationship.RelatedElements memory allowedElements = LibRelationship - .RelatedElements({ - src: LibRelationship.Relatables.ADDRESS, - dst: LibRelationship.Relatables.ADDRESS - }); - uint8[] memory allowedSrcs = new uint8[](0); - uint8[] memory allowedDsts = new uint8[](0); - LibRelationship.AddRelationshipTypeParams - memory relTypeParams = LibRelationship.AddRelationshipTypeParams({ - relType: "APPEAR_IN", - ipOrg: ipOrg1, - allowedElements: allowedElements, - allowedSrcs: allowedSrcs, - allowedDsts: allowedDsts - }); - // TODO: event check for `addRelationshipType` (event `RelationshipTypeSet`) - vm.prank(ipOrgOwner1); - spg.addRelationshipType(relTypeParams); + // + // Register PolygonToken hook for IPOrg2 in pre-action hook + // This hook is triggered on both `REGISTER_IP_ASSET` and `TRANSFER_IP_ASSET` action. + // => this means user needs to hold some tokens on Polygon to register & transfer IPAs. + // + hooks[0] = address(polygonTokenHook); + hooksConfig[0] = abi.encode(polygonTokenConfig); + vm.prank(ipOrgOwner2); + vm.expectEmit(address(registrationModule)); + emit HooksRegistered( + HookRegistry.HookType.PreAction, + keccak256( + abi.encode( + address(ipOrg2), + Registration.TRANSFER_IP_ASSET, + "REGISTRATION" + ) + ), + hooks + ); + RegistrationModule(registrationModule).registerHooks( + HookRegistry.HookType.PreAction, + IIPOrg(ipOrg2), + hooks, + hooksConfig, + abi.encode(Registration.TRANSFER_IP_ASSET) + ); - Licensing.ParamValue[] memory lParams = new Licensing.ParamValue[](3); - lParams[0] = Licensing.ParamValue({ - tag: "TEST_TAG_1".toShortString(), - value: abi.encode(true) - }); - lParams[1] = Licensing.ParamValue({ - tag: "TEST_TAG_2".toShortString(), - value: abi.encode(222) - }); - ShortString[] memory ssValue = new ShortString[](2); - ssValue[0] = "test1".toShortString(); - ssValue[1] = "test2".toShortString(); - lParams[2] = Licensing.ParamValue({ - tag: "TEST_TAG_3".toShortString(), - value: abi.encode(ssValue) - }); + vm.prank(ipOrgOwner2); + vm.expectEmit(address(registrationModule)); + emit HooksRegistered( + HookRegistry.HookType.PreAction, + keccak256( + abi.encode( + address(ipOrg2), + Registration.REGISTER_IP_ASSET, + "REGISTRATION" + ) + ), + hooks + ); + RegistrationModule(registrationModule).registerHooks( + HookRegistry.HookType.PreAction, + IIPOrg(ipOrg2), + hooks, + hooksConfig, + abi.encode(Registration.REGISTER_IP_ASSET) + ); + } - Licensing.LicensingConfig memory licensingConfig = Licensing - .LicensingConfig({ - frameworkId: "test_framework", - params: lParams, - licensor: Licensing.LicensorConfig.IpOrgOwnerAlways - }); + /// + /// ========================================= + /// + /// Register IP Assets for IPOrgs + /// + /// ========================================= + /// - // TODO: event check for `configureIpOrgLicensing` (event `IpOrgTermsSet` emitted twice) - vm.prank(ipOrgOwner1); - spg.configureIpOrgLicensing(ipOrg1, licensingConfig); + function _registerIpAssets() internal { + // + // Asset ID 1 (Org 1, ID 1) + // - // ip asset owner register IP Asset - Registration.RegisterIPAssetParams - memory registerIpAssetParamsCharacter = Registration - .RegisterIPAssetParams({ - owner: ipAssetOwner1, - ipOrgAssetType: 0, - name: "Character IPA", - hash: 0x558b44f88e5959cec9c7836078a53ff4d6432142a9d5caa6f3a6eb7c83930000, - mediaUrl: "https://arweave.net/character" - }); - TokenGated.Params memory tokenGatedHookDataCharacter = TokenGated - .Params({ tokenOwner: ipAssetOwner1 }); - PolygonToken.Params memory polygonTokenDataCharacter = PolygonToken - .Params({ tokenOwnerAddress: ipAssetOwner1 }); - bytes[] memory preHooksDataCharacter = new bytes[](2); - preHooksDataCharacter[0] = abi.encode(tokenGatedHookDataCharacter); - preHooksDataCharacter[1] = abi.encode(polygonTokenDataCharacter); + string memory ipAssetMediaUrl = "https://arweave.net/character"; + bytes[] memory preHooksData = new bytes[](0); + + Registration.RegisterIPAssetParams memory ipAssetData = Registration + .RegisterIPAssetParams({ + owner: ipAssetOwner1, + ipOrgAssetType: 0, + name: "Character IPA", + hash: 0x558b44f88e5959cec9c7836078a53ff4d6432142a9d5caa6f3a6eb7c83930000, + mediaUrl: ipAssetMediaUrl + }); + + // hooks + TokenGated.Params memory tokenGatedHookData = TokenGated.Params({ + tokenOwner: ipAssetOwner1 + }); + preHooksData = new bytes[](1); + preHooksData[0] = abi.encode(tokenGatedHookData); // TODO: Solve "Stack too deep" for emitting this event // vm.expectEmit(address(tokenGatedHook)); @@ -305,149 +1179,281 @@ contract E2ETest is IE2ETest, BaseTest { // "" // ); vm.prank(ipAssetOwner1); - spg.registerIPAsset( + (ipAssetId_1, ipOrg1_AssetId_1) = spg.registerIPAsset( ipOrg1, - registerIpAssetParamsCharacter, - preHooksDataCharacter, - new bytes[](0) + ipAssetData, + preHooksData, // pre-hook param data for hooks: TokenGated + new bytes[](0) // no data since there's no registration action hook in post-hook ); - bytes32 requestIdCharacter = keccak256( - abi.encodePacked(polygonTokenHook, uint256(0)) - ); - PolygonTokenHook(polygonTokenHook).handleCallback(requestIdCharacter, 1); - assertEq(registry.ipAssetOwner(1), ipAssetOwner1); - assertEq(IIPOrg(ipOrg1).ownerOf(1), ipAssetOwner1); - - Registration.RegisterIPAssetParams - memory registerIpAssetParamsStory = Registration - .RegisterIPAssetParams({ - owner: ipAssetOwner2, - ipOrgAssetType: 1, - name: "Story IPA", - hash: 0x558b44f88e5959cec9c7836078a53ff4d6432142a9d5caa6f3a6eb7c83931111, - mediaUrl: "https://arweave.net/story" - }); - TokenGated.Params memory tokenGatedHookDataStory = TokenGated.Params({ - tokenOwner: ipAssetOwner2 - }); - PolygonToken.Params memory polygonTokenDataStory = PolygonToken.Params({ - tokenOwnerAddress: ipAssetOwner2 + assertEq(ipAssetId_1, 1, "ipAssetId_1 should be 1"); + assertEq(ipOrg1_AssetId_1, 1, "ipOrg1_AssetId_1 should be 1"); + assertEq( + IPOrg(ipOrg1).ipAssetId(ipOrg1_AssetId_1), + ipAssetId_1, + "ipOrg1_AssetId_1 should be global ID 1" + ); + assertEq( + IPOrg(ipOrg1).tokenURI(ipOrg1_AssetId_1), + ipAssetMediaUrl, + string.concat("tokenURI should be ", ipAssetMediaUrl) + ); + + // + // Asset ID 2 (Org 1, ID 2) + // + + ipAssetMediaUrl = "https://arweave.net/story"; + ipAssetData = Registration.RegisterIPAssetParams({ + owner: ipAssetOwner2, + ipOrgAssetType: 1, + name: "Story IPA", + hash: 0x558b44f88e5959cec9c7836078a53ff4d6432142a9d5caa6f3a6eb7c83931166, + mediaUrl: ipAssetMediaUrl }); - bytes[] memory preHooksDataStory = new bytes[](2); - preHooksDataStory[0] = abi.encode(tokenGatedHookDataStory); - preHooksDataStory[1] = abi.encode(polygonTokenDataStory); + tokenGatedHookData = TokenGated.Params({ tokenOwner: ipAssetOwner2 }); + preHooksData = new bytes[](1); + preHooksData[0] = abi.encode(tokenGatedHookData); vm.prank(ipAssetOwner2); - spg.registerIPAsset( + (ipAssetId_2, ipOrg1_AssetId_2) = spg.registerIPAsset( ipOrg1, - registerIpAssetParamsStory, - preHooksDataStory, - new bytes[](0) + ipAssetData, + preHooksData, // pre-hook param data for hooks: TokenGated + new bytes[](0) // no data since there's no registration action hook in post-hook ); - bytes32 requestIdStory = keccak256( - abi.encodePacked(polygonTokenHook, uint256(1)) - ); - PolygonTokenHook(polygonTokenHook).handleCallback(requestIdStory, 1); - uint256 ipAssetId_2 = 2; - assertEq(registry.ipAssetOwner(ipAssetId_2), ipAssetOwner2); - assertEq(IIPOrg(ipOrg1).ownerOf(ipAssetId_2), ipAssetOwner2); - - Registration.RegisterIPAssetParams - memory registerIpAssetParamsOrg2 = Registration - .RegisterIPAssetParams({ - owner: ipAssetOwner3, - ipOrgAssetType: 1, - name: "Story IPA Org2", - hash: 0x558b44f88e5959cec9c7836078a53ff4d6432142a9d5caa6f3a6eb7c83933333, - mediaUrl: "https://arweave.net/story2" - }); + assertEq(ipAssetId_2, 2, "ipAssetId_2 should be 2"); + assertEq(ipOrg1_AssetId_2, 2, "ipOrg1_AssetId_2 should be 2"); + assertEq( + IPOrg(ipOrg1).ipAssetId(ipOrg1_AssetId_2), + ipAssetId_2, + "ipOrg1_AssetId_2 should be global ID 2" + ); + assertEq( + IPOrg(ipOrg1).tokenURI(ipOrg1_AssetId_2), + ipAssetMediaUrl, + string.concat("tokenURI should be ", ipAssetMediaUrl) + ); + + // Random mint in between + // moduleRegistry.registerProtocolModule( + // ModuleRegistryKeys.REGISTRATION_MODULE, + // BaseModule(address(this)) + // ); + // IIPOrg(ipOrg2).mint(ipAssetOwner3, 1); + + // + // Asset ID 3 (Org 2, ID 1) + // + + PolygonToken.Params memory polygonTokenHookParams = PolygonToken + .Params({ tokenOwnerAddress: ipAssetOwner3 }); + preHooksData = new bytes[](1); + preHooksData[0] = abi.encode(polygonTokenHookParams); + + ipAssetMediaUrl = "https://arweave.net/story2"; + ipAssetData = Registration.RegisterIPAssetParams({ + owner: ipAssetOwner3, + ipOrgAssetType: 1, + name: "Story IPA 2", + hash: 0x558b44f88e5959cec9c7836078a53ff4d6432142a9d5caa6f3a6eb7c83933377, + mediaUrl: ipAssetMediaUrl + }); vm.prank(ipAssetOwner3); - (uint256 ipAssetId_3, uint256 ipOrg2_AssetId_1) = spg.registerIPAsset( + // TODO: also check for event `AsyncHookExecuted` + vm.expectEmit(address(registrationModule)); + emit RequestPending(ipAssetOwner3); + // vm.expectEmit(address(polygonTokenHook)); + // emit PolygonTokenBalanceRequest( + // _getMockPolygonTokenHookReqId(), + // address(registrationModule), + // address(mockERC20), + // address(ipAssetOwner3), + // address(polygonTokenHook), + // polygonTokenHook.handleCallback.selector + // ); + spg.registerIPAsset( ipOrg2, - registerIpAssetParamsOrg2, - new bytes[](0), - new bytes[](0) + ipAssetData, + preHooksData, // pre-hook params: PolygonToken + new bytes[](0) // no post-hook ); - assertEq(ipAssetId_3, 3); - assertEq(ipOrg2_AssetId_1, 1); - // ip asset owner transfer IP Asset - // ip asset owner create relationship - LibRelationship.CreateRelationshipParams - memory crParams = LibRelationship.CreateRelationshipParams({ - relType: "APPEAR_IN", - srcAddress: ipOrg1, - srcId: 1, - dstAddress: ipOrg1, - dstId: 2 - }); - bytes[] memory preHooksDataRel = new bytes[](0); - bytes[] memory postHooksDataRel = new bytes[](0); - vm.prank(ipOrg1); - uint256 id = spg.createRelationship( - ipOrg1, - crParams, - preHooksDataRel, - postHooksDataRel + // Registering IPAsset with Async hook will not return the proper Global Asset ID & Org's Asset ID + // So we manually have to find the Global Asset ID & Org's Asset ID + ipAssetId_3 = 3; + ipOrg2_AssetId_1 = 1; + + // IPOrg2 has Polygon Token hook as pre-hook action for + // IPA Registration. So we mock the callback from Polygon Token hook. + _triggerMockPolygonTokenHook(ipAssetOwner3); + + assertEq(ipAssetId_3, 3, "ipAssetId_3 should be 3"); + assertEq(ipOrg2_AssetId_1, 1, "ipOrg2_AssetId_1 should be 1"); + assertEq( + IPOrg(ipOrg2).ipAssetId(ipOrg2_AssetId_1), + ipAssetId_3, + "ipOrg2_AssetId_1 should be global ID 3" ); - assertEq(id, 1); - LibRelationship.Relationship memory rel = relationshipModule - .getRelationship(1); - assertEq(rel.relType, "APPEAR_IN"); - assertEq(rel.srcAddress, ipOrg1); - assertEq(rel.dstAddress, ipOrg1); - assertEq(rel.srcId, 1); - assertEq(rel.dstId, 2); - - uint256 _parentLicenseId = 0; // no parent - Licensing.LicenseCreation memory creation = Licensing.LicenseCreation({ - params: new Licensing.ParamValue[](0), // IPOrg has set all the values - parentLicenseId: _parentLicenseId, - ipaId: ipAssetId_3 - }); - vm.prank(ipOrgOwner1); - uint256 licenseId = spg.createLicense( - address(ipOrg1), - creation, - new bytes[](0), - new bytes[](0) + assertEq( + IPOrg(ipOrg2).tokenURI(ipOrg2_AssetId_1), + ipAssetMediaUrl, + string.concat("tokenURI should be ", ipAssetMediaUrl) ); - Licensing.LicenseData memory license = licenseRegistry.getLicenseData(licenseId); - assertEq(uint8(license.status), uint8(Licensing.LicenseStatus.Active)); - assertEq(license.derivativesAllowed, false); - assertEq(license.isReciprocal, false); - assertEq(license.derivativeNeedsApproval, false); - assertEq(license.ipaId, ipAssetId_3); - assertEq(license.parentLicenseId, _parentLicenseId); + // + // Asset ID 4 (Org 2, ID 2) + // + + polygonTokenHookParams = PolygonToken.Params({ + tokenOwnerAddress: ipAssetOwner4 + }); + preHooksData = new bytes[](1); + preHooksData[0] = abi.encode(polygonTokenHookParams); + ipAssetMediaUrl = "https://arweave.net/music1"; + ipAssetData = Registration.RegisterIPAssetParams({ + owner: ipAssetOwner4, + ipOrgAssetType: 1, + name: "Music IPA", + hash: 0x558b44f88e5959cec9c7836078a53ff4d6432142a9d5caa6f3a6eb7c83933388, + mediaUrl: ipAssetMediaUrl + }); + vm.prank(ipAssetOwner4); + // TODO: also check for event `AsyncHookExecuted` vm.expectEmit(address(registrationModule)); - emit IPAssetTransferred( - 1, - address(ipOrg1), - 1, - ipAssetOwner1, - ipAssetOwner2 + emit RequestPending(ipAssetOwner4); + // vm.expectEmit(address(polygonTokenHook)); + // emit PolygonTokenBalanceRequest( + // _getMockPolygonTokenHookReqId(), + // address(registrationModule), + // address(mockERC20), + // address(ipAssetOwner4), + // address(polygonTokenHook), + // polygonTokenHook.handleCallback.selector + // ); + spg.registerIPAsset( + ipOrg2, + ipAssetData, + preHooksData, // pre-hook params: PolygonToken + new bytes[](0) // no post-hook ); - vm.prank(ipAssetOwner1); - spg.transferIPAsset( - ipOrg1, - ipAssetOwner1, - ipAssetOwner2, - 1, - // BaseModule_HooksParamsLengthMismatc - new bytes[](0), - new bytes[](0) + + // Registering IPAsset with Async hook will not return the proper Global Asset ID & Org's Asset ID + // So we manually have to find the Global Asset ID & Org's Asset ID + ipAssetId_4 = 4; + ipOrg2_AssetId_2 = 2; + + // IPOrg2 has Polygon Token hook as pre-hook action for + // IPA Registration. So we mock the callback from Polygon Token hook. + _triggerMockPolygonTokenHook(ipAssetOwner4); + + assertEq(ipAssetId_4, 4, "ipAssetId_4 should be 4"); + assertEq(ipOrg2_AssetId_2, 2, "ipOrg2_AssetId_2 should be 2"); + assertEq( + IPOrg(ipOrg2).ipAssetId(ipOrg2_AssetId_2), + ipAssetId_4, + "ipOrg2_AssetId_2 should be global ID 4" + ); + assertEq( + IPOrg(ipOrg2).tokenURI(ipOrg2_AssetId_2), + ipAssetMediaUrl, + string.concat("tokenURI should be ", ipAssetMediaUrl) ); - vm.prank(address(registrationModule)); - vm.expectEmit(address(registry)); - emit IPOrgTransferred(ipAssetId_2, ipOrg1, ipOrg2); - registry.transferIPOrg(ipAssetId_2, ipOrg2); - assertEq(registry.ipAssetOrg(ipAssetId_2), ipOrg2); + // + // Asset ID 5 (Org 3, ID 1) + // - vm.prank(address(0)); // TODO: modify when `onlyDisputer` is complete - emit StatusChanged(ipAssetId_2, 1, 0); // 0 means unset, 1 means set (change when status is converted to ENUM) - registry.setStatus(ipAssetId_2, 0); - assertEq(registry.status(ipAssetId_2), 0); + ipAssetMediaUrl = "https://arweave.net/music2"; + ipAssetData = Registration.RegisterIPAssetParams({ + owner: ipAssetOwner5, + ipOrgAssetType: 1, + name: "Music IPA 2", + hash: 0x558b44f88e5959cec9c7836078a53ff4d6432142a9d5caa6f3a6eb7c83933399, + mediaUrl: ipAssetMediaUrl + }); + vm.prank(ipAssetOwner5); + (ipAssetId_5, ipOrg3_AssetId_1) = spg.registerIPAsset( + ipOrg3, + ipAssetData, + new bytes[](0), // no pre-hook + new bytes[](0) // no post-hook + ); + assertEq(ipAssetId_5, 5, "ipAssetId_5 should be 5"); + assertEq(ipOrg3_AssetId_1, 1, "ipOrg3_AssetId_1 should be 1"); + assertEq( + IPOrg(ipOrg3).ipAssetId(ipOrg3_AssetId_1), + ipAssetId_5, + "ipOrg3_AssetId_1 should be global ID 5" + ); + assertEq( + IPOrg(ipOrg3).tokenURI(ipOrg3_AssetId_1), + ipAssetMediaUrl, + string.concat("tokenURI should be ", ipAssetMediaUrl) + ); + } + + function _addRelationshipType( + address ipOrg, + LibRelationship.Relatables src, + LibRelationship.Relatables dst, + uint8 maxSrc + ) internal { + address caller = IIPOrg(ipOrg).owner(); + uint8[] memory allowedSrcs = new uint8[](0); + uint8[] memory allowedDsts = new uint8[](0); + if (ipOrg == address(0)) { + caller = admin; + } else { + allowedSrcs = new uint8[](3); + for (uint8 i = 0; i < maxSrc; i++) { + allowedSrcs[i] = uint8(i); + } + allowedDsts = new uint8[](1); + allowedDsts[0] = 1; + } + LibRelationship.RelatedElements memory allowedElements = LibRelationship + .RelatedElements({ src: src, dst: dst }); + + LibRelationship.AddRelationshipTypeParams + memory params = LibRelationship.AddRelationshipTypeParams({ + relType: "TEST_RELATIONSHIP", + ipOrg: ipOrg, + allowedElements: allowedElements, + allowedSrcs: allowedSrcs, + allowedDsts: allowedDsts + }); + + // TODO test event + vm.prank(caller); + spg.addRelationshipType(params); + } + + function _triggerMockPolygonTokenHook(address caller_) internal { + bytes32 polygonHookReqId = _getMockPolygonTokenHookReqId(); + mockPolygonTokenHookNonce++; + + // vm.expectEmit(address(registrationModule)); + // emit RequestCompleted(address(caller_)); + // vm.expectEmit(address(polygonTokenHook)); + // emit AsyncHookCalledBack( + // address(polygonTokenHook), + // address(registrationModule), + // polygonHookReqId, + // abi.encode(mockERC20.balanceOf(address(caller_))) + // ); + polygonTokenHook.handleCallback( + polygonHookReqId, + mockERC20.balanceOf(address(caller_)) + ); + } + + function _getMockPolygonTokenHookReqId() internal returns (bytes32) { + return + keccak256( + abi.encodePacked( + address(polygonTokenHook), + uint256(mockPolygonTokenHookNonce) + ) + ); } } diff --git a/test/foundry/interfaces/IE2ETest.sol b/test/foundry/interfaces/IE2ETest.sol index 6184f801..6c0ad1d2 100644 --- a/test/foundry/interfaces/IE2ETest.sol +++ b/test/foundry/interfaces/IE2ETest.sol @@ -37,6 +37,14 @@ interface IE2ETest { address newOwner_ ); + // + // IModule + // + + event RequestPending(address indexed sender); + event RequestCompleted(address indexed sender); + event RequestFailed(address indexed sender, string reason); + // // Relationship Module // @@ -110,17 +118,6 @@ interface IE2ETest { bytes32 indexed registryKey ); - // - // SyncHook - // - - event SyncHookExecuted( - address indexed hookAddress, - HookResult indexed result, - bytes contextData, - bytes returnData - ); - // // IPAssetRegistry // @@ -144,4 +141,48 @@ interface IE2ETest { uint8 oldStatus_, uint8 newStatus_ ); + + // + // Sync Hooks + // + + event SyncHookExecuted( + address indexed hookAddress, + HookResult indexed result, + bytes contextData, + bytes returnData + ); + + // + // Async Hooks + // + + event AsyncHookExecuted( + address indexed hookAddress, + address indexed callbackHandler, + HookResult indexed result, + bytes32 requestId, + bytes contextData, + bytes returnData + ); + + event AsyncHookCalledBack( + address indexed hookAddress, + address indexed callbackHandler, + bytes32 requestId, + bytes callbackData + ); + + // + // Polygon Token Hook + // + + event PolygonTokenBalanceRequest( + bytes32 indexed requestId, + address indexed requester, + address tokenAddress, + address tokenOwnerAddress, + address callbackAddr, + bytes4 callbackFunctionSignature + ); } diff --git a/test/foundry/mocks/MockIPOrg.sol b/test/foundry/mocks/MockIPOrg.sol index 531c369e..d9ecf51d 100644 --- a/test/foundry/mocks/MockIPOrg.sol +++ b/test/foundry/mocks/MockIPOrg.sol @@ -33,4 +33,8 @@ contract MockIPOrg is IIPOrg { function ipOrgAssetType(uint256 id_) external pure override(IIPOrg) returns (uint8) { return 0; } + + function ipAssetId(uint256 id_) external returns (uint256) { + return 0; + } } diff --git a/test/foundry/modules/ModuleRegistry.t.sol b/test/foundry/modules/ModuleRegistry.t.sol index 32f07458..f8c19661 100644 --- a/test/foundry/modules/ModuleRegistry.t.sol +++ b/test/foundry/modules/ModuleRegistry.t.sol @@ -148,7 +148,7 @@ contract ModuleRegistryTest is Test, AccessControlHelper { IPOrgController(address(0x123)) ); MockBaseModule module = new MockBaseModule(admin, moduleConstruction); - + vm.expectEmit(address(registry)); emit ModuleAdded(address(0), string(abi.encodePacked(TEST_MODULE)), address(module)); vm.prank(admin); @@ -164,7 +164,7 @@ contract ModuleRegistryTest is Test, AccessControlHelper { assertEq(address(registry.protocolModule(TEST_MODULE)), address(0)); } - + function test_moduleRegistry_removeProtocolModule() public { BaseModule.ModuleConstruction memory moduleConstruction = BaseModule.ModuleConstruction( IPAssetRegistry(address(0x123)), diff --git a/test/foundry/modules/licensing/LicensingModule.Config.t.sol b/test/foundry/modules/licensing/LicensingModule.Config.t.sol index 9a8f8a2e..90dc8a5c 100644 --- a/test/foundry/modules/licensing/LicensingModule.Config.t.sol +++ b/test/foundry/modules/licensing/LicensingModule.Config.t.sol @@ -55,7 +55,7 @@ contract LicensingModuleConfigTest is BaseTest { config ); } - + function test_LicensingModule_configIpOrg_revert_InvalidLicensorConfig() public @@ -76,7 +76,7 @@ contract LicensingModuleConfigTest is BaseTest { function test_LicensingModule_configIpOrg_revert_paramLengthNotValid() public { Licensing.LicensingConfig memory config = Licensing.LicensingConfig({ frameworkId: "test_framework", - params: new Licensing.ParamValue[](100), + params: new Licensing.ParamValue[](1000), licensor: Licensing.LicensorConfig.IpOrgOwnerAlways }); vm.prank(ipOrg.owner()); @@ -132,7 +132,6 @@ contract LicensingModuleConfigTest is BaseTest { abi.encode(ssValue) ); } - function test_LicensingModule_configIpOrg_revert_ipOrgAlreadySet() public { test_LicensingModule_configIpOrg(); @@ -154,6 +153,4 @@ contract LicensingModuleConfigTest is BaseTest { config ); } - - } diff --git a/test/foundry/modules/licensing/LicensingModule.Licensing.t.sol b/test/foundry/modules/licensing/LicensingModule.Licensing.t.sol index 747a1a9c..0ed3230f 100644 --- a/test/foundry/modules/licensing/LicensingModule.Licensing.t.sol +++ b/test/foundry/modules/licensing/LicensingModule.Licensing.t.sol @@ -9,6 +9,8 @@ import { Licensing } from "contracts/lib/modules/Licensing.sol"; import { IPAsset } from "contracts/lib/IPAsset.sol"; import { BaseTest } from "test/foundry/utils/BaseTest.sol"; import { Errors } from "contracts/lib/Errors.sol"; +import { IIPOrg } from "contracts/interfaces/ip-org/IIPOrg.sol"; +import { ModuleRegistryKeys } from "contracts/lib/modules/ModuleRegistryKeys.sol"; import { SPUMLParams } from "contracts/lib/modules/SPUMLParams.sol"; import { BitMask } from "contracts/lib/BitMask.sol"; import { ShortString, ShortStrings } from "@openzeppelin/contracts/utils/ShortStrings.sol"; @@ -198,6 +200,19 @@ contract LicensingModuleLicensingTest is BaseTest { assertEq(parentParams[3].value, childParams[3].value, "derivatives with approval"); } + function test_LicensingModule_revert_performAction_InvalidAction() public { + vm.prank(address(spg)); // spg has AccessControl.MODULE_EXECUTOR_ROLE access + vm.expectRevert(Errors.LicensingModule_InvalidAction.selector); + moduleRegistry.execute( + IIPOrg(ipOrg), + address(this), + ModuleRegistryKeys.LICENSING_MODULE, + abi.encode("INVALID_ACTION", abi.encode(0, ipaId_1)), + new bytes[](0), + new bytes[](0) + ); + } + function _constructInputParams() internal pure diff --git a/test/foundry/modules/relationships/RelationshipModule.Setting.t.sol b/test/foundry/modules/relationships/RelationshipModule.Setting.t.sol index c763e881..01d830af 100644 --- a/test/foundry/modules/relationships/RelationshipModule.Setting.t.sol +++ b/test/foundry/modules/relationships/RelationshipModule.Setting.t.sol @@ -1,5 +1,6 @@ +/* solhint-disable contract-name-camelcase, func-name-mixedcase, var-name-mixedcase */ // SPDX-License-Identifier: BUSDL-1.1 -pragma solidity ^0.8.13; +pragma solidity ^0.8.19; import "forge-std/Test.sol"; import "test/foundry/utils/BaseTest.sol"; @@ -153,6 +154,41 @@ contract RelationshipModuleSettingTest is BaseTest { assertEq(relationshipModule.getRelationshipId(rel), 1); } + function test_RelationshipModule_createExternalNftToExternalNft() public { + _addRelType( + LibRelationship.PROTOCOL_LEVEL_RELATIONSHIP, + LibRelationship.Relatables.EXTERNAL_NFT, + LibRelationship.Relatables.EXTERNAL_NFT, + 0 + ); + + LibRelationship.CreateRelationshipParams memory params = LibRelationship + .CreateRelationshipParams({ + relType: "TEST_RELATIONSHIP", + srcAddress: address(1111111), + srcId: 0, + dstAddress: address(2222222), + dstId: 0 + }); + bytes[] memory preHooksData = new bytes[](0); + bytes[] memory postHooksData = new bytes[](0); + uint256 id = spg.createRelationship( + LibRelationship.PROTOCOL_LEVEL_RELATIONSHIP, + params, + preHooksData, + postHooksData + ); + assertEq(id, 1); + LibRelationship.Relationship memory rel = relationshipModule + .getRelationship(1); + assertEq(rel.relType, "TEST_RELATIONSHIP"); + assertEq(rel.srcAddress, address(1111111)); + assertEq(rel.dstAddress, address(2222222)); + assertEq(rel.srcId, 0); + assertEq(rel.dstId, 0); + assertEq(relationshipModule.getRelationshipId(rel), 1); + } + function _addRelType( address ipOrg, LibRelationship.Relatables src,