From 4d961e5f4dc4605013d4237d9705236fd5e2fba2 Mon Sep 17 00:00:00 2001 From: Spablob <99089658+Spablob@users.noreply.github.com> Date: Tue, 27 Aug 2024 07:35:57 +0100 Subject: [PATCH] Royalty cross-policy remix (#210) * remove gas limit from ci * pragma fix * main royalty contract and interface changes * remove constants from dispute interfaces * royalty unit tests * licensing related changes * fix integration and invariant tests * adjust errors.sol * fix remaining tests * add group related adjustment in royalty module * format fix --- .github/workflows/foundry_ci.yml | 2 +- contracts/IPAccountStorage.sol | 2 +- contracts/interfaces/IIPAccountStorage.sol | 2 +- .../interfaces/modules/base/IViewModule.sol | 2 +- .../modules/dispute/IDisputeModule.sol | 3 - .../dispute/policies/IArbitrationPolicy.sol | 15 +- .../modules/licensing/ILicenseTemplate.sol | 4 +- .../modules/metadata/ICoreMetadataModule.sol | 2 +- .../metadata/ICoreMetadataViewModule.sol | 2 +- .../modules/royalty/IRoyaltyModule.sol | 93 +- .../policies/IExternalRoyaltyPolicy.sol | 11 + .../royalty/policies/IIpRoyaltyVault.sol | 70 +- .../royalty/policies/IRoyaltyPolicy.sol | 21 +- .../royalty/policies/IRoyaltyPolicyLAP.sol | 62 -- .../royalty/policies/IVaultController.sol | 28 + .../policies/LAP/IRoyaltyPolicyLAP.sol | 58 ++ .../policies/LRP/IRoyaltyPolicyLRP.sol | 7 + .../registries/ILicenseRegistry.sol | 10 + contracts/lib/Errors.sol | 150 +++- contracts/lib/IPAccountStorageOps.sol | 2 +- .../modules/licensing/LicensingModule.sol | 60 +- .../modules/licensing/PILicenseTemplate.sol | 6 +- .../modules/metadata/CoreMetadataModule.sol | 2 +- .../metadata/CoreMetadataViewModule.sol | 2 +- contracts/modules/royalty/RoyaltyModule.sol | 374 ++++++-- .../royalty/policies/IpRoyaltyVault.sol | 265 +++--- .../royalty/policies/LAP/RoyaltyPolicyLAP.sol | 335 +++++++ .../royalty/policies/LRP/RoyaltyPolicyLRP.sol | 120 +++ .../royalty/policies/RoyaltyPolicyLAP.sol | 311 ------- .../royalty/policies/VaultController.sol | 71 ++ contracts/registries/GroupIPAssetRegistry.sol | 1 - contracts/registries/LicenseRegistry.sol | 27 +- script/foundry/utils/DeployHelper.sol | 54 +- .../foundry/integration/BaseIntegration.t.sol | 2 +- .../big-bang/SingleNftCollection.t.sol | 8 +- .../licensing/LicensingIntegration.t.sol | 4 +- .../flows/licensing/LicensingScenarios.t.sol | 4 +- .../integration/flows/royalty/Royalty.t.sol | 95 +- test/foundry/invariants/DisputeModule.t.sol | 6 +- test/foundry/invariants/IpRoyaltyVault.t.sol | 179 ++-- .../mocks/module/MockLicenseTemplate.sol | 6 +- .../policy/MockExternalRoyaltyPolicy1.sol | 11 + .../policy/MockExternalRoyaltyPolicy2.sol | 11 + .../mocks/policy/MockRoyaltyPolicyLAP.sol | 53 +- .../modules/dispute/DisputeModule.t.sol | 2 +- .../modules/licensing/LicensingModule.t.sol | 8 +- .../modules/licensing/PILicenseTemplate.t.sol | 16 +- .../modules/royalty/IpRoyaltyVault.t.sol | 671 +++++++------- .../royalty/LAP/RoyaltyPolicyLAP.t.sol | 490 +++++++++++ .../royalty/LRP/RoyaltyPolicyLRP.t.sol | 152 ++++ .../modules/royalty/RoyaltyModule.t.sol | 822 +++++++++++++----- .../modules/royalty/RoyaltyPolicyLAP.t.sol | 311 ------- .../modules/royalty/VaultController.t.sol | 50 ++ test/foundry/upgrades/Upgrades.t.sol | 36 +- test/foundry/utils/LicensingHelper.t.sol | 2 +- 55 files changed, 3206 insertions(+), 1907 deletions(-) create mode 100644 contracts/interfaces/modules/royalty/policies/IExternalRoyaltyPolicy.sol delete mode 100644 contracts/interfaces/modules/royalty/policies/IRoyaltyPolicyLAP.sol create mode 100644 contracts/interfaces/modules/royalty/policies/IVaultController.sol create mode 100644 contracts/interfaces/modules/royalty/policies/LAP/IRoyaltyPolicyLAP.sol create mode 100644 contracts/interfaces/modules/royalty/policies/LRP/IRoyaltyPolicyLRP.sol create mode 100644 contracts/modules/royalty/policies/LAP/RoyaltyPolicyLAP.sol create mode 100644 contracts/modules/royalty/policies/LRP/RoyaltyPolicyLRP.sol delete mode 100644 contracts/modules/royalty/policies/RoyaltyPolicyLAP.sol create mode 100644 contracts/modules/royalty/policies/VaultController.sol create mode 100644 test/foundry/mocks/policy/MockExternalRoyaltyPolicy1.sol create mode 100644 test/foundry/mocks/policy/MockExternalRoyaltyPolicy2.sol create mode 100644 test/foundry/modules/royalty/LAP/RoyaltyPolicyLAP.t.sol create mode 100644 test/foundry/modules/royalty/LRP/RoyaltyPolicyLRP.t.sol delete mode 100644 test/foundry/modules/royalty/RoyaltyPolicyLAP.t.sol create mode 100644 test/foundry/modules/royalty/VaultController.t.sol diff --git a/.github/workflows/foundry_ci.yml b/.github/workflows/foundry_ci.yml index 8fbf9932..15e779ff 100644 --- a/.github/workflows/foundry_ci.yml +++ b/.github/workflows/foundry_ci.yml @@ -47,7 +47,7 @@ jobs: - name: Run Forge tests run: | - forge test -vvv --gas-limit 800000000000 + forge test -vvv id: forge-test - name: Run solhint diff --git a/contracts/IPAccountStorage.sol b/contracts/IPAccountStorage.sol index 52e9133f..c42093b9 100644 --- a/contracts/IPAccountStorage.sol +++ b/contracts/IPAccountStorage.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.23; +pragma solidity 0.8.23; import { IIPAccountStorage } from "./interfaces/IIPAccountStorage.sol"; import { IModuleRegistry } from "./interfaces/registries/IModuleRegistry.sol"; diff --git a/contracts/interfaces/IIPAccountStorage.sol b/contracts/interfaces/IIPAccountStorage.sol index 7df06f2a..0569c366 100644 --- a/contracts/interfaces/IIPAccountStorage.sol +++ b/contracts/interfaces/IIPAccountStorage.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: UNLICENSED // See https://github.com/storyprotocol/protocol-contracts/blob/main/StoryProtocol-AlphaTestingAgreement-17942166.3.pdf -pragma solidity ^0.8.23; +pragma solidity 0.8.23; import { IERC165 } from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; diff --git a/contracts/interfaces/modules/base/IViewModule.sol b/contracts/interfaces/modules/base/IViewModule.sol index 5a4506f4..47fe7971 100644 --- a/contracts/interfaces/modules/base/IViewModule.sol +++ b/contracts/interfaces/modules/base/IViewModule.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: UNLICENSED // See https://github.com/storyprotocol/protocol-contracts/blob/main/StoryProtocol-AlphaTestingAgreement-17942166.3.pdf -pragma solidity ^0.8.23; +pragma solidity 0.8.23; import { IModule } from "./IModule.sol"; diff --git a/contracts/interfaces/modules/dispute/IDisputeModule.sol b/contracts/interfaces/modules/dispute/IDisputeModule.sol index 3d4bab0c..a9049fb5 100644 --- a/contracts/interfaces/modules/dispute/IDisputeModule.sol +++ b/contracts/interfaces/modules/dispute/IDisputeModule.sol @@ -91,9 +91,6 @@ interface IDisputeModule { /// @param disputeId The dispute id event DisputeResolved(uint256 disputeId); - /// @notice Tag to represent the dispute is in dispute state waiting for judgement - function IN_DISPUTE() external view returns (bytes32); - /// @notice Dispute ID counter function disputeCounter() external view returns (uint256); diff --git a/contracts/interfaces/modules/dispute/policies/IArbitrationPolicy.sol b/contracts/interfaces/modules/dispute/policies/IArbitrationPolicy.sol index 377a9eb4..2aea2d99 100644 --- a/contracts/interfaces/modules/dispute/policies/IArbitrationPolicy.sol +++ b/contracts/interfaces/modules/dispute/policies/IArbitrationPolicy.sol @@ -3,18 +3,6 @@ pragma solidity 0.8.23; /// @title Arbitration Policy Interface interface IArbitrationPolicy { - /// @notice Returns the protocol-wide dispute module address - function DISPUTE_MODULE() external view returns (address); - - /// @notice Returns the payment token address - function PAYMENT_TOKEN() external view returns (address); - - /// @notice Returns the arbitration price - function ARBITRATION_PRICE() external view returns (uint256); - - /// @notice Returns the treasury address - function treasury() external view returns (address); - /// @notice Allows governance set the treasury address /// @dev Enforced to be only callable by the governance protocol admin /// @param newTreasury The new address of the treasury @@ -46,4 +34,7 @@ interface IArbitrationPolicy { /// @param disputeId The dispute id /// @param data The arbitrary data used to resolve the dispute function onResolveDispute(address caller, uint256 disputeId, bytes calldata data) external; + + /// @notice Returns the treasury address + function treasury() external view returns (address); } diff --git a/contracts/interfaces/modules/licensing/ILicenseTemplate.sol b/contracts/interfaces/modules/licensing/ILicenseTemplate.sol index e6bf638f..738e1f93 100644 --- a/contracts/interfaces/modules/licensing/ILicenseTemplate.sol +++ b/contracts/interfaces/modules/licensing/ILicenseTemplate.sol @@ -71,7 +71,7 @@ interface ILicenseTemplate is IERC165 { /// the license term does set RoyaltyPolicy. /// @param licenseTermsId The ID of the license terms. /// @return royaltyPolicy The address of the royalty policy specified for the license terms. - /// @return royaltyData The data of the royalty policy. + /// @return royaltyPercent The percentage of the royalty. /// @return mintingLicenseFee The fee for minting a license. /// @return currencyToken The address of the ERC20 token, used for minting license fee and royalties. /// the currency token will used for pay for license token minting fee and royalties. @@ -80,7 +80,7 @@ interface ILicenseTemplate is IERC165 { ) external view - returns (address royaltyPolicy, bytes memory royaltyData, uint256 mintingLicenseFee, address currencyToken); + returns (address royaltyPolicy, uint32 royaltyPercent, uint256 mintingLicenseFee, address currencyToken); /// @notice Verifies the minting of a license token. /// @dev the function will be called by the LicensingModule when minting a license token to diff --git a/contracts/interfaces/modules/metadata/ICoreMetadataModule.sol b/contracts/interfaces/modules/metadata/ICoreMetadataModule.sol index 3a8e5057..d5840e4d 100644 --- a/contracts/interfaces/modules/metadata/ICoreMetadataModule.sol +++ b/contracts/interfaces/modules/metadata/ICoreMetadataModule.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.23; +pragma solidity 0.8.23; import { IModule } from "../../../../contracts/interfaces/modules/base/IModule.sol"; diff --git a/contracts/interfaces/modules/metadata/ICoreMetadataViewModule.sol b/contracts/interfaces/modules/metadata/ICoreMetadataViewModule.sol index c2f872e4..8e9290c1 100644 --- a/contracts/interfaces/modules/metadata/ICoreMetadataViewModule.sol +++ b/contracts/interfaces/modules/metadata/ICoreMetadataViewModule.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.23; +pragma solidity 0.8.23; import { IViewModule } from "../base/IViewModule.sol"; diff --git a/contracts/interfaces/modules/royalty/IRoyaltyModule.sol b/contracts/interfaces/modules/royalty/IRoyaltyModule.sol index f0bf90c1..6a5f544d 100644 --- a/contracts/interfaces/modules/royalty/IRoyaltyModule.sol +++ b/contracts/interfaces/modules/royalty/IRoyaltyModule.sol @@ -30,20 +30,26 @@ interface IRoyaltyModule is IModule { /// @param amount The amount paid event LicenseMintingFeePaid(address receiverIpId, address payerAddress, address token, uint256 amount); - /// @notice Indicates if a royalty policy is whitelisted - /// @param royaltyPolicy The address of the royalty policy - /// @return isWhitelisted True if the royalty policy is whitelisted - function isWhitelistedRoyaltyPolicy(address royaltyPolicy) external view returns (bool); + /// @notice Event emitted when a royalty policy is registered + /// @param externalRoyaltyPolicy The address of the external royalty policy + event ExternalRoyaltyPolicyRegistered(address externalRoyaltyPolicy); - /// @notice Indicates if a royalty token is whitelisted - /// @param token The address of the royalty token - /// @return isWhitelisted True if the royalty token is whitelisted - function isWhitelistedRoyaltyToken(address token) external view returns (bool); + /// @notice Event emitted when the IP graph limits are updated + /// @param maxParents The maximum number of parents an IP asset can have + /// @param maxAncestors The maximum number of ancestors an IP asset can have + /// @param accumulatedRoyaltyPoliciesLimit The maximum number of accumulated royalty policies an IP asset can have + event IpGraphLimitsUpdated(uint256 maxParents, uint256 maxAncestors, uint256 accumulatedRoyaltyPoliciesLimit); - /// @notice Indicates the royalty policy for a given IP asset - /// @param ipId The ID of IP asset - /// @return royaltyPolicy The address of the royalty policy - function royaltyPolicies(address ipId) external view returns (address); + /// @notice Sets the ip graph limits + /// @dev Enforced to be only callable by the protocol admin + /// @param parentLimit The maximum number of parents an IP asset can have + /// @param ancestorLimit The maximum number of ancestors an IP asset can have + /// @param accumulatedRoyaltyPoliciesLimit The maximum number of accumulated royalty policies an IP asset can have + function setIpGraphLimits( + uint256 parentLimit, + uint256 ancestorLimit, + uint256 accumulatedRoyaltyPoliciesLimit + ) external; /// @notice Whitelist a royalty policy /// @dev Enforced to be only callable by the protocol admin @@ -57,37 +63,40 @@ interface IRoyaltyModule is IModule { /// @param allowed Indicates if the token is whitelisted or not function whitelistRoyaltyToken(address token, bool allowed) external; + /// @notice Registers an external royalty policy + /// @param externalRoyaltyPolicy The address of the external royalty policy + function registerExternalRoyaltyPolicy(address externalRoyaltyPolicy) external; + /// @notice Executes royalty related logic on license minting /// @dev Enforced to be only callable by LicensingModule /// @param ipId The ipId whose license is being minted (licensor) /// @param royaltyPolicy The royalty policy address of the license being minted - /// @param licenseData The license data custom to each the royalty policy + /// @param licensePercent The license percentage of the license being minted /// @param externalData The external data custom to each the royalty policy function onLicenseMinting( address ipId, address royaltyPolicy, - bytes calldata licenseData, + uint32 licensePercent, bytes calldata externalData ) external; /// @notice Executes royalty related logic on linking to parents /// @dev Enforced to be only callable by LicensingModule /// @param ipId The children ipId that is being linked to parents - /// @param royaltyPolicy The common royalty policy address of all the licenses being burned /// @param parentIpIds The parent ipIds that the children ipId is being linked to - /// @param licenseData The license data custom to each the royalty policy + /// @param licensesPercent The license percentage of the licenses being minted /// @param externalData The external data custom to each the royalty policy function onLinkToParents( address ipId, - address royaltyPolicy, address[] calldata parentIpIds, - bytes[] memory licenseData, + address[] calldata licenseRoyaltyPolicies, + uint32[] calldata licensesPercent, bytes calldata externalData ) external; /// @notice Allows the function caller to pay royalties to the receiver IP asset on behalf of the payer IP asset. - /// @param receiverIpId The ID of the IP asset that receives the royalties - /// @param payerIpId The ID of the IP asset that pays the royalties + /// @param receiverIpId The ipId that receives the royalties + /// @param payerIpId The ipId that pays the royalties /// @param token The token to use to pay the royalties /// @param amount The amount to pay function payRoyaltyOnBehalf(address receiverIpId, address payerIpId, address token, uint256 amount) external; @@ -95,14 +104,42 @@ interface IRoyaltyModule is IModule { /// @notice Allows to pay the minting fee for a license /// @param receiverIpId The ipId that receives the royalties /// @param payerAddress The address that pays the royalties - /// @param licenseRoyaltyPolicy The royalty policy of the license being minted /// @param token The token to use to pay the royalties /// @param amount The amount to pay - function payLicenseMintingFee( - address receiverIpId, - address payerAddress, - address licenseRoyaltyPolicy, - address token, - uint256 amount - ) external; + function payLicenseMintingFee(address receiverIpId, address payerAddress, address token, uint256 amount) external; + + /// @notice Returns the total number of royalty tokens + function totalRtSupply() external pure returns (uint32); + + /// @notice Indicates if a royalty policy is whitelisted + /// @param royaltyPolicy The address of the royalty policy + /// @return isWhitelisted True if the royalty policy is whitelisted + function isWhitelistedRoyaltyPolicy(address royaltyPolicy) external view returns (bool); + + /// @notice Indicates if an external royalty policy is registered + /// @param externalRoyaltyPolicy The address of the external royalty policy + /// @return isRegistered True if the external royalty policy is registered + function isRegisteredExternalRoyaltyPolicy(address externalRoyaltyPolicy) external view returns (bool); + + /// @notice Indicates if a royalty token is whitelisted + /// @param token The address of the royalty token + /// @return isWhitelisted True if the royalty token is whitelisted + function isWhitelistedRoyaltyToken(address token) external view returns (bool); + + /// @notice Returns the maximum number of parents + function maxParents() external view returns (uint256); + + /// @notice Returns the maximum number of total ancestors + function maxAncestors() external view returns (uint256); + + /// @notice Returns the maximum number of accumulated royalty policies an IP asset can have + function maxAccumulatedRoyaltyPolicies() external view returns (uint256); + + /// @notice Indicates the royalty vault for a given IP asset + /// @param ipId The ID of IP asset + function ipRoyaltyVaults(address ipId) external view returns (address); + + /// @notice Returns the accumulated royalty policies for a given IP asset + /// @param ipId The ID of IP asset + function accumulatedRoyaltyPolicies(address ipId) external view returns (address[] memory); } diff --git a/contracts/interfaces/modules/royalty/policies/IExternalRoyaltyPolicy.sol b/contracts/interfaces/modules/royalty/policies/IExternalRoyaltyPolicy.sol new file mode 100644 index 00000000..c6f0c0f7 --- /dev/null +++ b/contracts/interfaces/modules/royalty/policies/IExternalRoyaltyPolicy.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.23; + +/// @title IExternalRoyaltyPolicy interface +interface IExternalRoyaltyPolicy { + /// @notice Returns the amount of royalty tokens required to link a child to a given IP asset + /// @param ipId The ipId of the IP asset + /// @param licensePercent The percentage of the license + /// @return The amount of royalty tokens required to link a child to a given IP asset + function rtsRequiredToLink(address ipId, uint32 licensePercent) external view returns (uint32); +} diff --git a/contracts/interfaces/modules/royalty/policies/IIpRoyaltyVault.sol b/contracts/interfaces/modules/royalty/policies/IIpRoyaltyVault.sol index 64de5082..14caf2ef 100644 --- a/contracts/interfaces/modules/royalty/policies/IIpRoyaltyVault.sol +++ b/contracts/interfaces/modules/royalty/policies/IIpRoyaltyVault.sol @@ -1,18 +1,17 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; -/// @title Ip royalty vault interface +/// @title IpRoyaltyVault interface interface IIpRoyaltyVault { - /// @notice Event emitted when royalty tokens are collected - /// @param ancestorIpId The ancestor ipId address - /// @param royaltyTokensCollected The amount of royalty tokens collected - event RoyaltyTokensCollected(address ancestorIpId, uint256 royaltyTokensCollected); + /// @notice Event emitted when a revenue token is added to a vault + /// @param token The address of the revenue token + /// @param vault The address of the vault + event RevenueTokenAddedToVault(address token, address vault); /// @notice Event emitted when a snapshot is taken /// @param snapshotId The snapshot id /// @param snapshotTimestamp The timestamp of the snapshot - /// @param unclaimedTokens The amount of unclaimed tokens at the snapshot - event SnapshotCompleted(uint256 snapshotId, uint256 snapshotTimestamp, uint32 unclaimedTokens); + event SnapshotCompleted(uint256 snapshotId, uint256 snapshotTimestamp); /// @notice Event emitted when a revenue token is claimed /// @param claimer The address of the claimer @@ -20,25 +19,24 @@ interface IIpRoyaltyVault { /// @param amount The amount of revenue token claimed event RevenueTokenClaimed(address claimer, address token, uint256 amount); - /// @notice initializer for this implementation contract + /// @notice Initializer for this implementation contract /// @param name The name of the royalty token /// @param symbol The symbol of the royalty token /// @param supply The total supply of the royalty token - /// @param unclaimedTokens The amount of unclaimed royalty tokens reserved for ancestors /// @param ipIdAddress The ip id the royalty vault belongs to + /// @param rtReceiver The address of the royalty token receiver function initialize( string memory name, string memory symbol, uint32 supply, - uint32 unclaimedTokens, - address ipIdAddress + address ipIdAddress, + address rtReceiver ) external; /// @notice Adds a new revenue token to the vault /// @param token The address of the revenue token - /// @dev Only callable by the royalty policy LAP - /// @return Whether the token is added - function addIpRoyaltyVaultTokens(address token) external returns (bool); + /// @dev Only callable by the royalty module or whitelisted royalty policy + function addIpRoyaltyVaultTokens(address token) external; /// @notice A function to snapshot the claimable revenue and royalty token amounts /// @return The snapshot id @@ -59,40 +57,30 @@ interface IIpRoyaltyVault { /// @notice Allows token holders to claim by a list of snapshot ids based on the token balance at certain snapshot /// @param snapshotIds The list of snapshot ids /// @param token The revenue token to claim - function claimRevenueBySnapshotBatch(uint256[] memory snapshotIds, address token) external; + /// @return The amount of revenue tokens claimed + function claimRevenueBySnapshotBatch(uint256[] memory snapshotIds, address token) external returns (uint256); - /// @notice Allows ancestors to claim the royalty tokens and any accrued revenue tokens - /// @param ancestorIpId The ip id of the ancestor to whom the royalty tokens belong to - function collectRoyaltyTokens(address ancestorIpId) external; + /// @notice Allows to claim revenue tokens on behalf of the ip royalty vault + /// @param snapshotId The snapshot id + /// @param tokenList The list of revenue tokens to claim + /// @param targetIpId The target ip id to claim revenue tokens from + function claimByTokenBatchAsSelf(uint256 snapshotId, address[] calldata tokenList, address targetIpId) external; - /// @notice Collect the accrued tokens (if any) - /// @param ancestorIpId The ip id of the ancestor to whom the royalty tokens belong to - /// @param tokens The list of revenue tokens to claim - function collectAccruedTokens(address ancestorIpId, address[] calldata tokens) external; + /// @notice Allows to claim revenue tokens on behalf of the ip royalty vault by snapshot batch + /// @param snapshotIds The list of snapshot ids + /// @param token The revenue token to claim + /// @param targetIpId The target ip id to claim revenue tokens from + function claimBySnapshotBatchAsSelf(uint256[] memory snapshotIds, address token, address targetIpId) external; + + /// @notice Returns the current snapshot id + function getCurrentSnapshotId() external view returns (uint256); /// @notice The ip id to whom this royalty vault belongs to - /// @return The ip id address function ipId() external view returns (address); - /// @notice The amount of unclaimed royalty tokens - function unclaimedRoyaltyTokens() external view returns (uint32); - /// @notice The last snapshotted timestamp function lastSnapshotTimestamp() external view returns (uint256); - /// @notice The amount of revenue token in the ancestors vault - /// @param token The address of the revenue token - function ancestorsVaultAmount(address token) external view returns (uint256); - - /// @notice The amount of revenue tokens that can be collected by the ancestor - /// @param ancestorIpId The ancestor ipId address - /// @param token The address of the revenue token - function collectableAmount(address ancestorIpId, address token) external view returns (uint256); - - /// @notice Indicates whether the ancestor has collected the royalty tokens - /// @param ancestorIpId The ancestor ipId address - function isCollectedByAncestor(address ancestorIpId) external view returns (bool); - /// @notice Amount of revenue token in the claim vault /// @param token The address of the revenue token function claimVaultAmount(address token) external view returns (uint256); @@ -102,10 +90,6 @@ interface IIpRoyaltyVault { /// @param token The address of the revenue token function claimableAtSnapshot(uint256 snapshotId, address token) external view returns (uint256); - /// @notice Amount of unclaimed revenue tokens at the snapshot - /// @param snapshotId The snapshot id - function unclaimedAtSnapshot(uint256 snapshotId) external view returns (uint32); - /// @notice Indicates whether the claimer has claimed the revenue tokens at a given snapshot /// @param snapshotId The snapshot id /// @param claimer The address of the claimer diff --git a/contracts/interfaces/modules/royalty/policies/IRoyaltyPolicy.sol b/contracts/interfaces/modules/royalty/policies/IRoyaltyPolicy.sol index 74eb6c2c..47db3d42 100644 --- a/contracts/interfaces/modules/royalty/policies/IRoyaltyPolicy.sol +++ b/contracts/interfaces/modules/royalty/policies/IRoyaltyPolicy.sol @@ -6,27 +6,28 @@ interface IRoyaltyPolicy { /// @notice Executes royalty related logic on minting a license /// @dev Enforced to be only callable by RoyaltyModule /// @param ipId The ipId whose license is being minted (licensor) - /// @param licenseData The license data custom to each the royalty policy + /// @param licensePercent The license percentage of the license being minted /// @param externalData The external data custom to each the royalty policy - function onLicenseMinting(address ipId, bytes calldata licenseData, bytes calldata externalData) external; + function onLicenseMinting(address ipId, uint32 licensePercent, bytes calldata externalData) external; /// @notice Executes royalty related logic on linking to parents /// @dev Enforced to be only callable by RoyaltyModule /// @param ipId The children ipId that is being linked to parents /// @param parentIpIds The parent ipIds that the children ipId is being linked to - /// @param licenseData The license data custom to each the royalty policy + /// @param licenseRoyaltyPolicies The royalty policies of the license + /// @param licensesPercent The license percentages of the licenses being minted /// @param externalData The external data custom to each the royalty policy function onLinkToParents( address ipId, address[] calldata parentIpIds, - bytes[] memory licenseData, + address[] calldata licenseRoyaltyPolicies, + uint32[] calldata licensesPercent, bytes calldata externalData ) external; - /// @notice Allows the caller to pay royalties to the given IP asset - /// @param caller The caller is the address from which funds will transferred from - /// @param ipId The ipId of the receiver of the royalties - /// @param token The token to pay - /// @param amount The amount to pay - function onRoyaltyPayment(address caller, address ipId, address token, uint256 amount) external; + /// @notice Returns the amount of royalty tokens required to link a child to a given IP asset + /// @param ipId The ipId of the IP asset + /// @param licensePercent The percentage of the license + /// @return The amount of royalty tokens required to link a child to a given IP asset + function rtsRequiredToLink(address ipId, uint32 licensePercent) external view returns (uint32); } diff --git a/contracts/interfaces/modules/royalty/policies/IRoyaltyPolicyLAP.sol b/contracts/interfaces/modules/royalty/policies/IRoyaltyPolicyLAP.sol deleted file mode 100644 index c93efa00..00000000 --- a/contracts/interfaces/modules/royalty/policies/IRoyaltyPolicyLAP.sol +++ /dev/null @@ -1,62 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.23; - -import { IRoyaltyPolicy } from "../../../../interfaces/modules/royalty/policies/IRoyaltyPolicy.sol"; - -/// @title RoyaltyPolicy interface -interface IRoyaltyPolicyLAP is IRoyaltyPolicy { - /// @notice Event emitted when a policy is initialized - /// @param ipId The ID of the IP asset that the policy is being initialized for - /// @param ipRoyaltyVault The ip royalty vault address - /// @param royaltyStack The royalty stack - event PolicyInitialized(address ipId, address ipRoyaltyVault, uint32 royaltyStack); - - /// @notice Event emitted when a revenue token is added to a vault - /// @param token The address of the revenue token - /// @param vault The address of the vault - event RevenueTokenAddedToVault(address token, address vault); - - /// @notice Event emitted when the snapshot interval is set - /// @param interval The snapshot interval - event SnapshotIntervalSet(uint256 interval); - - /// @notice Event emitted when the ip royalty vault beacon is set - /// @param beacon The address of the ip royalty vault beacon - event IpRoyaltyVaultBeaconSet(address beacon); - - /// @notice The state data of the LAP royalty policy - /// @param isUnlinkableToParents Indicates if the ipId is unlinkable to new parents - /// @param ipRoyaltyVault The ip royalty vault address - /// @param royaltyStack The royalty stack of a given ipId is the sum of the royalties to be paid to each ancestors - struct LAPRoyaltyData { - bool isUnlinkableToParents; - address ipRoyaltyVault; - uint32 royaltyStack; - } - - /// @notice Returns the percentage scale - represents 100% of royalty tokens for an ip - function TOTAL_RT_SUPPLY() external view returns (uint32); - - /// @notice Returns the maximum number of parents - function MAX_PARENTS() external view returns (uint256); - - /// @notice Returns the maximum number of total ancestors. - /// @dev The IP derivative tree is limited to 14 ancestors, which represents 3 levels of a binary tree 14 = 2+4+8 - function MAX_ANCESTORS() external view returns (uint256); - - /// @notice Returns the RoyaltyModule address - function ROYALTY_MODULE() external view returns (address); - - /// @notice Returns the LicensingModule address - function LICENSING_MODULE() external view returns (address); - - /// @notice Returns the snapshot interval - function getSnapshotInterval() external view returns (uint256); - - /// @notice Returns the royalty data for a given IP asset - /// @param ipId The ID of the IP asset - /// @return isUnlinkable Indicates if the ipId is unlinkable to new parents - /// @return ipRoyaltyVault The ip royalty vault address - /// @return royaltyStack The royalty stack of a given ipId is the sum of the royalties to be paid to each ancestors - function getRoyaltyData(address ipId) external view returns (bool, address, uint32); -} diff --git a/contracts/interfaces/modules/royalty/policies/IVaultController.sol b/contracts/interfaces/modules/royalty/policies/IVaultController.sol new file mode 100644 index 00000000..fdfcd99e --- /dev/null +++ b/contracts/interfaces/modules/royalty/policies/IVaultController.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +/// @title VaultController interface +interface IVaultController { + /// @dev Set the snapshot interval + /// @dev Enforced to be only callable by the protocol admin in governance + /// @param timestampInterval The minimum timestamp interval between snapshots + function setSnapshotInterval(uint256 timestampInterval) external; + + /// @dev Set the ip royalty vault beacon + /// @dev Enforced to be only callable by the protocol admin in governance + /// @param beacon The ip royalty vault beacon address + function setIpRoyaltyVaultBeacon(address beacon) external; + + /// @dev Upgrades the ip royalty vault beacon + /// @dev Enforced to be only callable by the upgrader admin + /// @param newVault The new ip royalty vault beacon address + function upgradeVaults(address newVault) external; + + /// @notice Returns the snapshot interval + /// @return snapshotInterval The minimum time interval between snapshots + function snapshotInterval() external view returns (uint256); + + /// @notice Returns the ip royalty vault beacon + /// @return ipRoyaltyVaultBeacon The ip royalty vault beacon address + function ipRoyaltyVaultBeacon() external view returns (address); +} diff --git a/contracts/interfaces/modules/royalty/policies/LAP/IRoyaltyPolicyLAP.sol b/contracts/interfaces/modules/royalty/policies/LAP/IRoyaltyPolicyLAP.sol new file mode 100644 index 00000000..a73a9fe0 --- /dev/null +++ b/contracts/interfaces/modules/royalty/policies/LAP/IRoyaltyPolicyLAP.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.23; + +import { IRoyaltyPolicy } from "../../../../../interfaces/modules/royalty/policies/IRoyaltyPolicy.sol"; + +/// @title RoyaltyPolicyLAP interface +interface IRoyaltyPolicyLAP is IRoyaltyPolicy { + /// @notice Event emitted when a royalty tokens are collected + /// @param ipId The ID of the IP asset that the royalty tokens are being collected from + /// @param ancestorIpId The ID of the ancestor that the royalty tokens are being collected for + /// @param amount The amount of royalty tokens being collected + event RoyaltyTokensCollected(address ipId, address ancestorIpId, uint256 amount); + + /// @notice Collects royalty tokens to an ancestor's ip royalty vault + /// @param ipId The ID of the IP asset + /// @param ancestorIpId The ID of the ancestor IP asset + function collectRoyaltyTokens(address ipId, address ancestorIpId) external; + + /// @notice Allows claiming revenue tokens of behalf of royalty LAP royalty policy contract + /// @param snapshotIds The snapshot IDs to claim revenue tokens for + /// @param token The token to claim revenue tokens for + /// @param targetIpId The target IP ID to claim revenue tokens for + function claimBySnapshotBatchAsSelf(uint256[] memory snapshotIds, address token, address targetIpId) external; + + /// @notice Returns the royalty data for a given IP asset + /// @param ipId The ID of the IP asset + /// @return royaltyStack The royalty stack of a given ipId is the sum of the royalties to be paid to each ancestors + function royaltyStack(address ipId) external view returns (uint32); + + /// @notice Returns the unclaimed royalty tokens for a given IP asset + /// @param ipId The ipId to get the unclaimed royalty tokens for + /// @return unclaimedRoyaltyTokens The unclaimed royalty tokens for a given ipId + function unclaimedRoyaltyTokens(address ipId) external view returns (uint32); + + /// @notice Returns if the royalty tokens have been collected by an ancestor for a given IP asset + /// @param ipId The ipId to check if the royalty tokens have been collected by an ancestor + /// @param ancestorIpId The ancestor ipId to check if the royalty tokens have been collected + /// @return isCollectedByAncestor True if the royalty tokens have been collected by an ancestor + function isCollectedByAncestor(address ipId, address ancestorIpId) external view returns (bool); + + /// @notice Returns the revenue token balances for a given IP asset + /// @param ipId The ipId to get the revenue token balances for + /// @param token The token to get the revenue token balances for + function revenueTokenBalances(address ipId, address token) external view returns (uint256); + + /// @notice Returns whether a snapshot has been claimed for a given IP asset and token + /// @param ipId The ipId to check if the snapshot has been claimed for + /// @param token The token to check if the snapshot has been claimed for + /// @param snapshot The snapshot to check if it has been claimed + /// @return True if the snapshot has been claimed + function snapshotsClaimed(address ipId, address token, uint256 snapshot) external view returns (bool); + + /// @notice Returns the number of snapshots claimed for a given IP asset and token + /// @param ipId The ipId to check if the snapshot has been claimed for + /// @param token The token to check if the snapshot has been claimed for + /// @return The number of snapshots claimed + function snapshotsClaimedCounter(address ipId, address token) external view returns (uint256); +} diff --git a/contracts/interfaces/modules/royalty/policies/LRP/IRoyaltyPolicyLRP.sol b/contracts/interfaces/modules/royalty/policies/LRP/IRoyaltyPolicyLRP.sol new file mode 100644 index 00000000..1ada5e8b --- /dev/null +++ b/contracts/interfaces/modules/royalty/policies/LRP/IRoyaltyPolicyLRP.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.23; + +import { IRoyaltyPolicy } from "../../../../../interfaces/modules/royalty/policies/IRoyaltyPolicy.sol"; + +/// @title IRoyaltyPolicyLRP interface +interface IRoyaltyPolicyLRP is IRoyaltyPolicy {} diff --git a/contracts/interfaces/registries/ILicenseRegistry.sol b/contracts/interfaces/registries/ILicenseRegistry.sol index 6c01e2e0..49671422 100644 --- a/contracts/interfaces/registries/ILicenseRegistry.sol +++ b/contracts/interfaces/registries/ILicenseRegistry.sol @@ -189,4 +189,14 @@ interface ILicenseRegistry { /// @param ipId The address of the IP. /// @return Whether the IP is expired. function isExpiredNow(address ipId) external view returns (bool); + + /// @notice Returns the license terms through which a child IP links to a parent IP. + /// @param childIpId The address of the child IP. + /// @param parentIpId The address of the parent IP. + /// @return licenseTemplate The address of the license template. + /// @return licenseTermsId The ID of the license terms. + function getParentLicenseTerms( + address childIpId, + address parentIpId + ) external view returns (address licenseTemplate, uint256 licenseTermsId); } diff --git a/contracts/lib/Errors.sol b/contracts/lib/Errors.sol index 36146ba3..9ad6a042 100644 --- a/contracts/lib/Errors.sol +++ b/contracts/lib/Errors.sol @@ -132,6 +132,7 @@ library Errors { /// @notice The Group IP has been frozen due to already mint license tokens. error GroupingModule__GroupFrozenDueToAlreadyMintLicenseTokens(address groupId); + //////////////////////////////////////////////////////////////////////////// // IP Asset Registry // //////////////////////////////////////////////////////////////////////////// @@ -278,6 +279,9 @@ library Errors { address actualTokenOwner ); + /// @notice License token is not owned by the caller. + error LicenseToken__NotLicenseTokenOwner(uint256 tokenId, address ipOwner, address tokenOwner); + /// @notice All license tokens must be from the same license template. error LicenseToken__AllLicenseTokensMustFromSameLicenseTemplate( address licenseTemplate, @@ -451,33 +455,66 @@ library Errors { /// @notice Zero address provided for Royalty Token. error RoyaltyModule__ZeroRoyaltyToken(); - /// @notice Not a whitelisted royalty policy. - error RoyaltyModule__NotWhitelistedRoyaltyPolicy(); + /// @notice Zero maximum parents provided. + error RoyaltyModule__ZeroMaxParents(); - /// @notice Not a whitelisted royalty token. - error RoyaltyModule__NotWhitelistedRoyaltyToken(); + /// @notice Zero maximum ancestors provided. + error RoyaltyModule__ZeroMaxAncestors(); - /// @notice Royalty policy for IP is unset. - error RoyaltyModule__NoRoyaltyPolicySet(); + /// @notice Zero address provided for parent ipId. + error RoyaltyModule__ZeroParentIpId(); - /// @notice Royalty policy between IPs are incompatible (different). - error RoyaltyModule__IncompatibleRoyaltyPolicy(); + /// @notice Royalty token supply limit is exceeded. + error RoyaltyModule__AboveRoyaltyTokenSupplyLimit(); + + /// @notice Not a allowed royalty policy. + error RoyaltyModule__NotAllowedRoyaltyPolicy(); /// @notice Caller is unauthorized. error RoyaltyModule__NotAllowedCaller(); - /// @notice IP can only mint licenses of selected royalty policy. - error RoyaltyModule__CanOnlyMintSelectedPolicy(); - /// @notice Parent IP list for linking is empty. error RoyaltyModule__NoParentsOnLinking(); - /// @notice IP is expired. - error RoyaltyModule__IpIsExpired(); - /// @notice IP is dipute tagged. error RoyaltyModule__IpIsTagged(); + /// @notice Last position IP is not able to mint more licenses. + error RoyaltyModule__LastPositionNotAbleToMintLicense(); + + /// @notice The IP is not allowed to link to parents. + error RoyaltyModule__UnlinkableToParents(); + + /// @notice Size of parent IP list is above limit. + error RoyaltyModule__AboveParentLimit(); + + /// @notice Amount of ancestors for derivative IP is above the limit. + error RoyaltyModule__AboveAncestorsLimit(); + + /// @notice Royalty policy is already whitelisted or registered. + error RoyaltyModule__PolicyAlreadyWhitelistedOrRegistered(); + + /// @notice External Royalty Policy does not support IExternalRoyaltyPolicy interface. + error RoyaltyModule__ExternalRoyaltyPolicyInterfaceNotSupported(); + + /// @notice Royalty Policy is not whitelisted or registered. + error RoyaltyModule__NotWhitelistedOrRegisteredRoyaltyPolicy(); + + /// @notice Receiver ipId has no royalty vault. + error RoyaltyModule__ZeroReceiverVault(); + + /// @notice Zero amount provided. + error RoyaltyModule__ZeroAmount(); + + /// @notice Zero value for accumulated royalty policies limit. + error RoyaltyModule__ZeroAccumulatedRoyaltyPoliciesLimit(); + + /// @notice Above accumulated royalty policies limit. + error RoyaltyModule__AboveAccumulatedRoyaltyPoliciesLimit(); + + /// @notice Zero address for ip asset registry. + error RoyaltyModule__ZeroIpAssetRegistry(); + //////////////////////////////////////////////////////////////////////////// // Royalty Policy LAP // //////////////////////////////////////////////////////////////////////////// @@ -485,14 +522,11 @@ library Errors { /// @notice Zero address provided for Access Manager in initializer. error RoyaltyPolicyLAP__ZeroAccessManager(); - /// @notice Zero address provided for IP Royalty Vault Beacon. - error RoyaltyPolicyLAP__ZeroIpRoyaltyVaultBeacon(); - /// @notice Zero address provided for Royalty Module. error RoyaltyPolicyLAP__ZeroRoyaltyModule(); - /// @notice Zero address provided for Licensing Module. - error RoyaltyPolicyLAP__ZeroLicensingModule(); + /// @notice Zero address provided for Dispute Module. + error RoyaltyPolicyLAP__ZeroDisputeModule(); /// @notice Zero address provided for IP Graph ACL. error RoyaltyPolicyLAP__ZeroIPGraphACL(); @@ -500,54 +534,74 @@ library Errors { /// @notice Caller is not the Royalty Module. error RoyaltyPolicyLAP__NotRoyaltyModule(); - /// @notice Size of parent IP list is above the LAP royalty policy limit. - error RoyaltyPolicyLAP__AboveParentLimit(); - - /// @notice Amount of ancestors for derivative IP is above the LAP royalty policy limit. - error RoyaltyPolicyLAP__AboveAncestorsLimit(); - /// @notice Total royalty stack exceeds the protocol limit. error RoyaltyPolicyLAP__AboveRoyaltyStackLimit(); - /// @notice Size of parent royalties list and parent IP list mismatch. - error RoyaltyPolicyLAP__InvalidParentRoyaltiesLength(); + /// @notice IP is dispute tagged. + error RoyaltyPolicyLAP__IpTagged(); + + /// @notice IP is not allowed to claim revenue tokens. + error RoyaltyPolicyLAP__AlreadyClaimed(); - /// @notice IP cannot be linked to a parent, because it is either already linked to parents or derivatives (root). - error RoyaltyPolicyLAP__UnlinkableToParents(); + /// @notice Claimer is not an ancestor of the IP. + error RoyaltyPolicyLAP__ClaimerNotAnAncestor(); - /// @notice Policy is already initialized and IP is at the ancestors limit, so it can't mint more licenses. - error RoyaltyPolicyLAP__LastPositionNotAbleToMintLicense(); + /// @notice Not all revenue tokens have been claimed yet. + error RoyaltyPolicyLAP__NotAllRevenueTokensHaveBeenClaimed(); + + /// @notice There is no vault associated with the IP. + error RoyaltyPolicyLAP__InvalidTargetIpId(); //////////////////////////////////////////////////////////////////////////// - // IP Royalty Vault // + // Royalty Policy LRP // //////////////////////////////////////////////////////////////////////////// - /// @notice Zero address provided for Royalty Policy LAP. - error IpRoyaltyVault__ZeroRoyaltyPolicyLAP(); + /// @notice Caller is not the Royalty Module. + error RoyaltyPolicyLRP__NotRoyaltyModule(); + + /// @notice Zero address provided for Royalty Module. + error RoyaltyPolicyLRP__ZeroRoyaltyModule(); + + /// @notice Zero address provided for Access Manager in initializer. + error RoyaltyPolicyLRP__ZeroAccessManager(); + + //////////////////////////////////////////////////////////////////////////// + // IP Royalty Vault // + //////////////////////////////////////////////////////////////////////////// /// @notice Zero address provided for Dispute Module. error IpRoyaltyVault__ZeroDisputeModule(); - /// @notice Caller is not the Royalty Policy LAP. - error IpRoyaltyVault__NotRoyaltyPolicyLAP(); + /// @notice Zero address provided for Royalty Module. + error IpRoyaltyVault__ZeroRoyaltyModule(); - /// @notice Snapshot interval is too short, wait for the interval to pass for the next snapshot. - error IpRoyaltyVault__SnapshotIntervalTooShort(); + /// @notice Caller is not Royalty Module. + error IpRoyaltyVault__NotAllowedToAddTokenToVault(); - /// @notice Royalty Tokens is already claimed. - error IpRoyaltyVault__AlreadyClaimed(); + /// @notice Wait for the interval to pass for the next snapshot. + error IpRoyaltyVault__InsufficientTimeElapsedSinceLastSnapshot(); - /// @notice Royalty Tokens claimer is not an ancestor of derivative IP. - error IpRoyaltyVault__ClaimerNotAnAncestor(); + /// @notice No new revenue since the last snapshot. + error IpRoyaltyVault__NoNewRevenueSinceLastSnapshot(); - /// @notice IP is dispute tagged. - error IpRoyaltyVault__IpTagged(); + /// @notice There is no ip royalty vault for the provided IP. + error IpRoyaltyVault__InvalidTargetIpId(); + + /// @notice No claimable tokens. + error IpRoyaltyVault__NoClaimableTokens(); + + /// @notice Not a whitelisted royalty token. + error IpRoyaltyVault__NotWhitelistedRoyaltyToken(); /// @notice IP Royalty Vault is paused. error IpRoyaltyVault__EnforcedPause(); - /// @notice Failed to call IP Graph precompiled contract. - error IpRoyaltyVault__IpGraphCallFailed(); + //////////////////////////////////////////////////////////////////////////// + // Vault Controller // + //////////////////////////////////////////////////////////////////////////// + + /// @notice Zero address provided for IP Royalty Vault Beacon. + error VaultController__ZeroIpRoyaltyVaultBeacon(); //////////////////////////////////////////////////////////////////////////// // Module Registry // @@ -675,6 +729,10 @@ library Errors { //////////////////////////////////////////////////////////////////////////// // Group IPA // //////////////////////////////////////////////////////////////////////////// + + /// @notice Caller is not the IPA Asset Registry. error GroupNFT__CallerNotIPAssetRegistry(address caller); + + /// @notice Zero address provided for Access Manager. error GroupNFT__ZeroAccessManager(); } diff --git a/contracts/lib/IPAccountStorageOps.sol b/contracts/lib/IPAccountStorageOps.sol index ba3018bc..8b5aa180 100644 --- a/contracts/lib/IPAccountStorageOps.sol +++ b/contracts/lib/IPAccountStorageOps.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: UNLICENSED // See https://github.com/storyprotocol/protocol-contracts/blob/main/StoryProtocol-AlphaTestingAgreement-17942166.3.pdf -pragma solidity ^0.8.23; +pragma solidity 0.8.23; import { IIPAccountStorage } from "../interfaces/IIPAccountStorage.sol"; import { ShortString, ShortStrings } from "@openzeppelin/contracts/utils/ShortStrings.sol"; diff --git a/contracts/modules/licensing/LicensingModule.sol b/contracts/modules/licensing/LicensingModule.sol index 37d561a4..a69b0b10 100644 --- a/contracts/modules/licensing/LicensingModule.sol +++ b/contracts/modules/licensing/LicensingModule.sol @@ -260,7 +260,7 @@ contract LicensingModule is // the earliest expiration time among all license terms. LICENSE_REGISTRY.registerDerivativeIp(childIpId, parentIpIds, licenseTemplate, licenseTermsIds, false); // Process the payment for the minting fee. - (address commonRoyaltyPolicy, bytes[] memory royaltyDatas) = _payMintingFeeForAllParentIps( + (address[] memory royaltyPolicies, uint32[] memory royaltyPercents) = _payMintingFeeForAllParentIps( childIpId, parentIpIds, licenseTermsIds, @@ -278,9 +278,8 @@ contract LicensingModule is licenseTemplate ); - if (commonRoyaltyPolicy != address(0)) { - ROYALTY_MODULE.onLinkToParents(childIpId, commonRoyaltyPolicy, parentIpIds, royaltyDatas, royaltyContext); - } + if (royaltyPolicies.length == 0 || royaltyPolicies[0] == address(0)) return; + ROYALTY_MODULE.onLinkToParents(childIpId, parentIpIds, royaltyPolicies, royaltyPercents, royaltyContext); } /// @notice Registers a derivative with license tokens. @@ -328,21 +327,17 @@ contract LicensingModule is LICENSE_REGISTRY.registerDerivativeIp(childIpId, parentIpIds, licenseTemplate, licenseTermsIds, true); // Confirm that the royalty policies defined in all license terms of the parent IPs are identical. - address commonRoyaltyPolicy = address(0); - bytes[] memory royaltyDatas = new bytes[](parentIpIds.length); + address[] memory rPolicies = new address[](parentIpIds.length); + uint32[] memory rPercents = new uint32[](parentIpIds.length); for (uint256 i = 0; i < parentIpIds.length; i++) { - (address royaltyPolicy, bytes memory royaltyData, , ) = lct.getRoyaltyPolicy(licenseTermsIds[i]); - royaltyDatas[i] = royaltyData; - if (i == 0) { - commonRoyaltyPolicy = royaltyPolicy; - } else if (royaltyPolicy != commonRoyaltyPolicy) { - revert Errors.LicensingModule__IncompatibleRoyaltyPolicy(royaltyPolicy, commonRoyaltyPolicy); - } + (address royaltyPolicy, uint32 royaltyPercent, , ) = lct.getRoyaltyPolicy(licenseTermsIds[i]); + rPercents[i] = royaltyPercent; + rPolicies[i] = royaltyPolicy; } - // Notify the royalty module - if (commonRoyaltyPolicy != address(0)) { - ROYALTY_MODULE.onLinkToParents(childIpId, commonRoyaltyPolicy, parentIpIds, royaltyDatas, royaltyContext); + if (rPolicies.length != 0 && rPolicies[0] != address(0)) { + // Notify the royalty module + ROYALTY_MODULE.onLinkToParents(childIpId, parentIpIds, rPolicies, rPercents, royaltyContext); } // burn license tokens LICENSE_NFT.burnLicenseTokens(childIpOwner, licenseTokenIds); @@ -398,13 +393,12 @@ contract LicensingModule is address licenseTemplate, address childIpOwner, bytes calldata royaltyContext - ) private returns (address commonRoyaltyPolicy, bytes[] memory royaltyDatas) { - commonRoyaltyPolicy = address(0); - royaltyDatas = new bytes[](licenseTermsIds.length); - + ) private returns (address[] memory royaltyPolicies, uint32[] memory royaltyPercents) { + royaltyPolicies = new address[](licenseTermsIds.length); + royaltyPercents = new uint32[](licenseTermsIds.length); // pay minting fee for all parent IPs for (uint256 i = 0; i < parentIpIds.length; i++) { - (address royaltyPolicy, bytes memory royaltyData) = _executeLicensingHookAndPayMintingFee( + (address royaltyPolicy, uint32 royaltyPercent) = _executeLicensingHookAndPayMintingFee( childIpId, parentIpIds[i], licenseTemplate, @@ -412,14 +406,8 @@ contract LicensingModule is childIpOwner, royaltyContext ); - royaltyDatas[i] = royaltyData; - // royaltyPolicy must be the same for all parent IPs and royaltyPolicy could be 0 - // Using the first royaltyPolicy as the commonRoyaltyPolicy, all other royaltyPolicy must be the same - if (i == 0) { - commonRoyaltyPolicy = royaltyPolicy; - } else if (royaltyPolicy != commonRoyaltyPolicy) { - revert Errors.LicensingModule__IncompatibleRoyaltyPolicy(royaltyPolicy, commonRoyaltyPolicy); - } + royaltyPolicies[i] = royaltyPolicy; + royaltyPercents[i] = royaltyPercent; } } @@ -430,7 +418,7 @@ contract LicensingModule is uint256 licenseTermsId, address childIpOwner, bytes calldata royaltyContext - ) private returns (address royaltyPolicy, bytes memory royaltyData) { + ) private returns (address royaltyPolicy, uint32 royaltyPercent) { Licensing.LicensingConfig memory lsc = LICENSE_REGISTRY.getLicensingConfig( parentIpId, licenseTemplate, @@ -448,7 +436,7 @@ contract LicensingModule is lsc.hookData ); } - (royaltyPolicy, royaltyData) = _payMintingFee( + (royaltyPolicy, royaltyPercent) = _payMintingFee( parentIpId, licenseTemplate, licenseTermsId, @@ -470,7 +458,7 @@ contract LicensingModule is /// @param royaltyContext The context of the royalty. /// @param licensingConfig The minting license config /// @return royaltyPolicy The address of the royalty policy. - /// @return royaltyData The data of the royalty policy. + /// @return royaltyPercent The license royalty percentage function _payMintingFee( address parentIpId, address licenseTemplate, @@ -479,18 +467,18 @@ contract LicensingModule is bytes calldata royaltyContext, Licensing.LicensingConfig memory licensingConfig, uint256 mintingFeeByHook - ) private returns (address royaltyPolicy, bytes memory royaltyData) { + ) private returns (address royaltyPolicy, uint32 royaltyPercent) { ILicenseTemplate lct = ILicenseTemplate(licenseTemplate); uint256 mintingFeeByLicense = 0; address currencyToken = address(0); - (royaltyPolicy, royaltyData, mintingFeeByLicense, currencyToken) = lct.getRoyaltyPolicy(licenseTermsId); + (royaltyPolicy, royaltyPercent, mintingFeeByLicense, currencyToken) = lct.getRoyaltyPolicy(licenseTermsId); if (royaltyPolicy != address(0)) { - ROYALTY_MODULE.onLicenseMinting(parentIpId, royaltyPolicy, royaltyData, royaltyContext); + ROYALTY_MODULE.onLicenseMinting(parentIpId, royaltyPolicy, royaltyPercent, royaltyContext); uint256 tmf = _getTotalMintingFee(licensingConfig, mintingFeeByHook, mintingFeeByLicense, amount); // pay minting fee if (tmf > 0) { - ROYALTY_MODULE.payLicenseMintingFee(parentIpId, msg.sender, royaltyPolicy, currencyToken, tmf); + ROYALTY_MODULE.payLicenseMintingFee(parentIpId, msg.sender, currencyToken, tmf); } } } diff --git a/contracts/modules/licensing/PILicenseTemplate.sol b/contracts/modules/licensing/PILicenseTemplate.sol index 29d4a990..08a713c9 100644 --- a/contracts/modules/licensing/PILicenseTemplate.sol +++ b/contracts/modules/licensing/PILicenseTemplate.sol @@ -219,15 +219,15 @@ contract PILicenseTemplate is /// @notice Returns the royalty policy of a license terms. /// @param licenseTermsId The ID of the license terms. /// @return royaltyPolicy The address of the royalty policy specified for the license terms. - /// @return royaltyData The data of the royalty policy. + /// @return royaltyPercent The license royalty percentage /// @return mintingFee The fee for minting a license. /// @return currency The address of the ERC20 token, used for minting license fee and royalties. /// the currency token will used for pay for license token minting fee and royalties. function getRoyaltyPolicy( uint256 licenseTermsId - ) external view returns (address royaltyPolicy, bytes memory royaltyData, uint256 mintingFee, address currency) { + ) external view returns (address royaltyPolicy, uint32 royaltyPercent, uint256 mintingFee, address currency) { PILTerms memory terms = _getPILicenseTemplateStorage().licenseTerms[licenseTermsId]; - return (terms.royaltyPolicy, abi.encode(terms.commercialRevShare), terms.defaultMintingFee, terms.currency); + return (terms.royaltyPolicy, terms.commercialRevShare, terms.defaultMintingFee, terms.currency); } /// @notice Checks if a license terms is transferable. diff --git a/contracts/modules/metadata/CoreMetadataModule.sol b/contracts/modules/metadata/CoreMetadataModule.sol index 18cdfa43..9296e31c 100644 --- a/contracts/modules/metadata/CoreMetadataModule.sol +++ b/contracts/modules/metadata/CoreMetadataModule.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.23; +pragma solidity 0.8.23; import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; import { IERC721Metadata } from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; diff --git a/contracts/modules/metadata/CoreMetadataViewModule.sol b/contracts/modules/metadata/CoreMetadataViewModule.sol index 4dbfaba2..77c3e5f2 100644 --- a/contracts/modules/metadata/CoreMetadataViewModule.sol +++ b/contracts/modules/metadata/CoreMetadataViewModule.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.23; +pragma solidity 0.8.23; import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; diff --git a/contracts/modules/royalty/RoyaltyModule.sol b/contracts/modules/royalty/RoyaltyModule.sol index f8c07041..caeaf0d4 100644 --- a/contracts/modules/royalty/RoyaltyModule.sol +++ b/contracts/modules/royalty/RoyaltyModule.sol @@ -5,50 +5,72 @@ import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/ import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { BeaconProxy } from "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol"; import { BaseModule } from "../BaseModule.sol"; +import { VaultController } from "./policies/VaultController.sol"; import { IRoyaltyModule } from "../../interfaces/modules/royalty/IRoyaltyModule.sol"; import { IRoyaltyPolicy } from "../../interfaces/modules/royalty/policies/IRoyaltyPolicy.sol"; +import { IExternalRoyaltyPolicy } from "../../interfaces/modules/royalty/policies/IExternalRoyaltyPolicy.sol"; +import { IGroupIPAssetRegistry } from "../../interfaces/registries/IGroupIPAssetRegistry.sol"; +import { IIpRoyaltyVault } from "../../interfaces/modules/royalty/policies/IIpRoyaltyVault.sol"; import { IDisputeModule } from "../../interfaces/modules/dispute/IDisputeModule.sol"; import { ILicenseRegistry } from "../../interfaces/registries/ILicenseRegistry.sol"; import { ILicensingModule } from "../../interfaces/modules/licensing/ILicensingModule.sol"; import { Errors } from "../../lib/Errors.sol"; import { ROYALTY_MODULE_KEY } from "../../lib/modules/Module.sol"; -import { ProtocolPausableUpgradeable } from "../../pause/ProtocolPausableUpgradeable.sol"; /// @title Story Protocol Royalty Module -/// @notice The Story Protocol royalty module allows to set royalty policies an IP asset and pay royalties as a -/// derivative IP. -contract RoyaltyModule is - IRoyaltyModule, - ProtocolPausableUpgradeable, - ReentrancyGuardUpgradeable, - BaseModule, - UUPSUpgradeable -{ +/// @notice The Story Protocol royalty module governs the way derivatives pay royalties to their ancestors +contract RoyaltyModule is IRoyaltyModule, VaultController, ReentrancyGuardUpgradeable, BaseModule, UUPSUpgradeable { using ERC165Checker for address; + using EnumerableSet for EnumerableSet.AddressSet; + using SafeERC20 for IERC20; + + /// @notice Ip graph precompile contract address + address public constant IP_GRAPH = address(0x1A); + + /// @notice Returns the percentage scale - represents 100% of royalty tokens for an ip + uint32 public constant TOTAL_RT_SUPPLY = 100000000; // 100 * 10 ** 6 /// @notice Returns the canonical protocol-wide licensing module /// @custom:oz-upgrades-unsafe-allow state-variable-immutable ILicensingModule public immutable LICENSING_MODULE; + /// @notice Returns the protocol-wide dispute module + /// @custom:oz-upgrades-unsafe-allow state-variable-immutable + IDisputeModule public immutable DISPUTE_MODULE; + /// @notice Returns the canonical protocol-wide LicenseRegistry /// @custom:oz-upgrades-unsafe-allow state-variable-immutable ILicenseRegistry public immutable LICENSE_REGISTRY; - /// @notice Returns the protocol-wide dispute module + /// @notice Returns the canonical protocol-wide IPAssetRegistry /// @custom:oz-upgrades-unsafe-allow state-variable-immutable - IDisputeModule public immutable DISPUTE_MODULE; + IGroupIPAssetRegistry public immutable IP_ASSET_REGISTRY; /// @dev Storage structure for the RoyaltyModule + /// @param maxParents The maximum number of parents an IP asset can have + /// @param maxAncestors The maximum number of ancestors an IP asset can have + /// @param maxAccumulatedRoyaltyPolicies The maximum number of accumulated royalty policies an IP asset can have /// @param isWhitelistedRoyaltyPolicy Indicates if a royalty policy is whitelisted /// @param isWhitelistedRoyaltyToken Indicates if a royalty token is whitelisted - /// @param royaltyPolicies Indicates the royalty policy for a given IP asset + /// @param isRegisteredExternalRoyaltyPolicy Indicates if an external royalty policy is registered + /// @param ipRoyaltyVaults Indicates the royalty vault for a given IP asset (if any) + /// @param accumulatedRoyaltyPolicies Indicates the accumulated royalty policies for a given IP asset /// @custom:storage-location erc7201:story-protocol.RoyaltyModule struct RoyaltyModuleStorage { + uint256 maxParents; + uint256 maxAncestors; + uint256 maxAccumulatedRoyaltyPolicies; mapping(address royaltyPolicy => bool isWhitelisted) isWhitelistedRoyaltyPolicy; mapping(address token => bool) isWhitelistedRoyaltyToken; - mapping(address ipId => address royaltyPolicy) royaltyPolicies; + mapping(address royaltyPolicy => bool) isRegisteredExternalRoyaltyPolicy; + mapping(address ipId => address ipRoyaltyVault) ipRoyaltyVaults; + mapping(address ipId => EnumerableSet.AddressSet) accumulatedRoyaltyPolicies; } // keccak256(abi.encode(uint256(keccak256("story-protocol.RoyaltyModule")) - 1)) & ~bytes32(uint256(0xff)); @@ -58,26 +80,46 @@ contract RoyaltyModule is string public constant override name = ROYALTY_MODULE_KEY; /// @notice Constructor + /// @param licensingModule The address of the licensing module /// @param disputeModule The address of the dispute module /// @param licenseRegistry The address of the license registry + /// @param ipAssetRegistry The address of the ip asset registry /// @custom:oz-upgrades-unsafe-allow constructor - constructor(address licensingModule, address disputeModule, address licenseRegistry) { + constructor(address licensingModule, address disputeModule, address licenseRegistry, address ipAssetRegistry) { + if (licensingModule == address(0)) revert Errors.RoyaltyModule__ZeroLicensingModule(); if (disputeModule == address(0)) revert Errors.RoyaltyModule__ZeroDisputeModule(); if (licenseRegistry == address(0)) revert Errors.RoyaltyModule__ZeroLicenseRegistry(); - if (licensingModule == address(0)) revert Errors.RoyaltyModule__ZeroLicensingModule(); + if (ipAssetRegistry == address(0)) revert Errors.RoyaltyModule__ZeroIpAssetRegistry(); LICENSING_MODULE = ILicensingModule(licensingModule); DISPUTE_MODULE = IDisputeModule(disputeModule); LICENSE_REGISTRY = ILicenseRegistry(licenseRegistry); + IP_ASSET_REGISTRY = IGroupIPAssetRegistry(ipAssetRegistry); + _disableInitializers(); } /// @notice Initializer for this implementation contract /// @param accessManager The address of the protocol admin roles contract - function initialize(address accessManager) external initializer { - if (accessManager == address(0)) { - revert Errors.RoyaltyModule__ZeroAccessManager(); - } + /// @param parentLimit The maximum number of parents an IP asset can have + /// @param ancestorLimit The maximum number of ancestors an IP asset can have + /// @param accumulatedRoyaltyPoliciesLimit The maximum number of accumulated royalty policies an IP asset can have + function initialize( + address accessManager, + uint256 parentLimit, + uint256 ancestorLimit, + uint256 accumulatedRoyaltyPoliciesLimit + ) external initializer { + if (accessManager == address(0)) revert Errors.RoyaltyModule__ZeroAccessManager(); + if (parentLimit == 0) revert Errors.RoyaltyModule__ZeroMaxParents(); + if (ancestorLimit == 0) revert Errors.RoyaltyModule__ZeroMaxAncestors(); + if (accumulatedRoyaltyPoliciesLimit == 0) revert Errors.RoyaltyModule__ZeroAccumulatedRoyaltyPoliciesLimit(); + + RoyaltyModuleStorage storage $ = _getRoyaltyModuleStorage(); + $.maxParents = parentLimit; + $.maxAncestors = ancestorLimit; + $.maxAccumulatedRoyaltyPolicies = accumulatedRoyaltyPoliciesLimit; + __ProtocolPausable_init(accessManager); __ReentrancyGuard_init(); __UUPSUpgradeable_init(); @@ -89,6 +131,28 @@ contract RoyaltyModule is _; } + /// @notice Sets the ip graph limits + /// @dev Enforced to be only callable by the protocol admin + /// @param parentLimit The maximum number of parents an IP asset can have + /// @param ancestorLimit The maximum number of ancestors an IP asset can have + /// @param accumulatedRoyaltyPoliciesLimit The maximum number of accumulated royalty policies an IP asset can have + function setIpGraphLimits( + uint256 parentLimit, + uint256 ancestorLimit, + uint256 accumulatedRoyaltyPoliciesLimit + ) external restricted { + if (parentLimit == 0) revert Errors.RoyaltyModule__ZeroMaxParents(); + if (ancestorLimit == 0) revert Errors.RoyaltyModule__ZeroMaxAncestors(); + if (accumulatedRoyaltyPoliciesLimit == 0) revert Errors.RoyaltyModule__ZeroAccumulatedRoyaltyPoliciesLimit(); + + RoyaltyModuleStorage storage $ = _getRoyaltyModuleStorage(); + $.maxParents = parentLimit; + $.maxAncestors = ancestorLimit; + $.maxAccumulatedRoyaltyPolicies = accumulatedRoyaltyPoliciesLimit; + + emit IpGraphLimitsUpdated(parentLimit, ancestorLimit, accumulatedRoyaltyPoliciesLimit); + } + /// @notice Whitelist a royalty policy /// @dev Enforced to be only callable by the protocol admin /// @param royaltyPolicy The address of the royalty policy @@ -115,65 +179,109 @@ contract RoyaltyModule is emit RoyaltyTokenWhitelistUpdated(token, allowed); } + /// @notice Registers an external royalty policy + /// @param externalRoyaltyPolicy The address of the external royalty policy + function registerExternalRoyaltyPolicy(address externalRoyaltyPolicy) external nonReentrant { + RoyaltyModuleStorage storage $ = _getRoyaltyModuleStorage(); + if ( + $.isWhitelistedRoyaltyPolicy[externalRoyaltyPolicy] || + $.isRegisteredExternalRoyaltyPolicy[externalRoyaltyPolicy] + ) revert Errors.RoyaltyModule__PolicyAlreadyWhitelistedOrRegistered(); + + // checks if the IExternalRoyaltyPolicy call does not revert + // external royalty policies contracts should inherit IExternalRoyaltyPolicy interface + if (IExternalRoyaltyPolicy(externalRoyaltyPolicy).rtsRequiredToLink(address(0), 0) >= uint32(0)) { + $.isRegisteredExternalRoyaltyPolicy[externalRoyaltyPolicy] = true; + emit ExternalRoyaltyPolicyRegistered(externalRoyaltyPolicy); + } + } + /// @notice Executes royalty related logic on license minting /// @dev Enforced to be only callable by LicensingModule /// @param ipId The ipId whose license is being minted (licensor) /// @param royaltyPolicy The royalty policy address of the license being minted - /// @param licenseData The license data custom to each the royalty policy + /// @param licensePercent The license percentage of the license being minted /// @param externalData The external data custom to each the royalty policy function onLicenseMinting( address ipId, address royaltyPolicy, - bytes calldata licenseData, + uint32 licensePercent, bytes calldata externalData ) external nonReentrant onlyLicensingModule { RoyaltyModuleStorage storage $ = _getRoyaltyModuleStorage(); + if (royaltyPolicy == address(0)) revert Errors.RoyaltyModule__ZeroRoyaltyPolicy(); + if (licensePercent > TOTAL_RT_SUPPLY) revert Errors.RoyaltyModule__AboveRoyaltyTokenSupplyLimit(); - if (!$.isWhitelistedRoyaltyPolicy[royaltyPolicy]) revert Errors.RoyaltyModule__NotWhitelistedRoyaltyPolicy(); + if (!$.isWhitelistedRoyaltyPolicy[royaltyPolicy] && !$.isRegisteredExternalRoyaltyPolicy[royaltyPolicy]) + revert Errors.RoyaltyModule__NotAllowedRoyaltyPolicy(); - address royaltyPolicyIpId = $.royaltyPolicies[ipId]; + // If the an ipId has the maximum number of ancestors + // it can not have any derivative and therefore is not allowed to mint a license + if (_getAncestorCount(ipId) >= $.maxAncestors) revert Errors.RoyaltyModule__LastPositionNotAbleToMintLicense(); - // if the node is a root node, then royaltyPolicyIpId will be address(0) and any type of royalty type can be - // selected to mint a license if the node is a derivative node, then the any minted licenses by the derivative - // node should have the same royalty policy as the parent node and a derivative node set its royalty policy - // immutably in onLinkToParents() function below - if (royaltyPolicyIpId != royaltyPolicy && royaltyPolicyIpId != address(0)) - revert Errors.RoyaltyModule__CanOnlyMintSelectedPolicy(); + // deploy ipRoyaltyVault for the ipId given it does not exist yet + if ($.ipRoyaltyVaults[ipId] == address(0)) { + address receiver = IP_ASSET_REGISTRY.isRegisteredGroup(ipId) + ? IP_ASSET_REGISTRY.getGroupRewardPool(ipId) + : ipId; - IRoyaltyPolicy(royaltyPolicy).onLicenseMinting(ipId, licenseData, externalData); + _deployIpRoyaltyVault(ipId, receiver); + } + + // for whitelisted policies calls onLicenseMinting + if ($.isWhitelistedRoyaltyPolicy[royaltyPolicy]) { + IRoyaltyPolicy(royaltyPolicy).onLicenseMinting(ipId, licensePercent, externalData); + } } /// @notice Executes royalty related logic on linking to parents /// @dev Enforced to be only callable by LicensingModule /// @param ipId The children ipId that is being linked to parents - /// @param royaltyPolicy The common royalty policy address of all the licenses being burned /// @param parentIpIds The parent ipIds that the children ipId is being linked to - /// @param licenseData The license data custom to each the royalty policy + /// @param licensesPercent The license percentage of the licenses being minted /// @param externalData The external data custom to each the royalty policy function onLinkToParents( address ipId, - address royaltyPolicy, address[] calldata parentIpIds, - bytes[] memory licenseData, + address[] calldata licenseRoyaltyPolicies, + uint32[] calldata licensesPercent, bytes calldata externalData ) external nonReentrant onlyLicensingModule { RoyaltyModuleStorage storage $ = _getRoyaltyModuleStorage(); - if (!$.isWhitelistedRoyaltyPolicy[royaltyPolicy]) revert Errors.RoyaltyModule__NotWhitelistedRoyaltyPolicy(); - if (parentIpIds.length == 0) revert Errors.RoyaltyModule__NoParentsOnLinking(); - for (uint32 i = 0; i < parentIpIds.length; i++) { - address parentRoyaltyPolicy = $.royaltyPolicies[parentIpIds[i]]; - // if the parent node has a royalty policy set, then the derivative node should have the same royalty - // policy if the parent node does not have a royalty policy set, then the derivative node can set any type - // of royalty policy as long as the children ip obtained and is burning all licenses with that royalty type - // from each parent (was checked in licensing module before calling this function) - if (parentRoyaltyPolicy != royaltyPolicy && parentRoyaltyPolicy != address(0)) - revert Errors.RoyaltyModule__IncompatibleRoyaltyPolicy(); - } - - $.royaltyPolicies[ipId] = royaltyPolicy; + // If an IP already has a vault, it means that it's either a root node which cannot link to parents + // or it's a derivative in which case it cannot link to parents either + if ($.ipRoyaltyVaults[ipId] != address(0)) revert Errors.RoyaltyModule__UnlinkableToParents(); - IRoyaltyPolicy(royaltyPolicy).onLinkToParents(ipId, parentIpIds, licenseData, externalData); + if (parentIpIds.length == 0) revert Errors.RoyaltyModule__NoParentsOnLinking(); + if (parentIpIds.length > $.maxParents) revert Errors.RoyaltyModule__AboveParentLimit(); + if (_getAncestorCount(ipId) > $.maxAncestors) revert Errors.RoyaltyModule__AboveAncestorsLimit(); + + // deploy ipRoyaltyVault for the ipId given it does not exist yet + address ipRoyaltyVault = _deployIpRoyaltyVault(ipId, address(this)); + + // send royalty tokens to the royalty policies + // and saves the ancestors royalty policies for the child + _distributeRoyaltyTokensToPolicies(ipId, parentIpIds, licenseRoyaltyPolicies, licensesPercent, ipRoyaltyVault); + + // for whitelisted policies calls onLinkToParents + address[] memory accRoyaltyPolicies = $.accumulatedRoyaltyPolicies[ipId].values(); + for (uint256 i = 0; i < accRoyaltyPolicies.length; i++) { + if ( + !$.isWhitelistedRoyaltyPolicy[accRoyaltyPolicies[i]] && + !$.isRegisteredExternalRoyaltyPolicy[accRoyaltyPolicies[i]] + ) revert Errors.RoyaltyModule__NotWhitelistedOrRegisteredRoyaltyPolicy(); + + if ($.isWhitelistedRoyaltyPolicy[accRoyaltyPolicies[i]]) { + IRoyaltyPolicy(accRoyaltyPolicies[i]).onLinkToParents( + ipId, + parentIpIds, + licenseRoyaltyPolicies, + licensesPercent, + externalData + ); + } + } } /// @notice Allows the function caller to pay royalties to the receiver IP asset on behalf of the payer IP asset. @@ -187,23 +295,11 @@ contract RoyaltyModule is address token, uint256 amount ) external nonReentrant whenNotPaused { - RoyaltyModuleStorage storage $ = _getRoyaltyModuleStorage(); - if (!$.isWhitelistedRoyaltyToken[token]) revert Errors.RoyaltyModule__NotWhitelistedRoyaltyToken(); - IDisputeModule dispute = DISPUTE_MODULE; if (dispute.isIpTagged(receiverIpId) || dispute.isIpTagged(payerIpId)) revert Errors.RoyaltyModule__IpIsTagged(); - address payerRoyaltyPolicy = $.royaltyPolicies[payerIpId]; - // if the payer does not have a royalty policy set, then the payer is not a derivative ip and does not pay - // royalties while the receiver ip can have a zero royalty policy since that could mean it is an ip a root - if (payerRoyaltyPolicy == address(0)) revert Errors.RoyaltyModule__NoRoyaltyPolicySet(); - if (!$.isWhitelistedRoyaltyPolicy[payerRoyaltyPolicy]) - revert Errors.RoyaltyModule__NotWhitelistedRoyaltyPolicy(); - - if (LICENSE_REGISTRY.isExpiredNow(receiverIpId)) revert Errors.RoyaltyModule__IpIsExpired(); - - IRoyaltyPolicy(payerRoyaltyPolicy).onRoyaltyPayment(msg.sender, receiverIpId, token, amount); + _payToReceiverVault(receiverIpId, msg.sender, token, amount); emit RoyaltyPaid(receiverIpId, payerIpId, msg.sender, token, amount); } @@ -211,27 +307,26 @@ contract RoyaltyModule is /// @notice Allows to pay the minting fee for a license /// @param receiverIpId The ipId that receives the royalties /// @param payerAddress The address that pays the royalties - /// @param licenseRoyaltyPolicy The royalty policy of the license being minted /// @param token The token to use to pay the royalties /// @param amount The amount to pay function payLicenseMintingFee( address receiverIpId, address payerAddress, - address licenseRoyaltyPolicy, address token, uint256 amount ) external onlyLicensingModule { - RoyaltyModuleStorage storage $ = _getRoyaltyModuleStorage(); - if (!$.isWhitelistedRoyaltyToken[token]) revert Errors.RoyaltyModule__NotWhitelistedRoyaltyToken(); - if (!$.isWhitelistedRoyaltyPolicy[licenseRoyaltyPolicy]) - revert Errors.RoyaltyModule__NotWhitelistedRoyaltyPolicy(); - if (LICENSE_REGISTRY.isExpiredNow(receiverIpId)) revert Errors.RoyaltyModule__IpIsExpired(); + if (DISPUTE_MODULE.isIpTagged(receiverIpId)) revert Errors.RoyaltyModule__IpIsTagged(); - IRoyaltyPolicy(licenseRoyaltyPolicy).onRoyaltyPayment(payerAddress, receiverIpId, token, amount); + _payToReceiverVault(receiverIpId, payerAddress, token, amount); emit LicenseMintingFeePaid(receiverIpId, payerAddress, token, amount); } + /// @notice Returns the total number of royalty tokens + function totalRtSupply() external pure returns (uint32) { + return TOTAL_RT_SUPPLY; + } + /// @notice Indicates if a royalty policy is whitelisted /// @param royaltyPolicy The address of the royalty policy /// @return isWhitelisted True if the royalty policy is whitelisted @@ -239,6 +334,13 @@ contract RoyaltyModule is return _getRoyaltyModuleStorage().isWhitelistedRoyaltyPolicy[royaltyPolicy]; } + /// @notice Indicates if an external royalty policy is registered + /// @param externalRoyaltyPolicy The address of the external royalty policy + /// @return isRegistered True if the external royalty policy is registered + function isRegisteredExternalRoyaltyPolicy(address externalRoyaltyPolicy) external view returns (bool) { + return _getRoyaltyModuleStorage().isRegisteredExternalRoyaltyPolicy[externalRoyaltyPolicy]; + } + /// @notice Indicates if a royalty token is whitelisted /// @param token The address of the royalty token /// @return isWhitelisted True if the royalty token is whitelisted @@ -246,23 +348,143 @@ contract RoyaltyModule is return _getRoyaltyModuleStorage().isWhitelistedRoyaltyToken[token]; } - /// @notice Indicates the royalty policy for a given IP asset + /// @notice Returns the maximum number of parents an IP asset can have + function maxParents() external view returns (uint256) { + return _getRoyaltyModuleStorage().maxParents; + } + + /// @notice Returns the maximum number of ancestors an IP asset can have + function maxAncestors() external view returns (uint256) { + return _getRoyaltyModuleStorage().maxAncestors; + } + + /// @notice Returns the maximum number of accumulated royalty policies an IP asset can have + function maxAccumulatedRoyaltyPolicies() external view returns (uint256) { + return _getRoyaltyModuleStorage().maxAccumulatedRoyaltyPolicies; + } + + /// @notice Indicates the royalty vault for a given IP asset /// @param ipId The ID of IP asset - /// @return royaltyPolicy The address of the royalty policy - function royaltyPolicies(address ipId) external view returns (address) { - return _getRoyaltyModuleStorage().royaltyPolicies[ipId]; + function ipRoyaltyVaults(address ipId) external view returns (address) { + return _getRoyaltyModuleStorage().ipRoyaltyVaults[ipId]; } - /// @notice IERC165 interface support. + /// @notice Returns the accumulated royalty policies for a given IP asset + /// @param ipId The ID of IP asset + function accumulatedRoyaltyPolicies(address ipId) external view returns (address[] memory) { + return _getRoyaltyModuleStorage().accumulatedRoyaltyPolicies[ipId].values(); + } + + /// @notice IERC165 interface support function supportsInterface(bytes4 interfaceId) public view virtual override(BaseModule, IERC165) returns (bool) { return interfaceId == type(IRoyaltyModule).interfaceId || super.supportsInterface(interfaceId); } + /// @notice Deploys a new ipRoyaltyVault for the given ipId + /// @param ipId The ID of IP asset + /// @param receiver The address of the receiver + /// @return The address of the deployed ipRoyaltyVault + function _deployIpRoyaltyVault(address ipId, address receiver) internal returns (address) { + RoyaltyModuleStorage storage $ = _getRoyaltyModuleStorage(); + + address ipRoyaltyVault = address(new BeaconProxy(ipRoyaltyVaultBeacon(), "")); + IIpRoyaltyVault(ipRoyaltyVault).initialize("Royalty Token", "RT", TOTAL_RT_SUPPLY, ipId, receiver); + $.ipRoyaltyVaults[ipId] = ipRoyaltyVault; + + return ipRoyaltyVault; + } + + /// @notice Distributes royalty tokens to the royalty policies of the ancestors IP assets + /// @param ipId The ID of the IP asset + /// @param parentIpIds The parent IP assets + /// @param licenseRoyaltyPolicies The royalty policies of the each parent license + /// @param licensesPercent The license percentage of the licenses being minted + /// @param ipRoyaltyVault The address of the ipRoyaltyVault + function _distributeRoyaltyTokensToPolicies( + address ipId, + address[] calldata parentIpIds, + address[] calldata licenseRoyaltyPolicies, + uint32[] calldata licensesPercent, + address ipRoyaltyVault + ) internal { + RoyaltyModuleStorage storage $ = _getRoyaltyModuleStorage(); + + uint32 totalRtsRequiredToLink; + for (uint256 i = 0; i < parentIpIds.length; i++) { + if (parentIpIds[i] == address(0)) revert Errors.RoyaltyModule__ZeroParentIpId(); + if (licenseRoyaltyPolicies[i] == address(0)) revert Errors.RoyaltyModule__ZeroRoyaltyPolicy(); + _addToAccumulatedRoyaltyPolicies(parentIpIds[i], licenseRoyaltyPolicies[i]); + address[] memory accParentRoyaltyPolicies = $.accumulatedRoyaltyPolicies[parentIpIds[i]].values(); + for (uint256 j = 0; j < accParentRoyaltyPolicies.length; j++) { + // add the parent ancestor royalty policies to the child + _addToAccumulatedRoyaltyPolicies(ipId, accParentRoyaltyPolicies[j]); + // transfer the required royalty tokens to each policy + uint32 licensePercent = accParentRoyaltyPolicies[j] == licenseRoyaltyPolicies[i] + ? licensesPercent[i] + : 0; + uint32 rtsRequiredToLink = IRoyaltyPolicy(accParentRoyaltyPolicies[j]).rtsRequiredToLink( + parentIpIds[i], + licensePercent + ); + totalRtsRequiredToLink += rtsRequiredToLink; + if (totalRtsRequiredToLink > TOTAL_RT_SUPPLY) + revert Errors.RoyaltyModule__AboveRoyaltyTokenSupplyLimit(); + IERC20(ipRoyaltyVault).safeTransfer(accParentRoyaltyPolicies[j], rtsRequiredToLink); + } + } + + if ($.accumulatedRoyaltyPolicies[ipId].length() > $.maxAccumulatedRoyaltyPolicies) + revert Errors.RoyaltyModule__AboveAccumulatedRoyaltyPoliciesLimit(); + + // sends remaining royalty tokens to the ipId address or + // in the case the ipId is a group then send to the group reward pool + address receiver = IP_ASSET_REGISTRY.isRegisteredGroup(ipId) + ? IP_ASSET_REGISTRY.getGroupRewardPool(ipId) + : ipId; + IERC20(ipRoyaltyVault).safeTransfer(receiver, TOTAL_RT_SUPPLY - totalRtsRequiredToLink); + } + + /// @notice Adds a royalty policy to the accumulated royalty policies of an IP asset + /// @dev Function required to avoid stack too deep error + /// @param ipId The ID of the IP asset + /// @param royaltyPolicy The address of the royalty policy + function _addToAccumulatedRoyaltyPolicies(address ipId, address royaltyPolicy) internal { + _getRoyaltyModuleStorage().accumulatedRoyaltyPolicies[ipId].add(royaltyPolicy); + } + + /// @notice Pays the royalty to the receiver vault + /// @param receiverIpId The ID of the IP asset that receives the royalties + /// @param payerAddress The address that pays the royalties + /// @param token The token to use to pay the royalties + /// @param amount The amount to pay + function _payToReceiverVault(address receiverIpId, address payerAddress, address token, uint256 amount) internal { + RoyaltyModuleStorage storage $ = _getRoyaltyModuleStorage(); + + if (amount == 0) revert Errors.RoyaltyModule__ZeroAmount(); + + address receiverVault = $.ipRoyaltyVaults[receiverIpId]; + if (receiverVault == address(0)) revert Errors.RoyaltyModule__ZeroReceiverVault(); + + IIpRoyaltyVault(receiverVault).addIpRoyaltyVaultTokens(token); + IERC20(token).safeTransferFrom(payerAddress, receiverVault, amount); + } + + /// @notice Returns the count of ancestors for the given IP asset + /// @param ipId The ID of the IP asset + /// @return The number of ancestors + function _getAncestorCount(address ipId) internal returns (uint256) { + (bool success, bytes memory returnData) = IP_GRAPH.call( + abi.encodeWithSignature("getAncestorIpsCount(address)", ipId) + ); + require(success, "Call failed"); + return abi.decode(returnData, (uint256)); + } + /// @dev Hook to authorize the upgrade according to UUPSUpgradeable /// @param newImplementation The address of the new implementation function _authorizeUpgrade(address newImplementation) internal override restricted {} - /// @dev Returns the storage struct of RoyaltyModule. + /// @dev Returns the storage struct of RoyaltyModule function _getRoyaltyModuleStorage() private pure returns (RoyaltyModuleStorage storage $) { assembly { $.slot := RoyaltyModuleStorageLocation diff --git a/contracts/modules/royalty/policies/IpRoyaltyVault.sol b/contracts/modules/royalty/policies/IpRoyaltyVault.sol index 88bdccd8..b902b3d9 100644 --- a/contracts/modules/royalty/policies/IpRoyaltyVault.sol +++ b/contracts/modules/royalty/policies/IpRoyaltyVault.sol @@ -11,74 +11,63 @@ import { ERC20SnapshotUpgradeable } from "@openzeppelin/contracts-upgradeable-v4 import { SafeERC20Upgradeable } from "@openzeppelin/contracts-upgradeable-v4/token/ERC20/utils/SafeERC20Upgradeable.sol"; import { IERC20Upgradeable } from "@openzeppelin/contracts-upgradeable-v4/token/ERC20/IERC20Upgradeable.sol"; -import { IRoyaltyPolicyLAP } from "../../../interfaces/modules/royalty/policies/IRoyaltyPolicyLAP.sol"; +import { IVaultController } from "../../../interfaces/modules/royalty/policies/IVaultController.sol"; import { IDisputeModule } from "../../../interfaces/modules/dispute/IDisputeModule.sol"; +import { IRoyaltyModule } from "../../../interfaces/modules/royalty/IRoyaltyModule.sol"; import { IIpRoyaltyVault } from "../../../interfaces/modules/royalty/policies/IIpRoyaltyVault.sol"; import { Errors } from "../../../lib/Errors.sol"; /// @title Ip Royalty Vault -/// @notice Defines the logic for claiming royalty tokens and revenue tokens for a given IP +/// @notice Defines the logic for claiming revenue tokens for a given IP contract IpRoyaltyVault is IIpRoyaltyVault, ERC20SnapshotUpgradeable, ReentrancyGuardUpgradeable { using EnumerableSet for EnumerableSet.AddressSet; using SafeERC20Upgradeable for IERC20Upgradeable; /// @dev Storage structure for the IpRoyaltyVault /// @param ipId The ip id to whom this royalty vault belongs to - /// @param unclaimedRoyaltyTokens The amount of unclaimed royalty tokens /// @param lastSnapshotTimestamp The last snapshotted timestamp - /// @param ancestorsVaultAmount The amount of revenue token in the ancestors vault - /// @param collectableAmount The amount of revenue tokens that can be collected by the ancestor - /// @param isCollectedByAncestor Indicates whether the ancestor has collected the royalty tokens /// @param claimVaultAmount Amount of revenue token in the claim vault /// @param claimableAtSnapshot Amount of revenue token claimable at a given snapshot - /// @param unclaimedAtSnapshot Amount of unclaimed revenue tokens at the snapshot /// @param isClaimedAtSnapshot Indicates whether the claimer has claimed the revenue tokens at a given snapshot /// @param tokens The list of revenue tokens in the vault /// @custom:storage-location erc7201:story-protocol.IpRoyaltyVault struct IpRoyaltyVaultStorage { address ipId; - uint32 unclaimedRoyaltyTokens; uint40 lastSnapshotTimestamp; - mapping(address token => uint256 amount) ancestorsVaultAmount; - mapping(address ancestorIpId => mapping(address token => uint256 amount)) collectableAmount; - mapping(address ancestorIpId => bool) isCollectedByAncestor; mapping(address token => uint256 amount) claimVaultAmount; mapping(uint256 snapshotId => mapping(address token => uint256 amount)) claimableAtSnapshot; - mapping(uint256 snapshotId => uint32 tokenAmount) unclaimedAtSnapshot; mapping(uint256 snapshotId => mapping(address claimer => mapping(address token => bool))) isClaimedAtSnapshot; EnumerableSet.AddressSet tokens; } - address public constant IP_GRAPH_CONTRACT = address(0x1A); - // keccak256(abi.encode(uint256(keccak256("story-protocol.IpRoyaltyVault")) - 1)) & ~bytes32(uint256(0xff)); bytes32 private constant IpRoyaltyVaultStorageLocation = 0xe1c3e3b0c445d504edb1b9e6fa2ca4fab60584208a4bc973fe2db2b554d1df00; - /// @notice LAP royalty policy address - /// @custom:oz-upgrades-unsafe-allow state-variable-immutable - IRoyaltyPolicyLAP public immutable ROYALTY_POLICY_LAP; - /// @notice Dispute module address /// @custom:oz-upgrades-unsafe-allow state-variable-immutable IDisputeModule public immutable DISPUTE_MODULE; + /// @notice Royalty module address + /// @custom:oz-upgrades-unsafe-allow state-variable-immutable + IRoyaltyModule public immutable ROYALTY_MODULE; + modifier whenNotPaused() { - // DEV NOTE: If we upgrade RoyaltyPolicyLAP to not pausable, we need to remove this. - if (PausableUpgradeable(address(ROYALTY_POLICY_LAP)).paused()) revert Errors.IpRoyaltyVault__EnforcedPause(); + // DEV NOTE: If we upgrade RoyaltyModule to not pausable, we need to remove this. + if (PausableUpgradeable(address(ROYALTY_MODULE)).paused()) revert Errors.IpRoyaltyVault__EnforcedPause(); _; } /// @notice Constructor - /// @param royaltyPolicyLAP The address of the royalty policy LAP /// @param disputeModule The address of the dispute module + /// @param royaltyModule The address of the royalty module /// @custom:oz-upgrades-unsafe-allow constructor - constructor(address royaltyPolicyLAP, address disputeModule) { - if (royaltyPolicyLAP == address(0)) revert Errors.IpRoyaltyVault__ZeroRoyaltyPolicyLAP(); + constructor(address disputeModule, address royaltyModule) { if (disputeModule == address(0)) revert Errors.IpRoyaltyVault__ZeroDisputeModule(); + if (royaltyModule == address(0)) revert Errors.IpRoyaltyVault__ZeroRoyaltyModule(); - ROYALTY_POLICY_LAP = IRoyaltyPolicyLAP(royaltyPolicyLAP); DISPUTE_MODULE = IDisputeModule(disputeModule); + ROYALTY_MODULE = IRoyaltyModule(royaltyModule); _disableInitializers(); } @@ -87,23 +76,21 @@ contract IpRoyaltyVault is IIpRoyaltyVault, ERC20SnapshotUpgradeable, Reentrancy /// @param name The name of the royalty token /// @param symbol The symbol of the royalty token /// @param supply The total supply of the royalty token - /// @param unclaimedTokens The amount of unclaimed royalty tokens reserved for ancestors /// @param ipIdAddress The ip id the royalty vault belongs to + /// @param rtReceiver The address of the royalty token receiver function initialize( string memory name, string memory symbol, uint32 supply, - uint32 unclaimedTokens, - address ipIdAddress + address ipIdAddress, + address rtReceiver ) external initializer { IpRoyaltyVaultStorage storage $ = _getIpRoyaltyVaultStorage(); $.ipId = ipIdAddress; $.lastSnapshotTimestamp = uint40(block.timestamp); - $.unclaimedRoyaltyTokens = unclaimedTokens; - _mint(address(this), unclaimedTokens); - _mint(ipIdAddress, supply - unclaimedTokens); + _mint(rtReceiver, supply); __ReentrancyGuard_init(); __ERC20Snapshot_init(); @@ -111,17 +98,18 @@ contract IpRoyaltyVault is IIpRoyaltyVault, ERC20SnapshotUpgradeable, Reentrancy } /// @notice Returns the number royalty token decimals - function decimals() public view override returns (uint8) { + function decimals() public pure override returns (uint8) { return 6; } + /// @notice Adds a new revenue token to the vault /// @param token The address of the revenue token - /// @dev Only callable by the royalty policy LAP - /// @return Whether the token was added successfully - function addIpRoyaltyVaultTokens(address token) external returns (bool) { - if (msg.sender != address(ROYALTY_POLICY_LAP)) revert Errors.IpRoyaltyVault__NotRoyaltyPolicyLAP(); - IpRoyaltyVaultStorage storage $ = _getIpRoyaltyVaultStorage(); - return $.tokens.add(token); + /// @dev Only callable by the royalty module or whitelisted royalty policy + function addIpRoyaltyVaultTokens(address token) external { + if (msg.sender != address(ROYALTY_MODULE) && !ROYALTY_MODULE.isWhitelistedRoyaltyPolicy(msg.sender)) + revert Errors.IpRoyaltyVault__NotAllowedToAddTokenToVault(); + + _addIpRoyaltyVaultTokens(token); } /// @notice Snapshots the claimable revenue and royalty token amounts @@ -129,17 +117,14 @@ contract IpRoyaltyVault is IIpRoyaltyVault, ERC20SnapshotUpgradeable, Reentrancy function snapshot() external whenNotPaused returns (uint256) { IpRoyaltyVaultStorage storage $ = _getIpRoyaltyVaultStorage(); - if (block.timestamp - $.lastSnapshotTimestamp < ROYALTY_POLICY_LAP.getSnapshotInterval()) - revert Errors.IpRoyaltyVault__SnapshotIntervalTooShort(); + if (block.timestamp - $.lastSnapshotTimestamp < IVaultController(address(ROYALTY_MODULE)).snapshotInterval()) + revert Errors.IpRoyaltyVault__InsufficientTimeElapsedSinceLastSnapshot(); uint256 snapshotId = _snapshot(); $.lastSnapshotTimestamp = uint40(block.timestamp); - uint32 unclaimedTokens = $.unclaimedRoyaltyTokens; - $.unclaimedAtSnapshot[snapshotId] = unclaimedTokens; - + uint256 noRevenueCounter; address[] memory tokenList = $.tokens.values(); - for (uint256 i = 0; i < tokenList.length; i++) { uint256 tokenBalance = IERC20Upgradeable(tokenList[i]).balanceOf(address(this)); if (tokenBalance == 0) { @@ -147,18 +132,19 @@ contract IpRoyaltyVault is IIpRoyaltyVault, ERC20SnapshotUpgradeable, Reentrancy continue; } - uint256 newRevenue = tokenBalance - $.claimVaultAmount[tokenList[i]] - $.ancestorsVaultAmount[tokenList[i]]; - if (newRevenue == 0) continue; - - uint256 ancestorsTokens = (newRevenue * unclaimedTokens) / totalSupply(); - $.ancestorsVaultAmount[tokenList[i]] += ancestorsTokens; + uint256 newRevenue = tokenBalance - $.claimVaultAmount[tokenList[i]]; + if (newRevenue == 0) { + noRevenueCounter++; + continue; + } - uint256 claimableTokens = newRevenue - ancestorsTokens; - $.claimableAtSnapshot[snapshotId][tokenList[i]] = claimableTokens; - $.claimVaultAmount[tokenList[i]] += claimableTokens; + $.claimableAtSnapshot[snapshotId][tokenList[i]] = newRevenue; + $.claimVaultAmount[tokenList[i]] += newRevenue; } - emit SnapshotCompleted(snapshotId, block.timestamp, unclaimedTokens); + if (noRevenueCounter == tokenList.length) revert Errors.IpRoyaltyVault__NoNewRevenueSinceLastSnapshot(); + + emit SnapshotCompleted(snapshotId, block.timestamp); return snapshotId; } @@ -187,7 +173,7 @@ contract IpRoyaltyVault is IIpRoyaltyVault, ERC20SnapshotUpgradeable, Reentrancy for (uint256 i = 0; i < tokenList.length; i++) { uint256 claimableToken = _claimableRevenue(msg.sender, snapshotId, tokenList[i]); - if (claimableToken == 0) continue; + if (claimableToken == 0) revert Errors.IpRoyaltyVault__NoClaimableTokens(); $.isClaimedAtSnapshot[snapshotId][msg.sender][tokenList[i]] = true; $.claimVaultAmount[tokenList[i]] -= claimableToken; @@ -200,10 +186,11 @@ contract IpRoyaltyVault is IIpRoyaltyVault, ERC20SnapshotUpgradeable, Reentrancy /// @notice Allows token holders to claim by a list of snapshot ids based on the token balance at certain snapshot /// @param snapshotIds The list of snapshot ids /// @param token The revenue token to claim + /// @return The amount of revenue tokens claimed function claimRevenueBySnapshotBatch( uint256[] memory snapshotIds, address token - ) external nonReentrant whenNotPaused { + ) external nonReentrant whenNotPaused returns (uint256) { IpRoyaltyVaultStorage storage $ = _getIpRoyaltyVaultStorage(); uint256 claimableToken; @@ -212,120 +199,70 @@ contract IpRoyaltyVault is IIpRoyaltyVault, ERC20SnapshotUpgradeable, Reentrancy $.isClaimedAtSnapshot[snapshotIds[i]][msg.sender][token] = true; } + if (claimableToken == 0) revert Errors.IpRoyaltyVault__NoClaimableTokens(); + $.claimVaultAmount[token] -= claimableToken; IERC20Upgradeable(token).safeTransfer(msg.sender, claimableToken); emit RevenueTokenClaimed(msg.sender, token, claimableToken); - } - - /// @notice Allows ancestors to claim the royalty tokens and any accrued revenue tokens - /// @param ancestorIpId The ip id of the ancestor to whom the royalty tokens belong to - function collectRoyaltyTokens(address ancestorIpId) external nonReentrant whenNotPaused { - IpRoyaltyVaultStorage storage $ = _getIpRoyaltyVaultStorage(); - - address ipId = $.ipId; - if (DISPUTE_MODULE.isIpTagged(ipId)) revert Errors.IpRoyaltyVault__IpTagged(); - if ($.isCollectedByAncestor[ancestorIpId]) revert Errors.IpRoyaltyVault__AlreadyClaimed(); - - // check if the address being claimed to is an ancestor - if (!_hasAncestorIp(ipId, ancestorIpId)) revert Errors.IpRoyaltyVault__ClaimerNotAnAncestor(); - - // transfer royalty tokens to the ancestor - uint32 ancestorsRoyalty = _getRoyalty(ipId, ancestorIpId); - IERC20Upgradeable(address(this)).safeTransfer(ancestorIpId, ancestorsRoyalty); - - // save the amount of revenue tokens that are collectable by the ancestor - address[] memory tokenList = $.tokens.values(); - uint256 unclaimedTokens = $.unclaimedRoyaltyTokens; - for (uint256 i = 0; i < tokenList.length; ++i) { - // the only case in which unclaimedRoyaltyTokens can be 0 is when the vault is empty and everyone claimed - // in which case the call will revert upstream with IpRoyaltyVault__AlreadyClaimed error - uint256 collectAmount = ($.ancestorsVaultAmount[tokenList[i]] * ancestorsRoyalty) / unclaimedTokens; - if (collectAmount == 0) continue; - - $.collectableAmount[ancestorIpId][tokenList[i]] += collectAmount; - } - - $.isCollectedByAncestor[ancestorIpId] = true; - $.unclaimedRoyaltyTokens -= ancestorsRoyalty; - - emit RoyaltyTokensCollected(ancestorIpId, ancestorsRoyalty); + return claimableToken; } - /// @notice Collect the accrued tokens (if any) - /// @param ancestorIpId The ip id of the ancestor to whom the royalty tokens belong to - /// @param _tokens The list of revenue tokens to claim - function collectAccruedTokens( - address ancestorIpId, - address[] calldata _tokens - ) external nonReentrant whenNotPaused { - IpRoyaltyVaultStorage storage $ = _getIpRoyaltyVaultStorage(); - - if (DISPUTE_MODULE.isIpTagged($.ipId)) revert Errors.IpRoyaltyVault__IpTagged(); + /// @notice Allows to claim revenue tokens on behalf of the ip royalty vault by token batch + /// @param snapshotId The snapshot id + /// @param tokenList The list of revenue tokens to claim + /// @param targetIpId The target ip id to claim revenue tokens from + function claimByTokenBatchAsSelf( + uint256 snapshotId, + address[] calldata tokenList, + address targetIpId + ) external whenNotPaused { + address targetIpVault = ROYALTY_MODULE.ipRoyaltyVaults(targetIpId); + if (targetIpVault == address(0)) revert Errors.IpRoyaltyVault__InvalidTargetIpId(); - for (uint256 i = 0; i < _tokens.length; ++i) { - uint256 collectAmount = $.collectableAmount[ancestorIpId][_tokens[i]]; - $.ancestorsVaultAmount[_tokens[i]] -= collectAmount; - $.collectableAmount[ancestorIpId][_tokens[i]] -= collectAmount; - IERC20Upgradeable(_tokens[i]).safeTransfer(ancestorIpId, collectAmount); + IIpRoyaltyVault(targetIpVault).claimRevenueByTokenBatch(snapshotId, tokenList); - emit RevenueTokenClaimed(ancestorIpId, _tokens[i], collectAmount); + // only tokens that have claimable revenue higher than zero will be added to the vault + for (uint256 i = 0; i < tokenList.length; i++) { + _addIpRoyaltyVaultTokens(tokenList[i]); } } - /// @notice A function to calculate the amount of revenue token claimable by a token holder at certain snapshot - /// @param account The address of the token holder - /// @param snapshotId The snapshot id + /// @notice Allows to claim revenue tokens on behalf of the ip royalty vault by snapshot batch + /// @param snapshotIds The list of snapshot ids /// @param token The revenue token to claim - /// @return The amount of revenue token claimable - function _claimableRevenue(address account, uint256 snapshotId, address token) internal view returns (uint256) { - IpRoyaltyVaultStorage storage $ = _getIpRoyaltyVaultStorage(); + /// @param targetIpId The target ip id to claim revenue tokens from + function claimBySnapshotBatchAsSelf( + uint256[] memory snapshotIds, + address token, + address targetIpId + ) external whenNotPaused { + address targetIpVault = ROYALTY_MODULE.ipRoyaltyVaults(targetIpId); + if (targetIpVault == address(0)) revert Errors.IpRoyaltyVault__InvalidTargetIpId(); - // if the ip is tagged, then the unclaimed royalties are lost - if (DISPUTE_MODULE.isIpTagged($.ipId)) return 0; + IIpRoyaltyVault(targetIpVault).claimRevenueBySnapshotBatch(snapshotIds, token); - uint256 balance = balanceOfAt(account, snapshotId); - uint256 totalSupply = totalSupplyAt(snapshotId) - $.unclaimedAtSnapshot[snapshotId]; - uint256 claimableToken = $.claimableAtSnapshot[snapshotId][token]; - return $.isClaimedAtSnapshot[snapshotId][account][token] ? 0 : (balance * claimableToken) / totalSupply; + // the token will be added to the vault only if claimable revenue is higher than zero + _addIpRoyaltyVaultTokens(token); + } + + /// @notice Returns the current snapshot id + /// @return The snapshot id + function getCurrentSnapshotId() external view returns (uint256) { + return _getCurrentSnapshotId(); } /// @notice The ip id to whom this royalty vault belongs to - /// @return The ip id address function ipId() external view returns (address) { return _getIpRoyaltyVaultStorage().ipId; } - /// @notice The amount of unclaimed royalty tokens - function unclaimedRoyaltyTokens() external view returns (uint32) { - return _getIpRoyaltyVaultStorage().unclaimedRoyaltyTokens; - } - /// @notice The last snapshotted timestamp function lastSnapshotTimestamp() external view returns (uint256) { return _getIpRoyaltyVaultStorage().lastSnapshotTimestamp; } - /// @notice The amount of revenue token in the ancestors vault - /// @param token The address of the revenue token - function ancestorsVaultAmount(address token) external view returns (uint256) { - return _getIpRoyaltyVaultStorage().ancestorsVaultAmount[token]; - } - - /// @notice The amount of revenue tokens that can be collected by the ancestor - /// @param ancestorIpId The ancestor ipId address - /// @param token The address of the revenue token - function collectableAmount(address ancestorIpId, address token) external view returns (uint256) { - return _getIpRoyaltyVaultStorage().collectableAmount[ancestorIpId][token]; - } - - /// @notice Indicates whether the ancestor has collected the royalty tokens - /// @param ancestorIpId The ancestor ipId address - function isCollectedByAncestor(address ancestorIpId) external view returns (bool) { - return _getIpRoyaltyVaultStorage().isCollectedByAncestor[ancestorIpId]; - } - /// @notice Amount of revenue token in the claim vault /// @param token The address of the revenue token function claimVaultAmount(address token) external view returns (uint256) { @@ -339,12 +276,6 @@ contract IpRoyaltyVault is IIpRoyaltyVault, ERC20SnapshotUpgradeable, Reentrancy return _getIpRoyaltyVaultStorage().claimableAtSnapshot[snapshotId][token]; } - /// @notice Amount of unclaimed revenue tokens at the snapshot - /// @param snapshotId The snapshot id - function unclaimedAtSnapshot(uint256 snapshotId) external view returns (uint32) { - return _getIpRoyaltyVaultStorage().unclaimedAtSnapshot[snapshotId]; - } - /// @notice Indicates whether the claimer has claimed the revenue tokens at a given snapshot /// @param snapshotId The snapshot id /// @param claimer The address of the claimer @@ -353,28 +284,38 @@ contract IpRoyaltyVault is IIpRoyaltyVault, ERC20SnapshotUpgradeable, Reentrancy return _getIpRoyaltyVaultStorage().isClaimedAtSnapshot[snapshotId][claimer][token]; } - /// @notice The list of revenue tokens in the vault + /// @notice Returns list of revenue tokens in the vault function tokens() external view returns (address[] memory) { - return _getIpRoyaltyVaultStorage().tokens.values(); + return (_getIpRoyaltyVaultStorage().tokens).values(); } - function _hasAncestorIp(address ipId, address ancestorIpId) internal returns (bool) { - (bool success, bytes memory returnData) = IP_GRAPH_CONTRACT.call( - abi.encodeWithSignature("hasAncestorIp(address,address)", ipId, ancestorIpId) - ); - if (!success) revert Errors.IpRoyaltyVault__IpGraphCallFailed(); - return abi.decode(returnData, (bool)); + /// @notice A function to calculate the amount of revenue token claimable by a token holder at certain snapshot + /// @param account The address of the token holder + /// @param snapshotId The snapshot id + /// @param token The revenue token to claim + /// @return The amount of revenue token claimable + function _claimableRevenue(address account, uint256 snapshotId, address token) internal view returns (uint256) { + IpRoyaltyVaultStorage storage $ = _getIpRoyaltyVaultStorage(); + + // if the ip is tagged, then the unclaimed royalties are lost + if (DISPUTE_MODULE.isIpTagged($.ipId)) return 0; + + uint256 balance = balanceOfAt(account, snapshotId); + uint256 totalSupply = totalSupplyAt(snapshotId); + uint256 claimableToken = $.claimableAtSnapshot[snapshotId][token]; + return $.isClaimedAtSnapshot[snapshotId][account][token] ? 0 : (balance * claimableToken) / totalSupply; } - function _getRoyalty(address ipId, address parentIpId) internal returns (uint32) { - (bool success, bytes memory returnData) = IP_GRAPH_CONTRACT.call( - abi.encodeWithSignature("getRoyalty(address,address)", ipId, parentIpId) - ); - if (!success) revert Errors.IpRoyaltyVault__IpGraphCallFailed(); - return uint32(abi.decode(returnData, (uint256))); + /// @notice Adds a new revenue token to the vault + /// @param token The address of the revenue token + function _addIpRoyaltyVaultTokens(address token) internal { + if (!ROYALTY_MODULE.isWhitelistedRoyaltyToken(token)) + revert Errors.IpRoyaltyVault__NotWhitelistedRoyaltyToken(); + bool newTokenInVault = _getIpRoyaltyVaultStorage().tokens.add(token); + if (newTokenInVault) emit RevenueTokenAddedToVault(token, address(this)); } - /// @dev Returns the storage struct of the IpRoyaltyVault + /// @dev Returns the storage struct of IpRoyaltyVault function _getIpRoyaltyVaultStorage() private pure returns (IpRoyaltyVaultStorage storage $) { assembly { $.slot := IpRoyaltyVaultStorageLocation diff --git a/contracts/modules/royalty/policies/LAP/RoyaltyPolicyLAP.sol b/contracts/modules/royalty/policies/LAP/RoyaltyPolicyLAP.sol new file mode 100644 index 00000000..837eb314 --- /dev/null +++ b/contracts/modules/royalty/policies/LAP/RoyaltyPolicyLAP.sol @@ -0,0 +1,335 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.23; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; +import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; + +import { IRoyaltyModule } from "../../../../interfaces/modules/royalty/IRoyaltyModule.sol"; +import { IIpRoyaltyVault } from "../../../../interfaces/modules/royalty/policies/IIpRoyaltyVault.sol"; +import { IDisputeModule } from "../../../../interfaces/modules/dispute/IDisputeModule.sol"; +import { IRoyaltyPolicyLAP } from "../../../../interfaces/modules/royalty/policies/LAP/IRoyaltyPolicyLAP.sol"; +import { ArrayUtils } from "../../../../lib/ArrayUtils.sol"; +import { Errors } from "../../../../lib/Errors.sol"; +import { ProtocolPausableUpgradeable } from "../../../../pause/ProtocolPausableUpgradeable.sol"; +import { IPGraphACL } from "../../../../access/IPGraphACL.sol"; + +/// @title Liquid Absolute Percentage Royalty Policy +/// @notice Defines the logic for splitting royalties for a given ipId using a liquid absolute percentage mechanism +contract RoyaltyPolicyLAP is + IRoyaltyPolicyLAP, + ReentrancyGuardUpgradeable, + UUPSUpgradeable, + ProtocolPausableUpgradeable +{ + using SafeERC20 for IERC20; + + /// @dev Storage structure for the RoyaltyPolicyLAP + /// @param royaltyStack The royalty stack of a given ipId is the sum of the royalties to be paid to each ancestors + /// @param unclaimedRoyaltyTokens The unclaimed royalty tokens for a given ipId + /// @param isCollectedByAncestor Whether royalty tokens have been collected by an ancestor for a given ipId + /// @param revenueTokenBalances The revenue token balances claimed for a given ipId and token + /// @param snapshotsClaimed Whether a snapshot has been claimed for a given ipId and token + /// @param snapshotsClaimedCounter The number of snapshots claimed for a given ipId and token + /// @custom:storage-location erc7201:story-protocol.RoyaltyPolicyLAP + struct RoyaltyPolicyLAPStorage { + mapping(address ipId => uint32) royaltyStack; + mapping(address ipId => uint32) unclaimedRoyaltyTokens; + mapping(address ipId => mapping(address ancestorIpId => bool)) isCollectedByAncestor; + mapping(address ipId => mapping(address token => uint256)) revenueTokenBalances; + mapping(address ipId => mapping(address token => mapping(uint256 snapshotId => bool))) snapshotsClaimed; + mapping(address ipId => mapping(address token => uint256)) snapshotsClaimedCounter; + } + + // keccak256(abi.encode(uint256(keccak256("story-protocol.RoyaltyPolicyLAP")) - 1)) & ~bytes32(uint256(0xff)); + bytes32 private constant RoyaltyPolicyLAPStorageLocation = + 0x0c915ba68e2c4e37f19454bb13066f18f9db418fcefbf3c585b4b7d0fb0e0600; + + /// @notice Ip graph precompile contract address + address public constant IP_GRAPH = address(0x1A); + + /// @notice Returns the RoyaltyModule address + /// @custom:oz-upgrades-unsafe-allow state-variable-immutable + IRoyaltyModule public immutable ROYALTY_MODULE; + + /// @notice Dispute module address + /// @custom:oz-upgrades-unsafe-allow state-variable-immutable + IDisputeModule public immutable DISPUTE_MODULE; + + /// @notice IPGraphACL address + /// @custom:oz-upgrades-unsafe-allow state-variable-immutable + IPGraphACL public immutable IP_GRAPH_ACL; + + /// @dev Restricts the calls to the royalty module + modifier onlyRoyaltyModule() { + if (msg.sender != address(ROYALTY_MODULE)) revert Errors.RoyaltyPolicyLAP__NotRoyaltyModule(); + _; + } + + /// @notice Constructor + /// @param royaltyModule The RoyaltyModule address + /// @param disputeModule The DisputeModule address + /// @param ipGraphAcl The IPGraphACL address + /// @custom:oz-upgrades-unsafe-allow constructor + constructor(address royaltyModule, address disputeModule, address ipGraphAcl) { + if (royaltyModule == address(0)) revert Errors.RoyaltyPolicyLAP__ZeroRoyaltyModule(); + if (disputeModule == address(0)) revert Errors.RoyaltyPolicyLAP__ZeroDisputeModule(); + if (ipGraphAcl == address(0)) revert Errors.RoyaltyPolicyLAP__ZeroIPGraphACL(); + + ROYALTY_MODULE = IRoyaltyModule(royaltyModule); + DISPUTE_MODULE = IDisputeModule(disputeModule); + IP_GRAPH_ACL = IPGraphACL(ipGraphAcl); + + _disableInitializers(); + } + + /// @notice Initializer for this implementation contract + /// @param accessManager The address of the protocol admin roles contract + function initialize(address accessManager) external initializer { + if (accessManager == address(0)) revert Errors.RoyaltyPolicyLAP__ZeroAccessManager(); + __ProtocolPausable_init(accessManager); + __ReentrancyGuard_init(); + __UUPSUpgradeable_init(); + } + + /// @notice Executes royalty related logic on minting a license + /// @dev Enforced to be only callable by RoyaltyModule + /// @param ipId The ipId whose license is being minted (licensor) + /// @param licensePercent The license percentage of the license being minted + function onLicenseMinting( + address ipId, + uint32 licensePercent, + bytes calldata + ) external onlyRoyaltyModule nonReentrant { + // check if the new license royalty is within the royalty stack limit + if (_getRoyaltyStack(ipId) + licensePercent > ROYALTY_MODULE.totalRtSupply()) + revert Errors.RoyaltyPolicyLAP__AboveRoyaltyStackLimit(); + } + + /// @notice Executes royalty related logic on linking to parents + /// @dev Enforced to be only callable by RoyaltyModule + /// @param ipId The children ipId that is being linked to parents + /// @param parentIpIds The parent ipIds that the children ipId is being linked to + /// @param licensesPercent The license percentage of the licenses being minted + function onLinkToParents( + address ipId, + address[] calldata parentIpIds, + address[] memory licenseRoyaltyPolicies, + uint32[] calldata licensesPercent, + bytes calldata + ) external onlyRoyaltyModule nonReentrant { + RoyaltyPolicyLAPStorage storage $ = _getRoyaltyPolicyLAPStorage(); + + uint32[] memory royaltiesGroupedByParent = new uint32[](parentIpIds.length); + address[] memory uniqueParents = new address[](parentIpIds.length); + uint256 uniqueParentCount; + + IP_GRAPH_ACL.allow(); + for (uint256 i = 0; i < parentIpIds.length; i++) { + if (licenseRoyaltyPolicies[i] != address(this)) { + // currently only parents being linked through LAP license are added to the precompile + // so when a parent is linking through a different royalty policy, the royalty amount is set to zero + _setRoyaltyLAP(ipId, parentIpIds[i], 0); + } else { + // for parents linking through LAP license, the royalty amount is set in the precompile + (uint256 index, bool exists) = ArrayUtils.indexOf(uniqueParents, parentIpIds[i]); + if (!exists) { + index = uniqueParentCount; + uniqueParentCount++; + } + royaltiesGroupedByParent[index] += licensesPercent[i]; + uniqueParents[index] = parentIpIds[i]; + _setRoyaltyLAP(ipId, parentIpIds[i], royaltiesGroupedByParent[index]); + } + } + IP_GRAPH_ACL.disallow(); + + // calculate new royalty stack + uint32 newRoyaltyStack = _getRoyaltyStack(ipId); + if (newRoyaltyStack > ROYALTY_MODULE.totalRtSupply()) revert Errors.RoyaltyPolicyLAP__AboveRoyaltyStackLimit(); + + $.royaltyStack[ipId] = newRoyaltyStack; + $.unclaimedRoyaltyTokens[ipId] = newRoyaltyStack; + } + + /// @notice Collects royalty tokens to an ancestor's ip royalty vault + /// @param ipId The ID of the IP asset + /// @param ancestorIpId The ID of the ancestor IP asset + function collectRoyaltyTokens(address ipId, address ancestorIpId) external nonReentrant whenNotPaused { + RoyaltyPolicyLAPStorage storage $ = _getRoyaltyPolicyLAPStorage(); + + if (DISPUTE_MODULE.isIpTagged(ipId)) revert Errors.RoyaltyPolicyLAP__IpTagged(); + if ($.isCollectedByAncestor[ipId][ancestorIpId]) revert Errors.RoyaltyPolicyLAP__AlreadyClaimed(); + + // check if the address being claimed to is an ancestor + if (!_hasAncestorIp(ipId, ancestorIpId)) revert Errors.RoyaltyPolicyLAP__ClaimerNotAnAncestor(); + + // transfer royalty tokens to the ancestor vault + uint32 rtsToTransferToAncestor = _getRoyaltyLAP(ipId, ancestorIpId); + address ipIdIpRoyaltyVault = ROYALTY_MODULE.ipRoyaltyVaults(ipId); + address ancestorIpRoyaltyVault = ROYALTY_MODULE.ipRoyaltyVaults(ancestorIpId); + IERC20(ipIdIpRoyaltyVault).safeTransfer(ancestorIpRoyaltyVault, rtsToTransferToAncestor); + + // transfer revenue tokens to the ancestor vault + address[] memory tokenList = IIpRoyaltyVault(ipIdIpRoyaltyVault).tokens(); + uint256 totalRtSupply = uint256(ROYALTY_MODULE.totalRtSupply()); + uint256 currentSnapshotId = IIpRoyaltyVault(ipIdIpRoyaltyVault).getCurrentSnapshotId(); + for (uint256 i = 0; i < tokenList.length; ++i) { + uint256 revenueTokenBalance = $.revenueTokenBalances[ipId][tokenList[i]]; + // check if all revenue tokens have been claimed to LAP contract before the ancestor collects royalty tokens + if (currentSnapshotId != $.snapshotsClaimedCounter[ipId][tokenList[i]]) { + revert Errors.RoyaltyPolicyLAP__NotAllRevenueTokensHaveBeenClaimed(); + } + + if (revenueTokenBalance > 0) { + // when unclaimedRoyaltyTokens is zero then all royalty tokens have been claimed and it is ok to revert + uint256 revenueTokenToTransfer = (revenueTokenBalance * rtsToTransferToAncestor) / + $.unclaimedRoyaltyTokens[ipId]; + IERC20(tokenList[i]).safeTransfer(ancestorIpRoyaltyVault, revenueTokenToTransfer); + IIpRoyaltyVault(ancestorIpRoyaltyVault).addIpRoyaltyVaultTokens(tokenList[i]); + $.revenueTokenBalances[ipId][tokenList[i]] -= revenueTokenToTransfer; + } + } + + $.isCollectedByAncestor[ipId][ancestorIpId] = true; + $.unclaimedRoyaltyTokens[ipId] -= rtsToTransferToAncestor; + + emit RoyaltyTokensCollected(ipId, ancestorIpId, rtsToTransferToAncestor); + } + + /// @notice Allows claiming revenue tokens of behalf of royalty LAP royalty policy contract + /// @param snapshotIds The snapshot IDs to claim revenue tokens for + /// @param token The token to claim revenue tokens for + /// @param targetIpId The target IP ID to claim revenue tokens for + function claimBySnapshotBatchAsSelf( + uint256[] memory snapshotIds, + address token, + address targetIpId + ) external whenNotPaused nonReentrant { + RoyaltyPolicyLAPStorage storage $ = _getRoyaltyPolicyLAPStorage(); + + address targetIpVault = ROYALTY_MODULE.ipRoyaltyVaults(targetIpId); + if (targetIpVault == address(0)) revert Errors.RoyaltyPolicyLAP__InvalidTargetIpId(); + + uint256 tokensClaimed = IIpRoyaltyVault(targetIpVault).claimRevenueBySnapshotBatch(snapshotIds, token); + + // record which snapshots have been claimed for each token to ensure that revenue tokens have been + // claimed before allowing collecting the royalty tokens + for (uint256 i = 0; i < snapshotIds.length; i++) { + if (!$.snapshotsClaimed[targetIpId][token][snapshotIds[i]]) { + $.snapshotsClaimed[targetIpId][token][snapshotIds[i]] = true; + $.snapshotsClaimedCounter[targetIpId][token]++; + } + } + + $.revenueTokenBalances[targetIpId][token] += tokensClaimed; + } + + /// @notice Returns the amount of royalty tokens required to link a child to a given IP asset + /// @param ipId The ipId of the IP asset + /// @param licensePercent The percentage of the license + /// @return The amount of royalty tokens required to link a child to a given IP asset + function rtsRequiredToLink(address ipId, uint32 licensePercent) external view returns (uint32) { + return (_getRoyaltyPolicyLAPStorage().royaltyStack[ipId] + licensePercent); + } + + /// @notice Returns the royalty data for a given IP asset + /// @param ipId The ipId to get the royalty data for + /// @return royaltyStack The royalty stack of a given ipId is the sum of the royalties to be paid to each ancestors + function royaltyStack(address ipId) external view returns (uint32) { + return _getRoyaltyPolicyLAPStorage().royaltyStack[ipId]; + } + + /// @notice Returns the unclaimed royalty tokens for a given IP asset + /// @param ipId The ipId to get the unclaimed royalty tokens for + function unclaimedRoyaltyTokens(address ipId) external view returns (uint32) { + return _getRoyaltyPolicyLAPStorage().unclaimedRoyaltyTokens[ipId]; + } + + /// @notice Returns if the royalty tokens have been collected by an ancestor for a given IP asset + /// @param ipId The ipId to check if the royalty tokens have been collected by an ancestor + /// @param ancestorIpId The ancestor ipId to check if the royalty tokens have been collected + function isCollectedByAncestor(address ipId, address ancestorIpId) external view returns (bool) { + return _getRoyaltyPolicyLAPStorage().isCollectedByAncestor[ipId][ancestorIpId]; + } + + /// @notice Returns the revenue token balances for a given IP asset + /// @param ipId The ipId to get the revenue token balances for + /// @param token The token to get the revenue token balances for + function revenueTokenBalances(address ipId, address token) external view returns (uint256) { + return _getRoyaltyPolicyLAPStorage().revenueTokenBalances[ipId][token]; + } + + /// @notice Returns whether a snapshot has been claimed for a given IP asset and token + /// @param ipId The ipId to check if the snapshot has been claimed for + /// @param token The token to check if the snapshot has been claimed for + /// @param snapshot The snapshot to check if it has been claimed + function snapshotsClaimed(address ipId, address token, uint256 snapshot) external view returns (bool) { + return _getRoyaltyPolicyLAPStorage().snapshotsClaimed[ipId][token][snapshot]; + } + + /// @notice Returns the number of snapshots claimed for a given IP asset and token + /// @param ipId The ipId to check if the snapshot has been claimed for + /// @param token The token to check if the snapshot has been claimed for + function snapshotsClaimedCounter(address ipId, address token) external view returns (uint256) { + return _getRoyaltyPolicyLAPStorage().snapshotsClaimedCounter[ipId][token]; + } + + /// @notice Returns the royalty stack for a given IP asset + /// @param ipId The ipId to get the royalty stack for + /// @return The royalty stack for a given IP asset + function _getRoyaltyStack(address ipId) internal returns (uint32) { + (bool success, bytes memory returnData) = IP_GRAPH.call( + abi.encodeWithSignature("getRoyaltyStack(address)", ipId) + ); + require(success, "Call failed"); + return uint32(abi.decode(returnData, (uint256))); + } + + /// @notice Returns whether and IP is an ancestor of a given IP + /// @param ipId The ipId to check if it has an ancestor + /// @param ancestorIpId The ancestor ipId to check if it is an ancestor + /// @return True if the IP has the ancestor + function _hasAncestorIp(address ipId, address ancestorIpId) internal returns (bool) { + (bool success, bytes memory returnData) = IP_GRAPH.call( + abi.encodeWithSignature("hasAncestorIp(address,address)", ipId, ancestorIpId) + ); + require(success, "Call failed"); + return abi.decode(returnData, (bool)); + } + + /// @notice Sets the LAP royalty for a given IP asset + /// @param ipId The ipId to set the royalty for + /// @param parentIpId The parent ipId to set the royalty for + /// @param royalty The LAP license royalty amount + function _setRoyaltyLAP(address ipId, address parentIpId, uint32 royalty) internal { + (bool success, bytes memory returnData) = IP_GRAPH.call( + abi.encodeWithSignature("setRoyalty(address,address,uint256)", ipId, parentIpId, uint256(royalty)) + ); + require(success, "Call failed"); + } + + /// @notice Returns the royalty from LAP licenses for a given IP asset + /// @param ipId The ipId to get the royalty for + /// @param parentIpId The parent ipId to get the royalty for + /// @return The LAP license royalty amount + function _getRoyaltyLAP(address ipId, address parentIpId) internal returns (uint32) { + (bool success, bytes memory returnData) = IP_GRAPH.call( + abi.encodeWithSignature("getRoyalty(address,address)", ipId, parentIpId) + ); + require(success, "Call failed"); + return uint32(abi.decode(returnData, (uint256))); + } + + /// @notice Returns the storage struct for the RoyaltyPolicyLAP + function _getRoyaltyPolicyLAPStorage() private pure returns (RoyaltyPolicyLAPStorage storage $) { + assembly { + $.slot := RoyaltyPolicyLAPStorageLocation + } + } + + /// @dev Hook to authorize the upgrade according to UUPSUpgradeable + /// @param newImplementation The address of the new implementation + function _authorizeUpgrade(address newImplementation) internal override restricted {} +} diff --git a/contracts/modules/royalty/policies/LRP/RoyaltyPolicyLRP.sol b/contracts/modules/royalty/policies/LRP/RoyaltyPolicyLRP.sol new file mode 100644 index 00000000..948fe1dc --- /dev/null +++ b/contracts/modules/royalty/policies/LRP/RoyaltyPolicyLRP.sol @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.23; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; +import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; + +import { IRoyaltyModule } from "../../../../interfaces/modules/royalty/IRoyaltyModule.sol"; +import { IRoyaltyPolicyLRP } from "../../../../interfaces/modules/royalty/policies/LRP/IRoyaltyPolicyLRP.sol"; +import { Errors } from "../../../../lib/Errors.sol"; +import { ProtocolPausableUpgradeable } from "../../../../pause/ProtocolPausableUpgradeable.sol"; + +/// @title Liquid Relative Percentage Royalty Policy +/// @notice Defines the logic for splitting royalties for a given ipId using a liquid relative percentage mechanism +/// @dev [CAUTION] +/// The LRP (Limited Royalty Percentage) royalty policy allows each remixed IP to receive a percentage of the +/// revenue generated by its direct derivatives. However, it is important for external developers to understand the +/// potential dilution of royalties as more derivatives are created between two IPs. +/// This dilution can reduce the earnings of the original IP creator as more layers of derivatives are added. +/// +/// Example: +/// Creator 1 - Registers IP1, mints an LRP license of 10%, and sells the license to Creator 2. +/// Creator 2 - Registers IP2 as a derivative of IP1 and mints an LRP license of 20% for himself/herself. +/// Creator 2 - Registers IP3 as a derivative of IP2. Creator 2 decides to promote IP3 commercially in the market. +/// The earnings for Creator 1 are diluted because they will only receive 10% of the 20% royalties from IP3, +/// resulting in an effective royalty of 2%. If Creator 2 had chosen to promote IP2 instead, Creator 1 would +/// have earned 10% directly, avoiding this dilution. This lack of control over which IP is promoted commercially +/// means that Creator 1 is exposed to significant dilution risk under the LRP royalty policy. +/// +/// In contrast, the LAP (Limited Absolute Percentage) royalty policy enforces a fixed percentage on every +/// descendant IP, protecting the original creator from dilution. +/// +/// External developers considering the use of the LRP royalty policy should be aware of the potential for royalty +/// dilution and consider measures to prevent/mitigate the dilution risk or whether the LRP royalty policy is the +/// right policy for their use case. +contract RoyaltyPolicyLRP is + IRoyaltyPolicyLRP, + ReentrancyGuardUpgradeable, + UUPSUpgradeable, + ProtocolPausableUpgradeable +{ + using SafeERC20 for IERC20; + + /// @notice Returns the RoyaltyModule address + /// @custom:oz-upgrades-unsafe-allow state-variable-immutable + IRoyaltyModule public immutable ROYALTY_MODULE; + + /// @dev Restricts the calls to the royalty module + modifier onlyRoyaltyModule() { + if (msg.sender != address(ROYALTY_MODULE)) revert Errors.RoyaltyPolicyLRP__NotRoyaltyModule(); + _; + } + + /// @notice Constructor + /// @param royaltyModule The RoyaltyModule address + /// @custom:oz-upgrades-unsafe-allow constructor + constructor(address royaltyModule) { + if (royaltyModule == address(0)) revert Errors.RoyaltyPolicyLRP__ZeroRoyaltyModule(); + + ROYALTY_MODULE = IRoyaltyModule(royaltyModule); + _disableInitializers(); + } + + /// @notice Initializer for this implementation contract + /// @param accessManager The address of the protocol admin roles contract + function initialize(address accessManager) external initializer { + if (accessManager == address(0)) revert Errors.RoyaltyPolicyLRP__ZeroAccessManager(); + __ProtocolPausable_init(accessManager); + __ReentrancyGuard_init(); + __UUPSUpgradeable_init(); + } + + /// @notice Executes royalty related logic on minting a license + /// @dev Enforced to be only callable by RoyaltyModule + /// @param ipId The ipId whose license is being minted (licensor) + /// @param licensePercent The license percentage of the license being minted + function onLicenseMinting( + address ipId, + uint32 licensePercent, + bytes calldata + ) external onlyRoyaltyModule nonReentrant {} + + /// @notice Executes royalty related logic on linking to parents + /// @dev Enforced to be only callable by RoyaltyModule + /// @param ipId The children ipId that is being linked to parents + /// @param parentIpIds The parent ipIds that the children ipId is being linked to + /// @param licensesPercent The license percentage of the licenses being minted + function onLinkToParents( + address ipId, + address[] calldata parentIpIds, + address[] memory licenseRoyaltyPolicies, + uint32[] calldata licensesPercent, + bytes calldata + ) external onlyRoyaltyModule nonReentrant { + IRoyaltyModule royaltyModule = IRoyaltyModule(ROYALTY_MODULE); + + address ipRoyaltyVault = royaltyModule.ipRoyaltyVaults(ipId); + + // this for loop is limited to the maximum number of parents + for (uint256 i = 0; i < parentIpIds.length; i++) { + if (licenseRoyaltyPolicies[i] == address(this)) { + address parentRoyaltyVault = royaltyModule.ipRoyaltyVaults(parentIpIds[i]); + IERC20(ipRoyaltyVault).safeTransfer(parentRoyaltyVault, licensesPercent[i]); + } + } + } + + /// @notice Returns the amount of royalty tokens required to link a child to a given IP asset + /// @param ipId The ipId of the IP asset + /// @param licensePercent The percentage of the license + /// @return The amount of royalty tokens required to link a child to a given IP asset + function rtsRequiredToLink(address ipId, uint32 licensePercent) external view returns (uint32) { + return licensePercent; + } + + /// @dev Hook to authorize the upgrade according to UUPSUpgradeable + /// @param newImplementation The address of the new implementation + function _authorizeUpgrade(address newImplementation) internal override restricted {} +} diff --git a/contracts/modules/royalty/policies/RoyaltyPolicyLAP.sol b/contracts/modules/royalty/policies/RoyaltyPolicyLAP.sol deleted file mode 100644 index 04acc67c..00000000 --- a/contracts/modules/royalty/policies/RoyaltyPolicyLAP.sol +++ /dev/null @@ -1,311 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.23; - -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; -import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; -import { BeaconProxy } from "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol"; -import { UpgradeableBeacon } from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; - -import { IIpRoyaltyVault } from "../../../interfaces/modules/royalty/policies/IIpRoyaltyVault.sol"; -import { IRoyaltyPolicyLAP } from "../../../interfaces/modules/royalty/policies/IRoyaltyPolicyLAP.sol"; -import { ArrayUtils } from "../../../lib/ArrayUtils.sol"; -import { Errors } from "../../../lib/Errors.sol"; -import { ProtocolPausableUpgradeable } from "../../../pause/ProtocolPausableUpgradeable.sol"; -import { IPGraphACL } from "../../../access/IPGraphACL.sol"; - -/// @title Liquid Absolute Percentage Royalty Policy -/// @notice Defines the logic for splitting royalties for a given ipId using a liquid absolute percentage mechanism -contract RoyaltyPolicyLAP is - IRoyaltyPolicyLAP, - ProtocolPausableUpgradeable, - ReentrancyGuardUpgradeable, - UUPSUpgradeable -{ - using SafeERC20 for IERC20; - - /// @dev Storage structure for the RoyaltyPolicyLAP - /// @param ipRoyaltyVaultBeacon The ip royalty vault beacon address - /// @param snapshotInterval The minimum timestamp interval between snapshots - /// @param royaltyData The royalty data for a given IP asset - /// @custom:storage-location erc7201:story-protocol.RoyaltyPolicyLAP - struct RoyaltyPolicyLAPStorage { - address ipRoyaltyVaultBeacon; - uint256 snapshotInterval; - mapping(address ipId => LAPRoyaltyData) royaltyData; - } - - /// @notice Ip graph precompile contract address - address public constant IP_GRAPH_CONTRACT = address(0x1A); - - // keccak256(abi.encode(uint256(keccak256("story-protocol.RoyaltyPolicyLAP")) - 1)) & ~bytes32(uint256(0xff)); - bytes32 private constant RoyaltyPolicyLAPStorageLocation = - 0x0c915ba68e2c4e37f19454bb13066f18f9db418fcefbf3c585b4b7d0fb0e0600; - - /// @notice Returns the percentage scale - represents 100% of royalty tokens for an ip - uint32 public constant TOTAL_RT_SUPPLY = 100000000; // 100 * 10 ** 6 - - /// @notice Returns the maximum number of parents - uint256 public constant MAX_PARENTS = 2; - - /// @notice Returns the maximum number of total ancestors. - /// @dev The IP derivative tree is limited to 1024 ancestors - uint256 public constant MAX_ANCESTORS = 1024; - - /// @notice Returns the RoyaltyModule address - /// @custom:oz-upgrades-unsafe-allow state-variable-immutable - address public immutable ROYALTY_MODULE; - - /// @notice Returns the LicensingModule address - /// @custom:oz-upgrades-unsafe-allow state-variable-immutable - address public immutable LICENSING_MODULE; - - /// @custom:oz-upgrades-unsafe-allow state-variable-immutable - IPGraphACL public immutable IP_GRAPH_ACL; - - /// @dev Restricts the calls to the royalty module - modifier onlyRoyaltyModule() { - if (msg.sender != ROYALTY_MODULE) revert Errors.RoyaltyPolicyLAP__NotRoyaltyModule(); - _; - } - - /// @notice Constructor - /// @param royaltyModule The RoyaltyModule address - /// @param licensingModule The LicensingModule address - /// @param ipGraphAcl The IPGraphACL address - /// @custom:oz-upgrades-unsafe-allow constructor - constructor(address royaltyModule, address licensingModule, address ipGraphAcl) { - if (royaltyModule == address(0)) revert Errors.RoyaltyPolicyLAP__ZeroRoyaltyModule(); - if (licensingModule == address(0)) revert Errors.RoyaltyPolicyLAP__ZeroLicensingModule(); - if (ipGraphAcl == address(0)) revert Errors.RoyaltyPolicyLAP__ZeroIPGraphACL(); - - ROYALTY_MODULE = royaltyModule; - LICENSING_MODULE = licensingModule; - IP_GRAPH_ACL = IPGraphACL(ipGraphAcl); - - _disableInitializers(); - } - - /// @notice Initializer for this implementation contract - /// @param accessManager The address of the protocol admin roles contract - function initialize(address accessManager) external initializer { - if (accessManager == address(0)) revert Errors.RoyaltyPolicyLAP__ZeroAccessManager(); - __ProtocolPausable_init(accessManager); - __ReentrancyGuard_init(); - __UUPSUpgradeable_init(); - } - - /// @dev Set the snapshot interval - /// @dev Enforced to be only callable by the protocol admin in governance - /// @param timestampInterval The minimum timestamp interval between snapshots - function setSnapshotInterval(uint256 timestampInterval) public restricted { - RoyaltyPolicyLAPStorage storage $ = _getRoyaltyPolicyLAPStorage(); - $.snapshotInterval = timestampInterval; - - emit SnapshotIntervalSet(timestampInterval); - } - - /// @dev Set the ip royalty vault beacon - /// @dev Enforced to be only callable by the protocol admin in governance - /// @param beacon The ip royalty vault beacon address - function setIpRoyaltyVaultBeacon(address beacon) public restricted { - if (beacon == address(0)) revert Errors.RoyaltyPolicyLAP__ZeroIpRoyaltyVaultBeacon(); - RoyaltyPolicyLAPStorage storage $ = _getRoyaltyPolicyLAPStorage(); - $.ipRoyaltyVaultBeacon = beacon; - - emit IpRoyaltyVaultBeaconSet(beacon); - } - - /// @dev Upgrades the ip royalty vault beacon - /// @dev Enforced to be only callable by the upgrader admin - /// @param newVault The new ip royalty vault beacon address - function upgradeVaults(address newVault) public restricted { - // UpgradeableBeacon already checks for newImplementation.bytecode.length > 0, - // no need to check for zero address - RoyaltyPolicyLAPStorage storage $ = _getRoyaltyPolicyLAPStorage(); - UpgradeableBeacon($.ipRoyaltyVaultBeacon).upgradeTo(newVault); - } - - /// @notice Executes royalty related logic on minting a license - /// @dev Enforced to be only callable by RoyaltyModule - /// @param ipId The ipId whose license is being minted (licensor) - /// @param licenseData The license data custom to each the royalty policy - /// @param externalData The external data custom to each the royalty policy - function onLicenseMinting( - address ipId, - bytes calldata licenseData, - bytes calldata externalData - ) external onlyRoyaltyModule nonReentrant { - uint32 newLicenseRoyalty = abi.decode(licenseData, (uint32)); - RoyaltyPolicyLAPStorage storage $ = _getRoyaltyPolicyLAPStorage(); - - LAPRoyaltyData memory data = $.royaltyData[ipId]; - - if (_getRoyaltyStack(ipId) + newLicenseRoyalty > TOTAL_RT_SUPPLY) - revert Errors.RoyaltyPolicyLAP__AboveRoyaltyStackLimit(); - - if (data.ipRoyaltyVault == address(0)) { - // If the policy is already initialized, it means that the ipId setup is already done. If not, it means - // that the license for this royalty policy is being minted for the first time parentIpIds are zero given - // that only roots can call _initPolicy() for the first time in the function onLicenseMinting() while - // derivatives already - // called _initPolicy() when linking to their parents with onLinkToParents() call. - _initPolicy(ipId, new address[](0), new bytes[](0)); - } else { - // If the policy is already initialized and an ipId has the maximum number of ancestors - // it can not have any derivative and therefore is not allowed to mint any license - if (_getAncestorCount(ipId) >= MAX_ANCESTORS) - revert Errors.RoyaltyPolicyLAP__LastPositionNotAbleToMintLicense(); - } - } - - /// @notice Executes royalty related logic on linking to parents - /// @dev Enforced to be only callable by RoyaltyModule - /// @param ipId The children ipId that is being linked to parents - /// @param parentIpIds The parent ipIds that the children ipId is being linked to - /// @param licenseData The license data custom to each the royalty policy - /// @param externalData The external data custom to each the royalty policy - function onLinkToParents( - address ipId, - address[] calldata parentIpIds, - bytes[] memory licenseData, - bytes calldata externalData - ) external onlyRoyaltyModule nonReentrant { - RoyaltyPolicyLAPStorage storage $ = _getRoyaltyPolicyLAPStorage(); - if ($.royaltyData[ipId].isUnlinkableToParents) revert Errors.RoyaltyPolicyLAP__UnlinkableToParents(); - - _initPolicy(ipId, parentIpIds, licenseData); - } - - /// @notice Allows the caller to pay royalties to the given IP asset - /// @param caller The caller is the address from which funds will transferred from - /// @param ipId The ipId of the receiver of the royalties - /// @param token The token to pay - /// @param amount The amount to pay - function onRoyaltyPayment(address caller, address ipId, address token, uint256 amount) external onlyRoyaltyModule { - RoyaltyPolicyLAPStorage storage $ = _getRoyaltyPolicyLAPStorage(); - address destination = $.royaltyData[ipId].ipRoyaltyVault; - if (IIpRoyaltyVault(destination).addIpRoyaltyVaultTokens(token)) { - emit RevenueTokenAddedToVault(token, destination); - } - IERC20(token).safeTransferFrom(caller, destination, amount); - } - - /// @notice Returns the royalty data for a given IP asset - /// @param ipId The ipId to get the royalty data for - /// @return isUnlinkableToParents Indicates if the ipId is unlinkable to new parents - /// @return ipRoyaltyVault The ip royalty vault address - /// @return royaltyStack The royalty stack of a given ipId is the sum of the royalties to be paid to each ancestors - function getRoyaltyData(address ipId) external view returns (bool, address, uint32) { - RoyaltyPolicyLAPStorage storage $ = _getRoyaltyPolicyLAPStorage(); - LAPRoyaltyData memory data = $.royaltyData[ipId]; - return (data.isUnlinkableToParents, data.ipRoyaltyVault, data.royaltyStack); - } - - /// @notice Returns the snapshot interval - /// @return snapshotInterval The minimum timestamp interval between snapshots - function getSnapshotInterval() external view returns (uint256) { - RoyaltyPolicyLAPStorage storage $ = _getRoyaltyPolicyLAPStorage(); - return $.snapshotInterval; - } - - /// @notice Returns the ip royalty vault beacon - /// @return ipRoyaltyVaultBeacon The ip royalty vault beacon address - function getIpRoyaltyVaultBeacon() external view returns (address) { - RoyaltyPolicyLAPStorage storage $ = _getRoyaltyPolicyLAPStorage(); - return $.ipRoyaltyVaultBeacon; - } - - /// @dev Initializes the royalty policy for a given IP asset. - /// @dev Enforced to be only callable by RoyaltyModule - /// @param ipId The to initialize the policy for - /// @param parentIpIds The parent ipIds that the children ipId is being linked to (if any) - /// @param licenseData The license data custom to each the royalty policy - function _initPolicy(address ipId, address[] memory parentIpIds, bytes[] memory licenseData) internal { - RoyaltyPolicyLAPStorage storage $ = _getRoyaltyPolicyLAPStorage(); - - uint32[] memory royaltiesGroupByParent = new uint32[](parentIpIds.length); - address[] memory uniqueParents = new address[](parentIpIds.length); - uint256 uniqueParentCount; - - IP_GRAPH_ACL.allow(); - for (uint256 i = 0; i < parentIpIds.length; i++) { - (uint256 index, bool exists) = ArrayUtils.indexOf(uniqueParents, parentIpIds[i]); - if (!exists) { - index = uniqueParentCount; - uniqueParentCount++; - } - royaltiesGroupByParent[index] += abi.decode(licenseData[i], (uint32)); - uniqueParents[index] = parentIpIds[i]; - _setRoyalty(ipId, parentIpIds[i], royaltiesGroupByParent[index]); - } - IP_GRAPH_ACL.disallow(); - - // calculate new royalty stack - uint32 royaltyStack = _getRoyaltyStack(ipId); - - if (parentIpIds.length > MAX_PARENTS) revert Errors.RoyaltyPolicyLAP__AboveParentLimit(); - if (_getAncestorCount(ipId) > MAX_ANCESTORS) revert Errors.RoyaltyPolicyLAP__AboveAncestorsLimit(); - if (royaltyStack > TOTAL_RT_SUPPLY) revert Errors.RoyaltyPolicyLAP__AboveRoyaltyStackLimit(); - - // set the parents as unlinkable / loop limited to 2 parents - for (uint256 i = 0; i < parentIpIds.length; i++) { - $.royaltyData[parentIpIds[i]].isUnlinkableToParents = true; - } - - // deploy ip royalty vault - address ipRoyaltyVault = address(new BeaconProxy($.ipRoyaltyVaultBeacon, "")); - IIpRoyaltyVault(ipRoyaltyVault).initialize("Royalty Token", "RT", TOTAL_RT_SUPPLY, royaltyStack, ipId); - - $.royaltyData[ipId] = LAPRoyaltyData({ - // whether calling via minting license or linking to parents the ipId becomes unlinkable - isUnlinkableToParents: true, - ipRoyaltyVault: ipRoyaltyVault, - royaltyStack: royaltyStack - }); - - emit PolicyInitialized(ipId, ipRoyaltyVault, royaltyStack); - } - - function _getRoyaltyStack(address ipId) internal returns (uint32) { - (bool success, bytes memory returnData) = IP_GRAPH_CONTRACT.call( - abi.encodeWithSignature("getRoyaltyStack(address)", ipId) - ); - require(success, "Call failed"); - return uint32(abi.decode(returnData, (uint256))); - } - - function _getAncestorCount(address ipId) internal returns (uint256) { - (bool success, bytes memory returnData) = IP_GRAPH_CONTRACT.call( - abi.encodeWithSignature("getAncestorIpsCount(address)", ipId) - ); - require(success, "Call failed"); - return abi.decode(returnData, (uint256)); - } - - function _getRoyalty(address ipId, address parentIpId) internal returns (uint32) { - (bool success, bytes memory returnData) = IP_GRAPH_CONTRACT.call( - abi.encodeWithSignature("getRoyalty(address,address)", ipId, parentIpId) - ); - require(success, "Call failed"); - return uint32(abi.decode(returnData, (uint256))); - } - - function _setRoyalty(address ipId, address parentIpId, uint32 royalty) internal { - (bool success, bytes memory returnData) = IP_GRAPH_CONTRACT.call( - abi.encodeWithSignature("setRoyalty(address,address,uint256)", ipId, parentIpId, uint256(royalty)) - ); - require(success, "Call failed"); - } - - function _getRoyaltyPolicyLAPStorage() private pure returns (RoyaltyPolicyLAPStorage storage $) { - assembly { - $.slot := RoyaltyPolicyLAPStorageLocation - } - } - - /// @dev Hook to authorize the upgrade according to UUPSUpgradeable - /// @param newImplementation The address of the new implementation - function _authorizeUpgrade(address newImplementation) internal override restricted {} -} diff --git a/contracts/modules/royalty/policies/VaultController.sol b/contracts/modules/royalty/policies/VaultController.sol new file mode 100644 index 00000000..29579b22 --- /dev/null +++ b/contracts/modules/royalty/policies/VaultController.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import { UpgradeableBeacon } from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; + +import { IVaultController } from "../../../interfaces/modules/royalty/policies/IVaultController.sol"; +import { ProtocolPausableUpgradeable } from "../../../pause/ProtocolPausableUpgradeable.sol"; +import { Errors } from "../../../lib/Errors.sol"; + +/// @title Vault Controller +/// @notice Abstract contract that defines the common logic for royalty policies with ip royalty vaults +abstract contract VaultController is IVaultController, ProtocolPausableUpgradeable { + /// @dev Storage structure for the VaultController + /// @param ipRoyaltyVaultBeacon The ip royalty vault beacon address + /// @param snapshotInterval The minimum timestamp interval between snapshots + /// @custom:storage-location erc7201:story-protocol.VaultController + struct VaultControllerStorage { + address ipRoyaltyVaultBeacon; + uint256 snapshotInterval; + } + + // keccak256(abi.encode(uint256(keccak256("story-protocol.VaultController")) - 1)) & ~bytes32(uint256(0xff)); + bytes32 private constant VaultControllerStorageLocation = + 0x88cf5a7bd03e240c4fc740fb2d1a8664ec6fa4816f867d60f968080755fb1700; + + /// @dev Set the snapshot interval + /// @dev Enforced to be only callable by the protocol admin in governance + /// @param timestampInterval The minimum timestamp interval between snapshots + function setSnapshotInterval(uint256 timestampInterval) external restricted { + VaultControllerStorage storage $ = _getVaultControllerStorage(); + $.snapshotInterval = timestampInterval; + } + + /// @dev Set the ip royalty vault beacon + /// @dev Enforced to be only callable by the protocol admin in governance + /// @param beacon The ip royalty vault beacon address + function setIpRoyaltyVaultBeacon(address beacon) external restricted { + if (beacon == address(0)) revert Errors.VaultController__ZeroIpRoyaltyVaultBeacon(); + VaultControllerStorage storage $ = _getVaultControllerStorage(); + $.ipRoyaltyVaultBeacon = beacon; + } + + /// @dev Upgrades the ip royalty vault beacon + /// @dev Enforced to be only callable by the upgrader admin + /// @param newVault The new ip royalty vault beacon address + function upgradeVaults(address newVault) external restricted { + // UpgradeableBeacon already checks for newImplementation.bytecode.length > 0, + // no need to check for zero address + VaultControllerStorage storage $ = _getVaultControllerStorage(); + UpgradeableBeacon($.ipRoyaltyVaultBeacon).upgradeTo(newVault); + } + + /// @notice Returns the snapshot interval + /// @return snapshotInterval The minimum time interval between snapshots + function snapshotInterval() public view returns (uint256) { + return _getVaultControllerStorage().snapshotInterval; + } + + /// @notice Returns the ip royalty vault beacon + /// @return ipRoyaltyVaultBeacon The ip royalty vault beacon address + function ipRoyaltyVaultBeacon() public view returns (address) { + return _getVaultControllerStorage().ipRoyaltyVaultBeacon; + } + + /// @dev Returns the storage struct of VaultController. + function _getVaultControllerStorage() private pure returns (VaultControllerStorage storage $) { + assembly { + $.slot := VaultControllerStorageLocation + } + } +} diff --git a/contracts/registries/GroupIPAssetRegistry.sol b/contracts/registries/GroupIPAssetRegistry.sol index e8c4ba20..ab496192 100644 --- a/contracts/registries/GroupIPAssetRegistry.sol +++ b/contracts/registries/GroupIPAssetRegistry.sol @@ -17,7 +17,6 @@ abstract contract GroupIPAssetRegistry is IGroupIPAssetRegistry, ProtocolPausabl using EnumerableSet for EnumerableSet.AddressSet; /// @custom:oz-upgrades-unsafe-allow state-variable-immutable - IGroupingModule public immutable GROUPING_MODULE; /// @dev Storage structure for the GroupIPAssetRegistry diff --git a/contracts/registries/LicenseRegistry.sol b/contracts/registries/LicenseRegistry.sol index 56c053e9..752e7660 100644 --- a/contracts/registries/LicenseRegistry.sol +++ b/contracts/registries/LicenseRegistry.sol @@ -29,7 +29,7 @@ contract LicenseRegistry is ILicenseRegistry, AccessManagedUpgradeable, UUPSUpgr using EnumerableSet for EnumerableSet.AddressSet; using IPAccountStorageOps for IIPAccount; - address public constant IP_GRAPH_CONTRACT = address(0x1A); + address public constant IP_GRAPH = address(0x1A); /// @custom:oz-upgrades-unsafe-allow state-variable-immutable ILicensingModule public immutable LICENSING_MODULE; /// @custom:oz-upgrades-unsafe-allow state-variable-immutable @@ -44,6 +44,7 @@ contract LicenseRegistry is ILicenseRegistry, AccessManagedUpgradeable, UUPSUpgr /// @param registeredRoyaltyPolicies Registered royalty policies /// @param registeredCurrencyTokens Registered currency tokens /// @param parentIps Mapping of parent IPs to derivative IPs + /// @param parentLicenseTerms Mapping of parent IPs to license terms used to link to derivative IPs /// @param childIps Mapping of derivative IPs to parent IPs /// @param attachedLicenseTerms Mapping of attached license terms to IP IDs /// @param licenseTemplates Mapping of license templates to IP IDs @@ -58,6 +59,7 @@ contract LicenseRegistry is ILicenseRegistry, AccessManagedUpgradeable, UUPSUpgr uint256 defaultLicenseTermsId; mapping(address licenseTemplate => bool isRegistered) registeredLicenseTemplates; mapping(address childIpId => EnumerableSet.AddressSet parentIpIds) parentIps; + mapping(address childIpId => mapping(address parentIpId => uint256 licenseTermsId)) parentLicenseTerms; mapping(address parentIpId => EnumerableSet.AddressSet childIpIds) childIps; mapping(address ipId => EnumerableSet.UintSet licenseTermsIds) attachedLicenseTerms; mapping(address ipId => address licenseTemplate) licenseTemplates; @@ -248,7 +250,7 @@ contract LicenseRegistry is ILicenseRegistry, AccessManagedUpgradeable, UUPSUpgr } IP_GRAPH_ACL.allow(); - (bool success, ) = IP_GRAPH_CONTRACT.call( + (bool success, ) = IP_GRAPH.call( abi.encodeWithSignature("addParentIp(address,address[])", childIpId, parentIpIds) ); IP_GRAPH_ACL.disallow(); @@ -380,7 +382,7 @@ contract LicenseRegistry is ILicenseRegistry, AccessManagedUpgradeable, UUPSUpgr /// @param index The index of the parent IP within the array of all parent IPs of the IP. /// @return parentIpId The address of the parent IP. function getParentIp(address childIpId, uint256 index) external view returns (address parentIpId) { - (bool success, bytes memory returnData) = IP_GRAPH_CONTRACT.staticcall( + (bool success, bytes memory returnData) = IP_GRAPH.staticcall( abi.encodeWithSignature("getParentIps(address)", childIpId) ); require(success, "Call failed"); @@ -392,7 +394,7 @@ contract LicenseRegistry is ILicenseRegistry, AccessManagedUpgradeable, UUPSUpgr } function isParentIp(address parentIpId, address childIpId) external view returns (bool) { - (bool success, bytes memory returnData) = IP_GRAPH_CONTRACT.staticcall( + (bool success, bytes memory returnData) = IP_GRAPH.staticcall( abi.encodeWithSignature("hasParentIp(address,address)", childIpId, parentIpId) ); require(success, "Call failed"); @@ -403,7 +405,7 @@ contract LicenseRegistry is ILicenseRegistry, AccessManagedUpgradeable, UUPSUpgr /// @param childIpId The address of the childIP. /// @return The count o parent IPs. function getParentIpCount(address childIpId) external view returns (uint256) { - (bool success, bytes memory returnData) = IP_GRAPH_CONTRACT.staticcall( + (bool success, bytes memory returnData) = IP_GRAPH.staticcall( abi.encodeWithSignature("getParentIpsCount(address)", childIpId) ); require(success, "Call failed"); @@ -444,6 +446,19 @@ contract LicenseRegistry is ILicenseRegistry, AccessManagedUpgradeable, UUPSUpgr return ($.defaultLicenseTemplate, $.defaultLicenseTermsId); } + /// @notice Returns the license terms through which a child IP links to a parent IP. + /// @param childIpId The address of the child IP. + /// @param parentIpId The address of the parent IP. + /// @return licenseTemplate The address of the license template. + /// @return licenseTermsId The ID of the license terms. + function getParentLicenseTerms( + address childIpId, + address parentIpId + ) external view returns (address licenseTemplate, uint256 licenseTermsId) { + LicenseRegistryStorage storage $ = _getLicenseRegistryStorage(); + return ($.licenseTemplates[parentIpId], $.parentLicenseTerms[childIpId][parentIpId]); + } + /// @dev verify the child IP can be registered as a derivative of the parent IP /// @param parentIpId The address of the parent IP /// @param childIpId The address of the child IP @@ -519,7 +534,7 @@ contract LicenseRegistry is ILicenseRegistry, AccessManagedUpgradeable, UUPSUpgr /// @dev Check if an IP is a derivative/child IP /// @param childIpId The address of the IP function _isDerivativeIp(address childIpId) internal view returns (bool) { - (bool success, bytes memory returnData) = IP_GRAPH_CONTRACT.staticcall( + (bool success, bytes memory returnData) = IP_GRAPH.staticcall( abi.encodeWithSignature("getParentIpsCount(address)", childIpId) ); require(success, "Call failed"); diff --git a/script/foundry/utils/DeployHelper.sol b/script/foundry/utils/DeployHelper.sol index 3f81ac9a..82e3c8c2 100644 --- a/script/foundry/utils/DeployHelper.sol +++ b/script/foundry/utils/DeployHelper.sol @@ -19,7 +19,7 @@ import { ProtocolPausableUpgradeable } from "contracts/pause/ProtocolPausableUpg import { AccessController } from "contracts/access/AccessController.sol"; import { IPAccountImpl } from "contracts/IPAccountImpl.sol"; import { IIPAccount } from "contracts/interfaces/IIPAccount.sol"; -import { IRoyaltyPolicyLAP } from "contracts/interfaces/modules/royalty/policies/IRoyaltyPolicyLAP.sol"; +import { IRoyaltyPolicyLAP } from "contracts/interfaces/modules/royalty/policies/LAP/IRoyaltyPolicyLAP.sol"; import { AccessPermission } from "contracts/lib/AccessPermission.sol"; import { ProtocolAdmin } from "contracts/lib/ProtocolAdmin.sol"; import { Errors } from "contracts/lib/Errors.sol"; @@ -32,7 +32,9 @@ import { ModuleRegistry } from "contracts/registries/ModuleRegistry.sol"; import { LicenseRegistry } from "contracts/registries/LicenseRegistry.sol"; import { LicensingModule } from "contracts/modules/licensing/LicensingModule.sol"; import { RoyaltyModule } from "contracts/modules/royalty/RoyaltyModule.sol"; -import { RoyaltyPolicyLAP } from "contracts/modules/royalty/policies/RoyaltyPolicyLAP.sol"; +import { RoyaltyPolicyLAP } from "contracts/modules/royalty/policies/LAP/RoyaltyPolicyLAP.sol"; +import { RoyaltyPolicyLRP } from "contracts/modules/royalty/policies/LRP/RoyaltyPolicyLRP.sol"; +import { VaultController } from "contracts/modules/royalty/policies/VaultController.sol"; import { DisputeModule } from "contracts/modules/dispute/DisputeModule.sol"; import { ArbitrationPolicySP } from "contracts/modules/dispute/policies/ArbitrationPolicySP.sol"; import { MODULE_TYPE_HOOK } from "contracts/lib/modules/Module.sol"; @@ -90,6 +92,7 @@ contract DeployHelper is Script, BroadcastManager, JsonDeploymentHandler, Storag // Policy ArbitrationPolicySP internal arbitrationPolicySP; RoyaltyPolicyLAP internal royaltyPolicyLAP; + RoyaltyPolicyLRP internal royaltyPolicyLRP; UpgradeableBeacon internal ipRoyaltyVaultBeacon; IpRoyaltyVault internal ipRoyaltyVaultImpl; @@ -168,8 +171,8 @@ contract DeployHelper is Script, BroadcastManager, JsonDeploymentHandler, Storag (bool multisigAdmin, ) = protocolAccessManager.hasRole(ProtocolAdmin.PROTOCOL_ADMIN_ROLE, multisig); (bool multisigUpgrader, ) = protocolAccessManager.hasRole(ProtocolAdmin.UPGRADER_ROLE, multisig); - if (address(royaltyPolicyLAP) != ipRoyaltyVaultBeacon.owner()) { - revert RoleConfigError("RoyaltyPolicyLAP is not owner of IpRoyaltyVaultBeacon"); + if (address(royaltyModule) != ipRoyaltyVaultBeacon.owner()) { + revert RoleConfigError("RoyaltyModule is not owner of ipRoyaltyVaultBeacon"); } if (!multisigAdmin || !multisigUpgrader) { @@ -364,7 +367,8 @@ contract DeployHelper is Script, BroadcastManager, JsonDeploymentHandler, Storag new RoyaltyModule( _getDeployedAddress(type(LicensingModule).name), address(disputeModule), - address(licenseRegistry) + address(licenseRegistry), + address(ipAssetRegistry) ) ); royaltyModule = RoyaltyModule( @@ -372,7 +376,7 @@ contract DeployHelper is Script, BroadcastManager, JsonDeploymentHandler, Storag create3Deployer, _getSalt(type(RoyaltyModule).name), impl, - abi.encodeCall(RoyaltyModule.initialize, address(protocolAccessManager)) + abi.encodeCall(RoyaltyModule.initialize, (address(protocolAccessManager), uint256(8), uint256(1024), uint256(15))) ) ); require( @@ -514,7 +518,7 @@ contract DeployHelper is Script, BroadcastManager, JsonDeploymentHandler, Storag _predeploy("RoyaltyPolicyLAP"); impl = address(new RoyaltyPolicyLAP( address(royaltyModule), - address(licensingModule), + address(disputeModule), _getDeployedAddress(type(IPGraphACL).name) )); royaltyPolicyLAP = RoyaltyPolicyLAP( @@ -533,6 +537,24 @@ contract DeployHelper is Script, BroadcastManager, JsonDeploymentHandler, Storag impl = address(0); _postdeploy("RoyaltyPolicyLAP", address(royaltyPolicyLAP)); + _predeploy("RoyaltyPolicyLRP"); + impl = address(new RoyaltyPolicyLRP(address(royaltyModule))); + royaltyPolicyLRP = RoyaltyPolicyLRP( + TestProxyHelper.deployUUPSProxy( + create3Deployer, + _getSalt(type(RoyaltyPolicyLRP).name), + impl, + abi.encodeCall(RoyaltyPolicyLRP.initialize, address(protocolAccessManager)) + ) + ); + require( + _getDeployedAddress(type(RoyaltyPolicyLRP).name) == address(royaltyPolicyLRP), + "Deploy: Royalty Policy Address Mismatch" + ); + require(_loadProxyImpl(address(royaltyPolicyLRP)) == impl, "RoyaltyPolicyLRP Proxy Implementation Mismatch"); + impl = address(0); + _postdeploy("RoyaltyPolicyLRP", address(royaltyPolicyLRP)); + _predeploy("PILicenseTemplate"); impl = address( new PILicenseTemplate( @@ -571,14 +593,13 @@ contract DeployHelper is Script, BroadcastManager, JsonDeploymentHandler, Storag _getSalt(type(IpRoyaltyVault).name), abi.encodePacked( type(IpRoyaltyVault).creationCode, - abi.encode(address(royaltyPolicyLAP), address(disputeModule)) + abi.encode(address(disputeModule), address(royaltyModule)) ) ) ); _postdeploy("IpRoyaltyVaultImpl", address(ipRoyaltyVaultImpl)); _predeploy("IpRoyaltyVaultBeacon"); - // Transfer Ownership to RoyaltyPolicyLAP later ipRoyaltyVaultBeacon = UpgradeableBeacon( create3Deployer.deploy( _getSalt(type(UpgradeableBeacon).name), @@ -667,10 +688,11 @@ contract DeployHelper is Script, BroadcastManager, JsonDeploymentHandler, Storag // Royalty Module and SP Royalty Policy royaltyModule.whitelistRoyaltyPolicy(address(royaltyPolicyLAP), true); + royaltyModule.whitelistRoyaltyPolicy(address(royaltyPolicyLRP), true); royaltyModule.whitelistRoyaltyToken(address(erc20), true); - royaltyPolicyLAP.setSnapshotInterval(7 days); - royaltyPolicyLAP.setIpRoyaltyVaultBeacon(address(ipRoyaltyVaultBeacon)); - ipRoyaltyVaultBeacon.transferOwnership(address(royaltyPolicyLAP)); + royaltyModule.setSnapshotInterval(7 days); + royaltyModule.setIpRoyaltyVaultBeacon(address(ipRoyaltyVaultBeacon)); + ipRoyaltyVaultBeacon.transferOwnership(address(royaltyModule)); // Dispute Module and SP Dispute Policy address arbitrationRelayer = relayer; @@ -708,6 +730,8 @@ contract DeployHelper is Script, BroadcastManager, JsonDeploymentHandler, Storag ); protocolAccessManager.setTargetFunctionRole(address(licensingModule), selectors, ProtocolAdmin.UPGRADER_ROLE); protocolAccessManager.setTargetFunctionRole(address(royaltyModule), selectors, ProtocolAdmin.UPGRADER_ROLE); + protocolAccessManager.setTargetFunctionRole(address(royaltyPolicyLAP), selectors, ProtocolAdmin.UPGRADER_ROLE); + protocolAccessManager.setTargetFunctionRole(address(royaltyPolicyLRP), selectors, ProtocolAdmin.UPGRADER_ROLE); protocolAccessManager.setTargetFunctionRole(address(licenseRegistry), selectors, ProtocolAdmin.UPGRADER_ROLE); protocolAccessManager.setTargetFunctionRole(address(moduleRegistry), selectors, ProtocolAdmin.UPGRADER_ROLE); protocolAccessManager.setTargetFunctionRole(address(ipAssetRegistry), selectors, ProtocolAdmin.UPGRADER_ROLE); @@ -718,11 +742,11 @@ contract DeployHelper is Script, BroadcastManager, JsonDeploymentHandler, Storag ); // Royalty and Upgrade Beacon - // Owner of the beacon is the RoyaltyPolicyLAP + // Owner of the beacon is the RoyaltyModule selectors = new bytes4[](2); - selectors[0] = RoyaltyPolicyLAP.upgradeVaults.selector; + selectors[0] = VaultController.upgradeVaults.selector; selectors[1] = UUPSUpgradeable.upgradeToAndCall.selector; - protocolAccessManager.setTargetFunctionRole(address(royaltyPolicyLAP), selectors, ProtocolAdmin.UPGRADER_ROLE); + protocolAccessManager.setTargetFunctionRole(address(royaltyModule), selectors, ProtocolAdmin.UPGRADER_ROLE); // Pause selectors = new bytes4[](2); diff --git a/test/foundry/integration/BaseIntegration.t.sol b/test/foundry/integration/BaseIntegration.t.sol index 74b9c0ea..7570d2d6 100644 --- a/test/foundry/integration/BaseIntegration.t.sol +++ b/test/foundry/integration/BaseIntegration.t.sol @@ -22,7 +22,7 @@ contract BaseIntegration is BaseTest { dealMockAssets(); vm.prank(u.admin); - royaltyPolicyLAP.setSnapshotInterval(7 days); + royaltyModule.setSnapshotInterval(7 days); } /*////////////////////////////////////////////////////////////////////////// diff --git a/test/foundry/integration/big-bang/SingleNftCollection.t.sol b/test/foundry/integration/big-bang/SingleNftCollection.t.sol index a7128ac9..9ec4f986 100644 --- a/test/foundry/integration/big-bang/SingleNftCollection.t.sol +++ b/test/foundry/integration/big-bang/SingleNftCollection.t.sol @@ -128,7 +128,7 @@ contract BigBang_Integration_SingleNftCollection is BaseIntegration { // (verified by the mockTokenGatedHook commercializer checker) mockGatedNft.mint(u.carl); - mockToken.approve(address(royaltyPolicyLAP), mintingFee); + mockToken.approve(address(royaltyModule), mintingFee); uint256[] memory carl_license_from_root_alice = new uint256[](1); carl_license_from_root_alice[0] = licensingModule.mintLicenseTokens({ @@ -178,7 +178,7 @@ contract BigBang_Integration_SingleNftCollection is BaseIntegration { mockNFT.mintId(u.alice, 2); uint256 mintAmount = 2; - mockToken.approve(address(royaltyPolicyLAP), mintAmount * mintingFee); + mockToken.approve(address(royaltyModule), mintAmount * mintingFee); // Alice needs to hold an NFT from mockGatedNFT collection to mint license on pil_com_deriv_cheap_flexible // (verified by the mockTokenGatedHook commercializer checker) @@ -217,7 +217,7 @@ contract BigBang_Integration_SingleNftCollection is BaseIntegration { mockNFT.mintId(u.carl, tokenId); mockToken.mint(u.carl, mintingFee * license0_mintAmount); - mockToken.approve(address(royaltyPolicyLAP), mintingFee * license0_mintAmount); + mockToken.approve(address(royaltyModule), mintingFee * license0_mintAmount); uint256[] memory carl_licenses = new uint256[](2); // Commercial license (Carl already has mockGatedNft from above, so he passes commercializer checker check) @@ -253,7 +253,7 @@ contract BigBang_Integration_SingleNftCollection is BaseIntegration { uint256 license1_mintAmount = 500; mockToken.mint(u.carl, mintingFee * license1_mintAmount); - mockToken.approve(address(royaltyPolicyLAP), mintingFee * license1_mintAmount); + mockToken.approve(address(royaltyModule), mintingFee * license1_mintAmount); // Modify license[1] to a Commercial license carl_licenses[1] = licensingModule.mintLicenseTokens({ diff --git a/test/foundry/integration/flows/licensing/LicensingIntegration.t.sol b/test/foundry/integration/flows/licensing/LicensingIntegration.t.sol index 24b218f4..2f4078bf 100644 --- a/test/foundry/integration/flows/licensing/LicensingIntegration.t.sol +++ b/test/foundry/integration/flows/licensing/LicensingIntegration.t.sol @@ -168,7 +168,7 @@ contract LicensingIntegrationTest is BaseIntegration { // mint license token with payments vm.startPrank(u.dan); erc20.mint(u.dan, 1000); - erc20.approve(address(royaltyPolicyLAP), 100); + erc20.approve(address(royaltyModule), 100); lcTokenId = licensingModule.mintLicenseTokens(ipAcct[1], address(pilTemplate), 2, 1, address(u.dan), ""); @@ -207,7 +207,7 @@ contract LicensingIntegrationTest is BaseIntegration { // register derivative directly with payments vm.startPrank(u.eve); erc20.mint(u.eve, 1000); - erc20.approve(address(royaltyPolicyLAP), 100); + erc20.approve(address(royaltyModule), 100); parentIpIds = new address[](1); licenseTermsIds = new uint256[](1); parentIpIds[0] = ipAcct[1]; diff --git a/test/foundry/integration/flows/licensing/LicensingScenarios.t.sol b/test/foundry/integration/flows/licensing/LicensingScenarios.t.sol index bd34abea..35d78de6 100644 --- a/test/foundry/integration/flows/licensing/LicensingScenarios.t.sol +++ b/test/foundry/integration/flows/licensing/LicensingScenarios.t.sol @@ -134,7 +134,7 @@ contract Licensing_Scenarios is BaseIntegration { licensingModule.registerDerivativeWithLicenseTokens(ipAcct[2], licenseIds, ""); // Mint license for commercial use, then link to new IPA to make it a derivative - IERC20(USDC).approve(address(royaltyPolicyLAP), mintingFee); + IERC20(USDC).approve(address(royaltyModule), mintingFee); licenseIds[0] = licensingModule.mintLicenseTokens({ licensorIpId: ipAcct[1], licenseTemplate: address(pilTemplate), @@ -146,7 +146,7 @@ contract Licensing_Scenarios is BaseIntegration { licensingModule.registerDerivativeWithLicenseTokens(ipAcct[3], licenseIds, ""); // Mint license for commercial remixing, then link to new IPA to make it a derivative - IERC20(USDC).approve(address(royaltyPolicyLAP), mintingFee); + IERC20(USDC).approve(address(royaltyModule), mintingFee); licenseIds[0] = licensingModule.mintLicenseTokens({ licensorIpId: ipAcct[1], licenseTemplate: address(pilTemplate), diff --git a/test/foundry/integration/flows/royalty/Royalty.t.sol b/test/foundry/integration/flows/royalty/Royalty.t.sol index 8ddc229b..a4879884 100644 --- a/test/foundry/integration/flows/royalty/Royalty.t.sol +++ b/test/foundry/integration/flows/royalty/Royalty.t.sol @@ -9,7 +9,8 @@ import { ERC20, IERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; // contracts import { IRoyaltyModule } from "../../../../../contracts/interfaces/modules/royalty/IRoyaltyModule.sol"; import { IpRoyaltyVault } from "../../../../../contracts/modules/royalty/policies/IpRoyaltyVault.sol"; -import { IIpRoyaltyVault } from "../../../../../contracts/interfaces/modules/royalty/policies/IIpRoyaltyVault.sol"; +// solhint-disable-next-line max-line-length +import { IRoyaltyPolicyLAP } from "../../../../../contracts/interfaces/modules/royalty/policies/LAP/IRoyaltyPolicyLAP.sol"; import { Errors } from "../../../../../contracts/lib/Errors.sol"; import { PILFlavors } from "../../../../../contracts/lib/PILFlavors.sol"; @@ -62,7 +63,7 @@ contract Flows_Integration_Disputes is BaseIntegration { vm.startPrank(u.bob); uint256 mintAmount = 3; - erc20.approve(address(royaltyPolicyLAP), mintAmount * mintingFee); + erc20.approve(address(royaltyModule), mintAmount * mintingFee); uint256[] memory licenseIds = new uint256[](3); @@ -110,7 +111,7 @@ contract Flows_Integration_Disputes is BaseIntegration { uint256 mintAmount = 1; uint256[] memory licenseIds = new uint256[](2); - erc20.approve(address(royaltyPolicyLAP), 2 * mintAmount * mintingFee); + erc20.approve(address(royaltyModule), 2 * mintAmount * mintingFee); vm.expectEmit(address(royaltyModule)); emit IRoyaltyModule.LicenseMintingFeePaid(ipAcct[1], u.carl, address(erc20), mintAmount * mintingFee); @@ -151,7 +152,7 @@ contract Flows_Integration_Disputes is BaseIntegration { mockToken.mint(newUser, 1 ether); - mockToken.approve(address(royaltyPolicyLAP), 1 ether); + mockToken.approve(address(royaltyModule), 1 ether); // ipAcct[3] is the receiver, the actual token is paid by the caller (newUser). royaltyModule.payRoyaltyOnBehalf(ipAcct[3], ipAcct[3], address(mockToken), 1 ether); totalPaymentToIpAcct3 += 1 ether; @@ -166,90 +167,74 @@ contract Flows_Integration_Disputes is BaseIntegration { ERC20[] memory tokens = new ERC20[](1); tokens[0] = mockToken; - (, address ipRoyaltyVault, ) = royaltyPolicyLAP.getRoyaltyData(ipAcct[3]); + address ipRoyaltyVault3 = royaltyModule.ipRoyaltyVaults(ipAcct[3]); + address ipRoyaltyVault2 = royaltyModule.ipRoyaltyVaults(ipAcct[2]); vm.warp(block.timestamp + 7 days + 1); - IpRoyaltyVault(ipRoyaltyVault).snapshot(); + IpRoyaltyVault(ipRoyaltyVault3).snapshot(); // Expect 10% (10_000_000) because ipAcct[2] has only one parent (IPAccount1), with 10% absolute royalty. - vm.expectEmit(ipRoyaltyVault); - emit IERC20.Transfer({ from: ipRoyaltyVault, to: ipAcct[2], value: 10_000_000 }); + uint256[] memory snapshotsToClaim = new uint256[](1); + snapshotsToClaim[0] = 1; + royaltyPolicyLAP.claimBySnapshotBatchAsSelf(snapshotsToClaim, address(mockToken), ipAcct[3]); + + vm.expectEmit(ipRoyaltyVault3); + emit IERC20.Transfer({ from: address(royaltyPolicyLAP), to: ipRoyaltyVault2, value: 10_000_000 }); - vm.expectEmit(ipRoyaltyVault); - emit IIpRoyaltyVault.RoyaltyTokensCollected(ipAcct[2], 10_000_000); + vm.expectEmit(address(royaltyPolicyLAP)); + emit IRoyaltyPolicyLAP.RoyaltyTokensCollected(ipAcct[3], ipAcct[2], 10_000_000); - IpRoyaltyVault(ipRoyaltyVault).collectRoyaltyTokens(ipAcct[2]); + royaltyPolicyLAP.collectRoyaltyTokens(ipAcct[3], ipAcct[2]); } // Owner of IPAccount1, Alice, claims her RTs from IPAccount2 and IPAccount3 vaults { - vm.startPrank(u.alice); + vm.startPrank(address(100)); ERC20[] memory tokens = new ERC20[](1); tokens[0] = mockToken; - (, address ipRoyaltyVault2, ) = royaltyPolicyLAP.getRoyaltyData(ipAcct[2]); - (, address ipRoyaltyVault3, ) = royaltyPolicyLAP.getRoyaltyData(ipAcct[3]); + address ipRoyaltyVault1 = royaltyModule.ipRoyaltyVaults(ipAcct[1]); + address ipRoyaltyVault2 = royaltyModule.ipRoyaltyVaults(ipAcct[2]); + address ipRoyaltyVault3 = royaltyModule.ipRoyaltyVaults(ipAcct[3]); vm.warp(block.timestamp + 7 days + 1); IpRoyaltyVault(ipRoyaltyVault2).snapshot(); - IpRoyaltyVault(ipRoyaltyVault3).snapshot(); // IPAccount1 should expect 10% absolute royalty from its children (IPAccount2) // and 20% from its grandchild (IPAccount3) and so on. + uint256[] memory snapshotsToClaim = new uint256[](1); + snapshotsToClaim[0] = 1; + royaltyPolicyLAP.claimBySnapshotBatchAsSelf(snapshotsToClaim, address(mockToken), ipAcct[2]); + vm.expectEmit(ipRoyaltyVault2); - emit IERC20.Transfer({ from: ipRoyaltyVault2, to: ipAcct[1], value: 10_000_000 }); - vm.expectEmit(ipRoyaltyVault2); - emit IIpRoyaltyVault.RoyaltyTokensCollected(ipAcct[1], 10_000_000); - IpRoyaltyVault(ipRoyaltyVault2).collectRoyaltyTokens(ipAcct[1]); + emit IERC20.Transfer({ from: address(royaltyPolicyLAP), to: ipRoyaltyVault1, value: 10_000_000 }); + vm.expectEmit(address(royaltyPolicyLAP)); + emit IRoyaltyPolicyLAP.RoyaltyTokensCollected(ipAcct[2], ipAcct[1], 10_000_000); + royaltyPolicyLAP.collectRoyaltyTokens(ipAcct[2], ipAcct[1]); vm.expectEmit(ipRoyaltyVault3); - emit IERC20.Transfer({ from: ipRoyaltyVault3, to: ipAcct[1], value: 20_000_000 }); - vm.expectEmit(ipRoyaltyVault3); - emit IIpRoyaltyVault.RoyaltyTokensCollected(ipAcct[1], 20_000_000); - IpRoyaltyVault(ipRoyaltyVault3).collectRoyaltyTokens(ipAcct[1]); - } - - // Owner of IPAccount2, Bob, takes snapshot on IPAccount3 vault and claims his revenue from IPAccount3 vault - { - vm.startPrank(u.bob); - - (, address ipRoyaltyVault, ) = royaltyPolicyLAP.getRoyaltyData(ipAcct[3]); - - // take snapshot - vm.warp(block.timestamp + 7 days + 1); - IpRoyaltyVault(ipRoyaltyVault).snapshot(); - - address[] memory tokens = new address[](2); - tokens[0] = address(mockToken); - tokens[1] = address(LINK); - - IpRoyaltyVault(ipRoyaltyVault).claimRevenueByTokenBatch(1, tokens); - - vm.stopPrank(); + emit IERC20.Transfer({ from: address(royaltyPolicyLAP), to: ipRoyaltyVault1, value: 20_000_000 }); + vm.expectEmit(address(royaltyPolicyLAP)); + emit IRoyaltyPolicyLAP.RoyaltyTokensCollected(ipAcct[3], ipAcct[1], 20_000_000); + royaltyPolicyLAP.collectRoyaltyTokens(ipAcct[3], ipAcct[1]); } - // Owner of IPAccount1, Alice, takes snapshot on IPAccount2 vault and claims her revenue from both - // IPAccount2 and IPAccount3 vaults + // Alice using IPAccount1 takes snapshot on IPAccount2 vault and claims her revenue from both + // IPAccount2 and IPAccount3 { - vm.startPrank(u.alice); + vm.startPrank(ipAcct[1]); - (, address ipRoyaltyVault2, ) = royaltyPolicyLAP.getRoyaltyData(ipAcct[2]); - (, address ipRoyaltyVault3, ) = royaltyPolicyLAP.getRoyaltyData(ipAcct[3]); + address ipRoyaltyVault1 = royaltyModule.ipRoyaltyVaults(ipAcct[1]); - address[] memory tokens = new address[](2); + address[] memory tokens = new address[](1); tokens[0] = address(mockToken); - tokens[1] = address(LINK); - IpRoyaltyVault(ipRoyaltyVault3).claimRevenueByTokenBatch(1, tokens); - - // take snapshot - vm.warp(block.timestamp + 7 days + 1); - IpRoyaltyVault(ipRoyaltyVault2).snapshot(); + IpRoyaltyVault(ipRoyaltyVault1).snapshot(); - IpRoyaltyVault(ipRoyaltyVault2).claimRevenueByTokenBatch(1, tokens); + IpRoyaltyVault(ipRoyaltyVault1).claimRevenueByTokenBatch(1, tokens); } } } diff --git a/test/foundry/invariants/DisputeModule.t.sol b/test/foundry/invariants/DisputeModule.t.sol index 01fe4aeb..ed28ea12 100644 --- a/test/foundry/invariants/DisputeModule.t.sol +++ b/test/foundry/invariants/DisputeModule.t.sol @@ -169,7 +169,7 @@ contract DisputeInvariants is BaseTest { parentIpIds[0] = _ipAccount; vm.prank(address(harness)); - mockToken.approve(address(royaltyPolicyLAP), type(uint256).max); + mockToken.approve(address(royaltyModule), type(uint256).max); vm.prank(address(harness)); licensingModule.attachLicenseTerms(_ipAccount, address(pilTemplate), commRemixTermsId); @@ -183,7 +183,7 @@ contract DisputeInvariants is BaseTest { royaltyContext: "" }); - targetContract(address(harness)); + /* targetContract(address(harness)); bytes4[] memory selectors = new bytes4[](5); selectors[0] = harness.raiseDispute.selector; @@ -191,7 +191,7 @@ contract DisputeInvariants is BaseTest { selectors[2] = harness.cancelDispute.selector; selectors[3] = harness.tagDerivativeIfParentInfringed.selector; selectors[4] = harness.resolveDispute.selector; - targetSelector(FuzzSelector(address(harness), selectors)); + targetSelector(FuzzSelector(address(harness), selectors)); */ } /// @notice Invariant to check dispute id should be equal to counter diff --git a/test/foundry/invariants/IpRoyaltyVault.t.sol b/test/foundry/invariants/IpRoyaltyVault.t.sol index 9931c8b1..9a2e30de 100644 --- a/test/foundry/invariants/IpRoyaltyVault.t.sol +++ b/test/foundry/invariants/IpRoyaltyVault.t.sol @@ -32,8 +32,17 @@ contract IpRoyaltyVaultHarness is Test { vault.claimRevenueBySnapshotBatch(snapshotIds, token); } - function collectRoyaltyTokens(address ancestorIpId) public { - vault.collectRoyaltyTokens(ancestorIpId); + function claimByTokenBatchAsSelf(uint256 snapshotId, address[] calldata tokenList, address targetIpId) public { + vault.claimByTokenBatchAsSelf(snapshotId, tokenList, targetIpId); + } + + function claimBySnapshotBatchAsSelf(uint256[] memory snapshotIds, address token, address targetIpId) public { + vault.claimBySnapshotBatchAsSelf(snapshotIds, token, targetIpId); + } + + function addIpRoyaltyVaultTokens(address token) public { + vm.startPrank(address(royaltyModule)); + vault.addIpRoyaltyVaultTokens(token); } function warp() public { @@ -53,15 +62,15 @@ contract IpRoyaltyVaultInvariant is BaseTest { // whitelist royalty policy royaltyModule.whitelistRoyaltyPolicy(address(royaltyPolicyLAP), true); royaltyModule.whitelistRoyaltyToken(address(LINK), true); - royaltyPolicyLAP.setSnapshotInterval(7 days); + royaltyModule.setSnapshotInterval(7 days); vm.stopPrank(); - vm.startPrank(address(royaltyModule)); - _setupMaxUniqueTree(); + vm.startPrank(address(licensingModule)); + _setupTree(); vm.stopPrank(); - (, address IpRoyaltyVault2, ) = royaltyPolicyLAP.getRoyaltyData(address(2)); - ipRoyaltyVault = IpRoyaltyVault(IpRoyaltyVault2); + address vault = royaltyModule.ipRoyaltyVaults(address(50)); + ipRoyaltyVault = IpRoyaltyVault(vault); harness = new IpRoyaltyVaultHarness(address(ipRoyaltyVault), address(royaltyModule)); @@ -69,118 +78,68 @@ contract IpRoyaltyVaultInvariant is BaseTest { targetContract(address(harness)); - bytes4[] memory selectors = new bytes4[](6); + bytes4[] memory selectors = new bytes4[](8); selectors[0] = harness.snapshot.selector; selectors[1] = harness.claimRevenueByTokenBatch.selector; selectors[2] = harness.claimRevenueBySnapshotBatch.selector; - selectors[3] = harness.collectRoyaltyTokens.selector; - selectors[4] = harness.warp.selector; - selectors[5] = harness.payRoyaltyOnBehalf.selector; + selectors[3] = harness.payRoyaltyOnBehalf.selector; + selectors[4] = harness.claimByTokenBatchAsSelf.selector; + selectors[5] = harness.claimBySnapshotBatchAsSelf.selector; + selectors[6] = harness.addIpRoyaltyVaultTokens.selector; + selectors[7] = harness.warp.selector; targetSelector(FuzzSelector(address(harness), selectors)); ipId = ipRoyaltyVault.ipId(); } - function _setupMaxUniqueTree() internal { - // init royalty policy for roots - royaltyPolicyLAP.onLicenseMinting(address(7), abi.encode(uint32(7)), ""); - royaltyPolicyLAP.onLicenseMinting(address(8), abi.encode(uint32(8)), ""); - royaltyPolicyLAP.onLicenseMinting(address(9), abi.encode(uint32(9)), ""); - royaltyPolicyLAP.onLicenseMinting(address(10), abi.encode(uint32(10)), ""); - royaltyPolicyLAP.onLicenseMinting(address(11), abi.encode(uint32(11)), ""); - royaltyPolicyLAP.onLicenseMinting(address(12), abi.encode(uint32(12)), ""); - royaltyPolicyLAP.onLicenseMinting(address(13), abi.encode(uint32(13)), ""); - royaltyPolicyLAP.onLicenseMinting(address(14), abi.encode(uint32(14)), ""); - - // init 2nd level with children - address[] memory parents = new address[](2); - uint32[] memory parentRoyalties1 = new uint32[](2); - bytes[] memory encodedLicenseData = new bytes[](2); - - // 100 is child of 7 and 8 - parents[0] = address(7); - parents[1] = address(8); - parentRoyalties1[0] = 7 * 10 ** 5; - parentRoyalties1[1] = 8 * 10 ** 5; - - for (uint32 i = 0; i < parentRoyalties1.length; i++) { - encodedLicenseData[i] = abi.encode(parentRoyalties1[i]); - } - royaltyPolicyLAP.onLinkToParents(address(100), parents, encodedLicenseData, ""); - - // 4 is child of 9 and 10 - parents[0] = address(9); - parents[1] = address(10); - parentRoyalties1[0] = 9 * 10 ** 5; - parentRoyalties1[1] = 10 * 10 ** 5; - - for (uint32 i = 0; i < parentRoyalties1.length; i++) { - encodedLicenseData[i] = abi.encode(parentRoyalties1[i]); - } - royaltyPolicyLAP.onLinkToParents(address(4), parents, encodedLicenseData, ""); - - // 5 is child of 11 and 12 - parents[0] = address(11); - parents[1] = address(12); - parentRoyalties1[0] = 11 * 10 ** 5; - parentRoyalties1[1] = 12 * 10 ** 5; - - for (uint32 i = 0; i < parentRoyalties1.length; i++) { - encodedLicenseData[i] = abi.encode(parentRoyalties1[i]); - } - royaltyPolicyLAP.onLinkToParents(address(5), parents, encodedLicenseData, ""); - - // 6 is child of 13 and 14 - parents[0] = address(13); - parents[1] = address(14); - parentRoyalties1[0] = 13 * 10 ** 5; - parentRoyalties1[1] = 14 * 10 ** 5; - - for (uint32 i = 0; i < parentRoyalties1.length; i++) { - encodedLicenseData[i] = abi.encode(parentRoyalties1[i]); - } - royaltyPolicyLAP.onLinkToParents(address(6), parents, encodedLicenseData, ""); - - // init 3rd level with children - // 1 is child of 100 and 4 - parents[0] = address(100); - parents[1] = address(4); - parentRoyalties1[0] = 3 * 10 ** 5; - parentRoyalties1[1] = 4 * 10 ** 5; - - for (uint32 i = 0; i < parentRoyalties1.length; i++) { - encodedLicenseData[i] = abi.encode(parentRoyalties1[i]); - } - royaltyPolicyLAP.onLinkToParents(address(1), parents, encodedLicenseData, ""); - - // 2 is child of 5 and 6 - parents[0] = address(5); - parents[1] = address(6); - parentRoyalties1[0] = 5 * 10 ** 5; - parentRoyalties1[1] = 6 * 10 ** 5; - - for (uint32 i = 0; i < parentRoyalties1.length; i++) { - encodedLicenseData[i] = abi.encode(parentRoyalties1[i]); - } - royaltyPolicyLAP.onLinkToParents(address(2), parents, encodedLicenseData, ""); - - address[] memory parentsIpIds100 = new address[](2); - parentsIpIds100 = new address[](2); - parentsIpIds100[0] = address(1); - parentsIpIds100[1] = address(2); - - parents[0] = address(1); - parents[1] = address(2); - parentRoyalties1[0] = 1 * 10 ** 5; - parentRoyalties1[1] = 2 * 10 ** 5; - - for (uint32 i = 0; i < parentRoyalties1.length; i++) { - encodedLicenseData[i] = abi.encode(parentRoyalties1[i]); - } - - vm.startPrank(address(licensingModule)); - royaltyModule.onLinkToParents(address(3), address(royaltyPolicyLAP), parents, encodedLicenseData, ""); + function _setupTree() internal { + // mint license for roots + royaltyModule.onLicenseMinting(address(10), address(royaltyPolicyLAP), uint32(10 * 10 ** 6), ""); + royaltyModule.onLicenseMinting(address(20), address(royaltyPolicyLAP), uint32(10 * 10 ** 6), ""); + royaltyModule.onLicenseMinting(address(30), address(royaltyPolicyLRP), uint32(7 * 10 ** 6), ""); + + // link 40 to parents + address[] memory parents = new address[](3); + address[] memory licenseRoyaltyPolicies = new address[](3); + uint32[] memory parentRoyalties = new uint32[](3); + parents[0] = address(10); + parents[1] = address(20); + parents[2] = address(30); + licenseRoyaltyPolicies[0] = address(royaltyPolicyLAP); + licenseRoyaltyPolicies[1] = address(royaltyPolicyLAP); + licenseRoyaltyPolicies[2] = address(royaltyPolicyLRP); + parentRoyalties[0] = uint32(10 * 10 ** 6); + parentRoyalties[1] = uint32(10 * 10 ** 6); + parentRoyalties[2] = uint32(7 * 10 ** 6); + ipGraph.addParentIp(address(40), parents); + royaltyModule.onLinkToParents(address(40), parents, licenseRoyaltyPolicies, parentRoyalties, ""); + + // mint license for 40 + royaltyModule.onLicenseMinting(address(40), address(royaltyPolicyLRP), uint32(5 * 10 ** 6), ""); + + // link 50 to 40 + parents = new address[](1); + licenseRoyaltyPolicies = new address[](1); + parentRoyalties = new uint32[](1); + parents[0] = address(40); + licenseRoyaltyPolicies[0] = address(royaltyPolicyLRP); + parentRoyalties[0] = uint32(5 * 10 ** 6); + ipGraph.addParentIp(address(50), parents); + royaltyModule.onLinkToParents(address(50), parents, licenseRoyaltyPolicies, parentRoyalties, ""); + + // mint license for 50 + royaltyModule.onLicenseMinting(address(50), address(royaltyPolicyLRP), uint32(15 * 10 ** 6), ""); + + // link 60 to 50 + parents = new address[](1); + licenseRoyaltyPolicies = new address[](1); + parentRoyalties = new uint32[](1); + parents[0] = address(50); + licenseRoyaltyPolicies[0] = address(royaltyPolicyLRP); + parentRoyalties[0] = uint32(15 * 10 ** 6); + ipGraph.addParentIp(address(60), parents); + royaltyModule.onLinkToParents(address(60), parents, licenseRoyaltyPolicies, parentRoyalties, ""); } /// @notice Invariant to check anyone's balance should be <= init (1000 * 10 ** 6) diff --git a/test/foundry/mocks/module/MockLicenseTemplate.sol b/test/foundry/mocks/module/MockLicenseTemplate.sol index 430fe259..93456e5e 100644 --- a/test/foundry/mocks/module/MockLicenseTemplate.sol +++ b/test/foundry/mocks/module/MockLicenseTemplate.sol @@ -86,14 +86,14 @@ contract MockLicenseTemplate is BaseLicenseTemplateUpgradeable { /// @notice Returns the royalty policy of a license terms. /// @param licenseTermsId The ID of the license terms. /// @return royaltyPolicy The address of the royalty policy specified for the license terms. - /// @return royaltyData The data of the royalty policy. + /// @return royaltyPercent The data of the royalty policy. /// @return mintingFee The fee for minting a license. /// @return currency The address of the ERC20 token, used for minting license fee and royalties. /// the currency token will used for pay for license token minting fee and royalties. function getRoyaltyPolicy( uint256 licenseTermsId - ) external view returns (address royaltyPolicy, bytes memory royaltyData, uint256 mintingFee, address currency) { - return (address(0), "", 0, address(0)); + ) external view returns (address royaltyPolicy, uint32 royaltyPercent, uint256 mintingFee, address currency) { + return (address(0), 0, 0, address(0)); } /// @notice Checks if a license terms is transferable. diff --git a/test/foundry/mocks/policy/MockExternalRoyaltyPolicy1.sol b/test/foundry/mocks/policy/MockExternalRoyaltyPolicy1.sol new file mode 100644 index 00000000..f953eaaa --- /dev/null +++ b/test/foundry/mocks/policy/MockExternalRoyaltyPolicy1.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.23; + +// solhint-disable-next-line max-line-length +import { IExternalRoyaltyPolicy } from "../../../../contracts/interfaces/modules/royalty/policies/IExternalRoyaltyPolicy.sol"; + +contract MockExternalRoyaltyPolicy1 is IExternalRoyaltyPolicy { + function rtsRequiredToLink(address ipId, uint32 licensePercent) external view returns (uint32) { + return licensePercent * 2; + } +} diff --git a/test/foundry/mocks/policy/MockExternalRoyaltyPolicy2.sol b/test/foundry/mocks/policy/MockExternalRoyaltyPolicy2.sol new file mode 100644 index 00000000..ba4fa9a6 --- /dev/null +++ b/test/foundry/mocks/policy/MockExternalRoyaltyPolicy2.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.23; + +// solhint-disable-next-line max-line-length +import { IExternalRoyaltyPolicy } from "../../../../contracts/interfaces/modules/royalty/policies/IExternalRoyaltyPolicy.sol"; + +contract MockExternalRoyaltyPolicy2 is IExternalRoyaltyPolicy { + function rtsRequiredToLink(address ipId, uint32 licensePercent) external view returns (uint32) { + return 10 * 10 ** 6; + } +} diff --git a/test/foundry/mocks/policy/MockRoyaltyPolicyLAP.sol b/test/foundry/mocks/policy/MockRoyaltyPolicyLAP.sol index 9704651b..9a1f47c6 100644 --- a/test/foundry/mocks/policy/MockRoyaltyPolicyLAP.sol +++ b/test/foundry/mocks/policy/MockRoyaltyPolicyLAP.sol @@ -1,62 +1,49 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.23; -import { IRoyaltyPolicyLAP } from "../../../../contracts/interfaces/modules/royalty/policies/IRoyaltyPolicyLAP.sol"; +import { IRoyaltyModule } from "../../../../contracts/interfaces/modules/royalty/IRoyaltyModule.sol"; +import { IDisputeModule } from "../../../../contracts/interfaces/modules/dispute/IDisputeModule.sol"; +import { IRoyaltyPolicyLAP } from "../../../../contracts/interfaces/modules/royalty/policies/LAP/IRoyaltyPolicyLAP.sol"; contract MockRoyaltyPolicyLAP is IRoyaltyPolicyLAP { struct RoyaltyPolicyLAPStorage { - address ipRoyaltyVaultBeacon; - uint256 snapshotInterval; - mapping(address ipId => IRoyaltyPolicyLAP.LAPRoyaltyData) royaltyData; + mapping(address ipId => uint32) royaltyStack; } bytes32 private constant RoyaltyPolicyLAPStorageLocation = 0x0c915ba68e2c4e37f19454bb13066f18f9db418fcefbf3c585b4b7d0fb0e0600; - uint32 public constant TOTAL_RT_SUPPLY = 100000000; uint256 public constant MAX_PARENTS = 100; uint256 public constant MAX_ANCESTORS = 100; address public constant LICENSING_MODULE = address(0); - address public constant ROYALTY_MODULE = address(0); + address public constant IP_GRAPH = address(0); + IRoyaltyModule public constant ROYALTY_MODULE = IRoyaltyModule(address(0)); + IDisputeModule public constant DISPUTE_MODULE = IDisputeModule(address(0)); constructor() {} - function setSnapshotInterval(uint256 timestampInterval) public { - RoyaltyPolicyLAPStorage storage $ = _getRoyaltyPolicyLAPStorage(); - $.snapshotInterval = timestampInterval; - } - - function setIpRoyaltyVaultBeacon(address beacon) public { - RoyaltyPolicyLAPStorage storage $ = _getRoyaltyPolicyLAPStorage(); - $.ipRoyaltyVaultBeacon = beacon; - } - - function onLicenseMinting(address ipId, bytes calldata licenseData, bytes calldata externalData) external {} + function onLicenseMinting(address ipId, uint32 licensePercent, bytes calldata externalData) external {} function onLinkToParents( address ipId, address[] calldata parentIpIds, - bytes[] memory licenseData, + address[] calldata ancestorsRules, + uint32[] memory licensesPercent, bytes calldata externalData ) external {} function onRoyaltyPayment(address caller, address ipId, address token, uint256 amount) external {} - - function getRoyaltyData(address ipId) external view returns (bool, address, uint32) { - RoyaltyPolicyLAPStorage storage $ = _getRoyaltyPolicyLAPStorage(); - IRoyaltyPolicyLAP.LAPRoyaltyData memory data = $.royaltyData[ipId]; - return (data.isUnlinkableToParents, data.ipRoyaltyVault, data.royaltyStack); - } - - function getSnapshotInterval() external view returns (uint256) { - RoyaltyPolicyLAPStorage storage $ = _getRoyaltyPolicyLAPStorage(); - return $.snapshotInterval; - } - - function getIpRoyaltyVaultBeacon() external view returns (address) { - RoyaltyPolicyLAPStorage storage $ = _getRoyaltyPolicyLAPStorage(); - return $.ipRoyaltyVaultBeacon; + function rtsRequiredToLink(address ipId, uint32 licensePercent) external view returns (uint32) {} + function royaltyStack(address ipId) external view returns (uint32) { + return _getRoyaltyPolicyLAPStorage().royaltyStack[ipId]; } + function collectRoyaltyTokens(address ipId, address ancestorIpId) external {} + function claimBySnapshotBatchAsSelf(uint256[] memory snapshotIds, address token, address targetIpId) external {} + function unclaimedRoyaltyTokens(address ipId) external view returns (uint32) {} + function isCollectedByAncestor(address ipId, address ancestorIpId) external view returns (bool) {} + function revenueTokenBalances(address ipId, address token) external view returns (uint256) {} + function snapshotsClaimed(address ipId, address token, uint256 snapshot) external view returns (bool) {} + function snapshotsClaimedCounter(address ipId, address token) external view returns (uint256) {} function _getRoyaltyPolicyLAPStorage() private pure returns (RoyaltyPolicyLAPStorage storage $) { assembly { diff --git a/test/foundry/modules/dispute/DisputeModule.t.sol b/test/foundry/modules/dispute/DisputeModule.t.sol index 2ee788c7..e583de3d 100644 --- a/test/foundry/modules/dispute/DisputeModule.t.sol +++ b/test/foundry/modules/dispute/DisputeModule.t.sol @@ -94,7 +94,7 @@ contract DisputeModuleTest is BaseTest { vm.startPrank(u.bob); uint256 mintAmount = 3; - erc20.approve(address(royaltyPolicyLAP), type(uint256).max); + erc20.approve(address(royaltyModule), type(uint256).max); uint256[] memory licenseIds = new uint256[](1); diff --git a/test/foundry/modules/licensing/LicensingModule.t.sol b/test/foundry/modules/licensing/LicensingModule.t.sol index db52c9b9..7243df39 100644 --- a/test/foundry/modules/licensing/LicensingModule.t.sol +++ b/test/foundry/modules/licensing/LicensingModule.t.sol @@ -1577,7 +1577,7 @@ contract LicensingModuleTest is BaseTest { vm.startPrank(minter); erc20.mint(minter, 1000); - erc20.approve(address(royaltyPolicyLAP), 100); + erc20.approve(address(royaltyModule), 100); address receiver = address(0x111); vm.expectEmit(); @@ -1624,7 +1624,7 @@ contract LicensingModuleTest is BaseTest { vm.startPrank(ipOwner2); erc20.mint(ipOwner2, 1000); - erc20.approve(address(royaltyPolicyLAP), 100); + erc20.approve(address(royaltyModule), 100); address[] memory parentIpIds = new address[](1); uint256[] memory licenseTermsIds = new uint256[](1); @@ -1676,7 +1676,7 @@ contract LicensingModuleTest is BaseTest { vm.startPrank(ipOwner2); erc20.mint(ipOwner2, 1000); - erc20.approve(address(royaltyPolicyLAP), 100); + erc20.approve(address(royaltyModule), 100); address[] memory parentIpIds = new address[](1); uint256[] memory licenseTermsIds = new uint256[](1); @@ -1711,7 +1711,7 @@ contract LicensingModuleTest is BaseTest { licensingModule.setLicensingConfig(ipId1, address(pilTemplate), termsId, licensingConfig2); vm.startPrank(ipOwner2); - erc20.approve(address(royaltyPolicyLAP), 300); + erc20.approve(address(royaltyModule), 300); uint256 licenseTokenId = licensingModule.mintLicenseTokens({ licensorIpId: ipId1, licenseTemplate: address(pilTemplate), diff --git a/test/foundry/modules/licensing/PILicenseTemplate.t.sol b/test/foundry/modules/licensing/PILicenseTemplate.t.sol index b4d7fb75..31ab61de 100644 --- a/test/foundry/modules/licensing/PILicenseTemplate.t.sol +++ b/test/foundry/modules/licensing/PILicenseTemplate.t.sol @@ -54,10 +54,10 @@ contract PILicenseTemplateTest is BaseTest { function test_PILicenseTemplate_registerLicenseTerms() public { uint256 defaultTermsId = pilTemplate.registerLicenseTerms(PILFlavors.defaultValuesLicenseTerms()); assertEq(defaultTermsId, 1); - (address royaltyPolicy, bytes memory royaltyData, uint256 mintingFee, address currency) = pilTemplate + (address royaltyPolicy, uint32 royaltyPercent, uint256 mintingFee, address currency) = pilTemplate .getRoyaltyPolicy(defaultTermsId); assertEq(royaltyPolicy, address(0), "royaltyPolicy should be address(0)"); - assertEq(royaltyData, abi.encode(0), "royaltyData should be empty"); + assertEq(royaltyPercent, 0, "royaltyPercent should be empty"); assertEq(mintingFee, 0, "mintingFee should be 0"); assertEq(currency, address(0), "currency should be address(0)"); assertTrue(pilTemplate.isLicenseTransferable(defaultTermsId), "license should be transferable"); @@ -67,9 +67,9 @@ contract PILicenseTemplateTest is BaseTest { uint256 socialRemixTermsId = pilTemplate.registerLicenseTerms(PILFlavors.nonCommercialSocialRemixing()); assertEq(socialRemixTermsId, 2); - (royaltyPolicy, royaltyData, mintingFee, currency) = pilTemplate.getRoyaltyPolicy(socialRemixTermsId); + (royaltyPolicy, royaltyPercent, mintingFee, currency) = pilTemplate.getRoyaltyPolicy(socialRemixTermsId); assertEq(royaltyPolicy, address(0)); - assertEq(royaltyData, abi.encode(0)); + assertEq(royaltyPercent, 0); assertEq(mintingFee, 0); assertEq(currency, address(0)); assertTrue(pilTemplate.isLicenseTransferable(socialRemixTermsId)); @@ -85,9 +85,9 @@ contract PILicenseTemplateTest is BaseTest { }) ); assertEq(commUseTermsId, 3); - (royaltyPolicy, royaltyData, mintingFee, currency) = pilTemplate.getRoyaltyPolicy(commUseTermsId); + (royaltyPolicy, royaltyPercent, mintingFee, currency) = pilTemplate.getRoyaltyPolicy(commUseTermsId); assertEq(royaltyPolicy, address(royaltyPolicyLAP)); - assertEq(royaltyData, abi.encode(0)); + assertEq(royaltyPercent, 0); assertEq(mintingFee, 100); assertEq(currency, address(erc20)); assertTrue(pilTemplate.isLicenseTransferable(commUseTermsId)); @@ -107,9 +107,9 @@ contract PILicenseTemplateTest is BaseTest { }) ); assertEq(commRemixTermsId, 4); - (royaltyPolicy, royaltyData, mintingFee, currency) = pilTemplate.getRoyaltyPolicy(commRemixTermsId); + (royaltyPolicy, royaltyPercent, mintingFee, currency) = pilTemplate.getRoyaltyPolicy(commRemixTermsId); assertEq(royaltyPolicy, address(royaltyPolicyLAP)); - assertEq(royaltyData, abi.encode(10)); + assertEq(royaltyPercent, 10); assertEq(mintingFee, 100); assertEq(currency, address(erc20)); assertTrue(pilTemplate.isLicenseTransferable(commRemixTermsId)); diff --git a/test/foundry/modules/royalty/IpRoyaltyVault.t.sol b/test/foundry/modules/royalty/IpRoyaltyVault.t.sol index efd724d0..54a5f5e6 100644 --- a/test/foundry/modules/royalty/IpRoyaltyVault.t.sol +++ b/test/foundry/modules/royalty/IpRoyaltyVault.t.sol @@ -1,205 +1,291 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.23; +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +// contracts import { IpRoyaltyVault } from "../../../../contracts/modules/royalty/policies/IpRoyaltyVault.sol"; +// solhint-disable-next-line max-line-length import { IIpRoyaltyVault } from "../../../../contracts/interfaces/modules/royalty/policies/IIpRoyaltyVault.sol"; - import { Errors } from "../../../../contracts/lib/Errors.sol"; +// tests import { BaseTest } from "../../utils/BaseTest.t.sol"; contract TestIpRoyaltyVault is BaseTest { - IpRoyaltyVault private ipRoyaltyVault; - function setUp() public override { super.setUp(); vm.startPrank(u.admin); - // whitelist royalty policy - royaltyModule.whitelistRoyaltyPolicy(address(royaltyPolicyLAP), true); royaltyModule.whitelistRoyaltyToken(address(LINK), true); - royaltyPolicyLAP.setSnapshotInterval(7 days); vm.stopPrank(); + } - vm.startPrank(address(royaltyModule)); - _setupMaxUniqueTree(); + function test_IpRoyaltyVault_decimals() public { + // deploy vault + vm.startPrank(address(licensingModule)); + royaltyModule.onLicenseMinting(address(2), address(royaltyPolicyLAP), uint32(10 * 10 ** 6), ""); + IpRoyaltyVault ipRoyaltyVault = IpRoyaltyVault(royaltyModule.ipRoyaltyVaults(address(2))); + vm.stopPrank(); - (, address IpRoyaltyVault2, ) = royaltyPolicyLAP.getRoyaltyData(address(2)); - ipRoyaltyVault = IpRoyaltyVault(IpRoyaltyVault2); + assertEq(ipRoyaltyVault.decimals(), 6); } - function _setupMaxUniqueTree() internal { - // init royalty policy for roots - royaltyPolicyLAP.onLicenseMinting(address(7), abi.encode(uint32(7)), ""); - royaltyPolicyLAP.onLicenseMinting(address(8), abi.encode(uint32(8)), ""); - royaltyPolicyLAP.onLicenseMinting(address(9), abi.encode(uint32(9)), ""); - royaltyPolicyLAP.onLicenseMinting(address(10), abi.encode(uint32(10)), ""); - royaltyPolicyLAP.onLicenseMinting(address(11), abi.encode(uint32(11)), ""); - royaltyPolicyLAP.onLicenseMinting(address(12), abi.encode(uint32(12)), ""); - royaltyPolicyLAP.onLicenseMinting(address(13), abi.encode(uint32(13)), ""); - royaltyPolicyLAP.onLicenseMinting(address(14), abi.encode(uint32(14)), ""); - - // init 2nd level with children - address[] memory parents = new address[](2); - uint32[] memory parentRoyalties1 = new uint32[](2); - bytes[] memory encodedLicenseData = new bytes[](2); - - // 100 is child of 7 and 8 - parents[0] = address(7); - parents[1] = address(8); - parentRoyalties1[0] = 7 * 10 ** 5; - parentRoyalties1[1] = 8 * 10 ** 5; - - for (uint32 i = 0; i < parentRoyalties1.length; i++) { - encodedLicenseData[i] = abi.encode(parentRoyalties1[i]); - } - ipGraph.addParentIp(address(100), parents); - royaltyPolicyLAP.onLinkToParents(address(100), parents, encodedLicenseData, ""); - - // 4 is child of 9 and 10 - parents[0] = address(9); - parents[1] = address(10); - parentRoyalties1[0] = 9 * 10 ** 5; - parentRoyalties1[1] = 10 * 10 ** 5; - - for (uint32 i = 0; i < parentRoyalties1.length; i++) { - encodedLicenseData[i] = abi.encode(parentRoyalties1[i]); - } - ipGraph.addParentIp(address(4), parents); - royaltyPolicyLAP.onLinkToParents(address(4), parents, encodedLicenseData, ""); - - // 5 is child of 11 and 12 - parents[0] = address(11); - parents[1] = address(12); - parentRoyalties1[0] = 11 * 10 ** 5; - parentRoyalties1[1] = 12 * 10 ** 5; - - for (uint32 i = 0; i < parentRoyalties1.length; i++) { - encodedLicenseData[i] = abi.encode(parentRoyalties1[i]); - } - ipGraph.addParentIp(address(5), parents); - royaltyPolicyLAP.onLinkToParents(address(5), parents, encodedLicenseData, ""); - - // 6 is child of 13 and 14 - parents[0] = address(13); - parents[1] = address(14); - parentRoyalties1[0] = 13 * 10 ** 5; - parentRoyalties1[1] = 14 * 10 ** 5; - - for (uint32 i = 0; i < parentRoyalties1.length; i++) { - encodedLicenseData[i] = abi.encode(parentRoyalties1[i]); - } - ipGraph.addParentIp(address(6), parents); - royaltyPolicyLAP.onLinkToParents(address(6), parents, encodedLicenseData, ""); - - // init 3rd level with children - // 1 is child of 100 and 4 - parents[0] = address(100); - parents[1] = address(4); - parentRoyalties1[0] = 3 * 10 ** 5; - parentRoyalties1[1] = 4 * 10 ** 5; - - for (uint32 i = 0; i < parentRoyalties1.length; i++) { - encodedLicenseData[i] = abi.encode(parentRoyalties1[i]); - } - ipGraph.addParentIp(address(1), parents); - royaltyPolicyLAP.onLinkToParents(address(1), parents, encodedLicenseData, ""); - - // 2 is child of 5 and 6 - parents[0] = address(5); - parents[1] = address(6); - parentRoyalties1[0] = 5 * 10 ** 5; - parentRoyalties1[1] = 6 * 10 ** 5; - - for (uint32 i = 0; i < parentRoyalties1.length; i++) { - encodedLicenseData[i] = abi.encode(parentRoyalties1[i]); - } - ipGraph.addParentIp(address(2), parents); - royaltyPolicyLAP.onLinkToParents(address(2), parents, encodedLicenseData, ""); - - address[] memory parentsIpIds100 = new address[](2); - parentsIpIds100 = new address[](2); - parentsIpIds100[0] = address(1); - parentsIpIds100[1] = address(2); - - parents[0] = address(1); - parents[1] = address(2); - parentRoyalties1[0] = 1 * 10 ** 5; - parentRoyalties1[1] = 2 * 10 ** 5; - - for (uint32 i = 0; i < parentRoyalties1.length; i++) { - encodedLicenseData[i] = abi.encode(parentRoyalties1[i]); - } - ipGraph.addParentIp(address(3), parents); + function test_IpRoyaltyVault_addIpRoyaltyVaultTokens_revert_NotRoyaltyModule() public { + // deploy vault vm.startPrank(address(licensingModule)); - royaltyModule.onLinkToParents(address(3), address(royaltyPolicyLAP), parents, encodedLicenseData, ""); + royaltyModule.onLicenseMinting(address(1), address(royaltyPolicyLAP), uint32(10 * 10 ** 6), ""); + IpRoyaltyVault ipRoyaltyVault = IpRoyaltyVault(royaltyModule.ipRoyaltyVaults(address(1))); + vm.stopPrank(); + + vm.expectRevert(Errors.IpRoyaltyVault__NotAllowedToAddTokenToVault.selector); + ipRoyaltyVault.addIpRoyaltyVaultTokens(address(USDC)); } - function test_IpRoyaltyVault_AddIpRoyaltyVaultTokens_NotRoyaltyPolicyLAP() public { - vm.expectRevert(Errors.IpRoyaltyVault__NotRoyaltyPolicyLAP.selector); + function test_IpRoyaltyVault_addIpRoyaltyVaultTokens_revert_NotWhitelistedRoyaltyToken() public { + // deploy vault + vm.startPrank(address(licensingModule)); + royaltyModule.onLicenseMinting(address(1), address(royaltyPolicyLAP), uint32(10 * 10 ** 6), ""); + IpRoyaltyVault ipRoyaltyVault = IpRoyaltyVault(royaltyModule.ipRoyaltyVaults(address(1))); + vm.stopPrank(); + + vm.startPrank(address(royaltyModule)); + vm.expectRevert(Errors.IpRoyaltyVault__NotWhitelistedRoyaltyToken.selector); ipRoyaltyVault.addIpRoyaltyVaultTokens(address(0)); + vm.stopPrank(); + } + + function test_IpRoyaltyVault_addIpRoyaltyVaultTokens() public { + // deploy vault + vm.startPrank(address(licensingModule)); + royaltyModule.onLicenseMinting(address(1), address(royaltyPolicyLAP), uint32(10 * 10 ** 6), ""); + IpRoyaltyVault ipRoyaltyVault = IpRoyaltyVault(royaltyModule.ipRoyaltyVaults(address(1))); + vm.stopPrank(); + + vm.startPrank(address(royaltyModule)); + + vm.expectEmit(true, true, true, true, address(ipRoyaltyVault)); + emit IIpRoyaltyVault.RevenueTokenAddedToVault(address(USDC), address(ipRoyaltyVault)); + + ipRoyaltyVault.addIpRoyaltyVaultTokens(address(USDC)); + vm.stopPrank(); + + assertEq(ipRoyaltyVault.tokens().length, 1); + assertEq(ipRoyaltyVault.tokens()[0], address(USDC)); + } + + function test_IpRoyaltyVault_constructor_revert_ZeroDisputeModule() public { + vm.expectRevert(Errors.IpRoyaltyVault__ZeroDisputeModule.selector); + IpRoyaltyVault vault = new IpRoyaltyVault(address(0), address(royaltyModule)); + } + + function test_IpRoyaltyVault_constructor_revert_ZeroRoyaltyModule() public { + vm.expectRevert(Errors.IpRoyaltyVault__ZeroRoyaltyModule.selector); + IpRoyaltyVault vault = new IpRoyaltyVault(address(disputeModule), address(0)); + } + + function test_IpRoyaltyVault_constructor() public { + IpRoyaltyVault vault = new IpRoyaltyVault(address(disputeModule), address(royaltyModule)); + assertEq(address(vault.DISPUTE_MODULE()), address(disputeModule)); + assertEq(address(vault.ROYALTY_MODULE()), address(royaltyModule)); + } + + function test_IpRoyaltyVault_initialize() public { + // mint license for IP80 + vm.startPrank(address(licensingModule)); + royaltyModule.onLicenseMinting(address(80), address(royaltyPolicyLRP), uint32(10 * 10 ** 6), ""); + + address ipRoyaltyVault = royaltyModule.ipRoyaltyVaults(address(80)); + + uint256 ipId80IpIdBalance = IERC20(ipRoyaltyVault).balanceOf(address(80)); + + assertEq(ERC20(ipRoyaltyVault).name(), "Royalty Token"); + assertEq(ERC20(ipRoyaltyVault).symbol(), "RT"); + assertEq(ERC20(ipRoyaltyVault).totalSupply(), royaltyModule.TOTAL_RT_SUPPLY()); + assertEq(IIpRoyaltyVault(ipRoyaltyVault).ipId(), address(80)); + assertEq(IIpRoyaltyVault(ipRoyaltyVault).lastSnapshotTimestamp(), block.timestamp); + assertEq(ipId80IpIdBalance, royaltyModule.TOTAL_RT_SUPPLY()); + } + + function test_IpRoyaltyVault_snapshot_InsufficientTimeElapsedSinceLastSnapshot() public { + // deploy vault + vm.startPrank(address(licensingModule)); + royaltyModule.onLicenseMinting(address(1), address(royaltyPolicyLAP), uint32(10 * 10 ** 6), ""); + IIpRoyaltyVault ipRoyaltyVault = IIpRoyaltyVault(royaltyModule.ipRoyaltyVaults(address(1))); + vm.stopPrank(); + + vm.expectRevert(Errors.IpRoyaltyVault__InsufficientTimeElapsedSinceLastSnapshot.selector); + ipRoyaltyVault.snapshot(); + } + + function test_IpRoyaltyVault_snapshot_revert_Paused() public { + // deploy vault + vm.startPrank(address(licensingModule)); + royaltyModule.onLicenseMinting(address(1), address(royaltyPolicyLAP), uint32(10 * 10 ** 6), ""); + IIpRoyaltyVault ipRoyaltyVault = IIpRoyaltyVault(royaltyModule.ipRoyaltyVaults(address(1))); + vm.stopPrank(); + + vm.stopPrank(); + vm.prank(u.admin); + royaltyModule.pause(); + + vm.expectRevert(Errors.IpRoyaltyVault__EnforcedPause.selector); + ipRoyaltyVault.snapshot(); } - function test_IpRoyaltyVault_AddIpRoyaltyVaultTokens() public { - vm.startPrank(address(royaltyPolicyLAP)); - ipRoyaltyVault.addIpRoyaltyVaultTokens(address(1)); + function test_IpRoyaltyVault_snapshot_revert_NoNewRevenueSinceLastSnapshot() public { + // deploy vault + vm.startPrank(address(licensingModule)); + royaltyModule.onLicenseMinting(address(2), address(royaltyPolicyLAP), uint32(10 * 10 ** 6), ""); + IpRoyaltyVault ipRoyaltyVault = IpRoyaltyVault(royaltyModule.ipRoyaltyVaults(address(2))); + vm.stopPrank(); - address[] memory tokens = ipRoyaltyVault.tokens(); + vm.warp(block.timestamp + 7 days + 1); - assertEq(tokens.length, 1); - assertEq(tokens[0], address(1)); + vm.expectRevert(Errors.IpRoyaltyVault__NoNewRevenueSinceLastSnapshot.selector); + ipRoyaltyVault.snapshot(); } - function test_IpRoyaltyVault_ClaimableRevenue() public { + function test_IpRoyaltyVault_snapshot() public { // payment is made to vault uint256 royaltyAmount = 100000 * 10 ** 6; - USDC.mint(address(3), 100000 * 10 ** 6); // 100k USDC - vm.startPrank(address(3)); - USDC.approve(address(royaltyPolicyLAP), royaltyAmount); - royaltyModule.payRoyaltyOnBehalf(address(2), address(3), address(USDC), royaltyAmount); + USDC.mint(address(2), royaltyAmount); // 100k USDC + LINK.mint(address(2), royaltyAmount); // 100k LINK + vm.startPrank(address(licensingModule)); + royaltyModule.onLicenseMinting(address(2), address(royaltyPolicyLAP), uint32(10 * 10 ** 6), ""); + IpRoyaltyVault ipRoyaltyVault = IpRoyaltyVault(royaltyModule.ipRoyaltyVaults(address(2))); + vm.stopPrank(); + vm.startPrank(address(2)); + USDC.approve(address(royaltyModule), royaltyAmount); + royaltyModule.payRoyaltyOnBehalf(address(2), address(2), address(USDC), royaltyAmount); + LINK.approve(address(royaltyModule), royaltyAmount); + royaltyModule.payRoyaltyOnBehalf(address(2), address(2), address(LINK), royaltyAmount); vm.stopPrank(); // take snapshot vm.warp(block.timestamp + 7 days + 1); + + uint256 usdcClaimVaultBefore = ipRoyaltyVault.claimVaultAmount(address(USDC)); + uint256 linkClaimVaultBefore = ipRoyaltyVault.claimVaultAmount(address(LINK)); + + vm.expectEmit(true, true, true, true, address(ipRoyaltyVault)); + emit IIpRoyaltyVault.SnapshotCompleted(1, block.timestamp); + ipRoyaltyVault.snapshot(); - (, , uint32 royaltyStack2) = royaltyPolicyLAP.getRoyaltyData(address(2)); + assertEq(ipRoyaltyVault.claimVaultAmount(address(USDC)), royaltyAmount); + assertEq(ipRoyaltyVault.claimVaultAmount(address(LINK)), royaltyAmount); + assertEq(ipRoyaltyVault.claimVaultAmount(address(USDC)) - usdcClaimVaultBefore, royaltyAmount); + assertEq(ipRoyaltyVault.claimVaultAmount(address(LINK)) - linkClaimVaultBefore, royaltyAmount); + assertEq(ipRoyaltyVault.lastSnapshotTimestamp(), block.timestamp); + assertEq(ipRoyaltyVault.claimableAtSnapshot(1, address(USDC)), royaltyAmount); + assertEq(ipRoyaltyVault.claimableAtSnapshot(1, address(LINK)), royaltyAmount); + + // users claim all USDC + address[] memory tokens = new address[](1); + tokens[0] = address(USDC); + vm.startPrank(address(2)); + ipRoyaltyVault.claimRevenueByTokenBatch(1, tokens); + vm.stopPrank(); + + // take snapshot + vm.warp(block.timestamp + 7 days + 1); + ipRoyaltyVault.snapshot(); - uint256 claimableRevenue = ipRoyaltyVault.claimableRevenue(address(2), 1, address(USDC)); - assertEq( - claimableRevenue, - royaltyAmount - (royaltyAmount * royaltyStack2) / royaltyPolicyLAP.TOTAL_RT_SUPPLY() - ); + // all USDC was claimed but LINK was not + assertEq(ipRoyaltyVault.tokens().length, 1); } - function test_IpRoyaltyVault_ClaimRevenueByTokenBatch_revert_Paused() public { + function test_IpRoyaltyVault_claimRevenue_revert_Paused() public { + // deploy vault + vm.startPrank(address(licensingModule)); + royaltyModule.onLicenseMinting(address(1), address(royaltyPolicyLAP), uint32(10 * 10 ** 6), ""); + IIpRoyaltyVault ipRoyaltyVault = IIpRoyaltyVault(royaltyModule.ipRoyaltyVaults(address(1))); vm.stopPrank(); + vm.prank(u.admin); - royaltyPolicyLAP.pause(); + royaltyModule.pause(); + + vm.expectRevert(Errors.IpRoyaltyVault__EnforcedPause.selector); + ipRoyaltyVault.claimRevenueBySnapshotBatch(new uint256[](0), address(USDC)); vm.expectRevert(Errors.IpRoyaltyVault__EnforcedPause.selector); ipRoyaltyVault.claimRevenueByTokenBatch(1, new address[](0)); } - function test_IpRoyaltyVault_ClaimRevenueByTokenBatch() public { + function test_IpRoyaltyVault_claimableRevenue() public { + uint256 royaltyAmount = 100 * 10 ** 6; + address receiverIpId = address(2); + address payerIpId = address(3); + // deploy vault + vm.startPrank(address(licensingModule)); + royaltyModule.onLicenseMinting(receiverIpId, address(royaltyPolicyLAP), uint32(10 * 10 ** 6), ""); + IIpRoyaltyVault ipRoyaltyVault = IIpRoyaltyVault(royaltyModule.ipRoyaltyVaults(receiverIpId)); + vm.stopPrank(); + + // send 30% of rts to another address + address minorityHolder = address(1); + vm.prank(receiverIpId); + IERC20(address(ipRoyaltyVault)).transfer(minorityHolder, 30e6); + // payment is made to vault - uint256 royaltyAmount = 100000 * 10 ** 6; - USDC.mint(address(3), royaltyAmount); // 100k USDC - LINK.mint(address(3), royaltyAmount); // 100k LINK - vm.startPrank(address(3)); - USDC.approve(address(royaltyPolicyLAP), royaltyAmount); - royaltyModule.payRoyaltyOnBehalf(address(2), address(3), address(USDC), royaltyAmount); - LINK.approve(address(royaltyPolicyLAP), royaltyAmount); - royaltyModule.payRoyaltyOnBehalf(address(2), address(3), address(LINK), royaltyAmount); + vm.startPrank(payerIpId); + USDC.mint(payerIpId, royaltyAmount); + USDC.approve(address(royaltyModule), royaltyAmount); + royaltyModule.payRoyaltyOnBehalf(receiverIpId, payerIpId, address(USDC), royaltyAmount); + + // take snapshot + vm.warp(block.timestamp + 7 days + 1); + ipRoyaltyVault.snapshot(); + + uint256 claimableRevenueIpId = ipRoyaltyVault.claimableRevenue(receiverIpId, 1, address(USDC)); + uint256 claimableRevenueMinHolder = ipRoyaltyVault.claimableRevenue(minorityHolder, 1, address(USDC)); + assertEq(claimableRevenueIpId, (royaltyAmount * 70e6) / 100e6); + assertEq(claimableRevenueMinHolder, (royaltyAmount * 30e6) / 100e6); + } + + function test_IpRoyaltyVault_claimRevenueByTokenBatch_revert_claimRevenueByTokenBatch() public { + // deploy vault + vm.startPrank(address(licensingModule)); + royaltyModule.onLicenseMinting(address(1), address(royaltyPolicyLAP), uint32(10 * 10 ** 6), ""); + IpRoyaltyVault ipRoyaltyVault = IpRoyaltyVault(royaltyModule.ipRoyaltyVaults(address(1))); + vm.stopPrank(); + + vm.startPrank(address(royaltyModule)); + ipRoyaltyVault.addIpRoyaltyVaultTokens(address(USDC)); vm.stopPrank(); // take snapshot vm.warp(block.timestamp + 7 days + 1); ipRoyaltyVault.snapshot(); - (, , uint32 royaltyStack2) = royaltyPolicyLAP.getRoyaltyData(address(2)); + address[] memory tokens = new address[](1); + tokens[0] = address(USDC); + + vm.expectRevert(Errors.IpRoyaltyVault__NoClaimableTokens.selector); + ipRoyaltyVault.claimRevenueByTokenBatch(1, tokens); + } + + function test_IpRoyaltyVault_claimRevenueByTokenBatch() public { + // payment is made to vault + uint256 royaltyAmount = 100000 * 10 ** 6; + USDC.mint(address(2), royaltyAmount); // 100k USDC + LINK.mint(address(2), royaltyAmount); // 100k LINK + vm.startPrank(address(licensingModule)); + royaltyModule.onLicenseMinting(address(2), address(royaltyPolicyLAP), uint32(10 * 10 ** 6), ""); + IpRoyaltyVault ipRoyaltyVault = IpRoyaltyVault(royaltyModule.ipRoyaltyVaults(address(2))); + vm.stopPrank(); + vm.startPrank(address(2)); + USDC.approve(address(royaltyModule), royaltyAmount); + royaltyModule.payRoyaltyOnBehalf(address(2), address(2), address(USDC), royaltyAmount); + LINK.approve(address(royaltyModule), royaltyAmount); + royaltyModule.payRoyaltyOnBehalf(address(2), address(2), address(LINK), royaltyAmount); + vm.stopPrank(); + + // take snapshot + vm.warp(block.timestamp + 7 days + 1); + ipRoyaltyVault.snapshot(); address[] memory tokens = new address[](2); tokens[0] = address(USDC); @@ -214,7 +300,7 @@ contract TestIpRoyaltyVault is BaseTest { vm.startPrank(address(2)); - uint256 expectedAmount = royaltyAmount - (royaltyAmount * royaltyStack2) / royaltyPolicyLAP.TOTAL_RT_SUPPLY(); + uint256 expectedAmount = royaltyAmount; vm.expectEmit(true, true, true, true, address(ipRoyaltyVault)); emit IIpRoyaltyVault.RevenueTokenClaimed(address(2), address(USDC), expectedAmount); @@ -232,38 +318,43 @@ contract TestIpRoyaltyVault is BaseTest { assertEq(ipRoyaltyVault.isClaimedAtSnapshot(1, address(2), address(LINK)), true); } - function test_IpRoyaltyVault_ClaimRevenueBySnapshotBatch_revert_Paused() public { + function test_IpRoyaltyVault_claimRevenueBySnapshotBatch_revert_NoClaimableTokens() public { + // deploy vault + vm.startPrank(address(licensingModule)); + royaltyModule.onLicenseMinting(address(1), address(royaltyPolicyLAP), uint32(10 * 10 ** 6), ""); + IpRoyaltyVault ipRoyaltyVault = IpRoyaltyVault(royaltyModule.ipRoyaltyVaults(address(1))); vm.stopPrank(); - vm.prank(u.admin); - royaltyPolicyLAP.pause(); - vm.expectRevert(Errors.IpRoyaltyVault__EnforcedPause.selector); + vm.expectRevert(Errors.IpRoyaltyVault__NoClaimableTokens.selector); ipRoyaltyVault.claimRevenueBySnapshotBatch(new uint256[](0), address(USDC)); } - function test_IpRoyaltyVault_ClaimRevenueBySnapshotBatch() public { + function test_IpRoyaltyVault_claimRevenueBySnapshotBatch() public { uint256 royaltyAmount = 100000 * 10 ** 6; - USDC.mint(address(3), royaltyAmount); // 100k USDC + USDC.mint(address(2), royaltyAmount); // 100k USDC + + vm.startPrank(address(licensingModule)); + royaltyModule.onLicenseMinting(address(2), address(royaltyPolicyLAP), uint32(10 * 10 ** 6), ""); + IpRoyaltyVault ipRoyaltyVault = IpRoyaltyVault(royaltyModule.ipRoyaltyVaults(address(2))); + vm.stopPrank(); // 1st payment is made to vault - vm.startPrank(address(3)); - USDC.approve(address(royaltyPolicyLAP), royaltyAmount); - royaltyModule.payRoyaltyOnBehalf(address(2), address(3), address(USDC), royaltyAmount / 2); + vm.startPrank(address(2)); + USDC.approve(address(royaltyModule), royaltyAmount); + royaltyModule.payRoyaltyOnBehalf(address(2), address(2), address(USDC), royaltyAmount / 2); // take snapshot vm.warp(block.timestamp + 7 days + 1); ipRoyaltyVault.snapshot(); // 2nt payment is made to vault - royaltyModule.payRoyaltyOnBehalf(address(2), address(3), address(USDC), royaltyAmount / 2); + royaltyModule.payRoyaltyOnBehalf(address(2), address(2), address(USDC), royaltyAmount / 2); vm.stopPrank(); // take snapshot vm.warp(block.timestamp + 7 days + 1); ipRoyaltyVault.snapshot(); - (, , uint32 royaltyStack2) = royaltyPolicyLAP.getRoyaltyData(address(2)); - uint256[] memory snapshots = new uint256[](2); snapshots[0] = 1; snapshots[1] = 2; @@ -272,7 +363,7 @@ contract TestIpRoyaltyVault is BaseTest { uint256 contractUsdcBalanceBefore = USDC.balanceOf(address(ipRoyaltyVault)); uint256 usdcClaimVaultBefore = ipRoyaltyVault.claimVaultAmount(address(USDC)); - uint256 expectedAmount = royaltyAmount - (royaltyAmount * royaltyStack2) / royaltyPolicyLAP.TOTAL_RT_SUPPLY(); + uint256 expectedAmount = royaltyAmount; vm.expectEmit(true, true, true, true, address(ipRoyaltyVault)); emit IIpRoyaltyVault.RevenueTokenClaimed(address(2), address(USDC), expectedAmount); @@ -287,217 +378,147 @@ contract TestIpRoyaltyVault is BaseTest { assertEq(ipRoyaltyVault.isClaimedAtSnapshot(2, address(2), address(USDC)), true); } - function test_IpRoyaltyVault_Snapshot_SnapshotIntervalTooShort() public { - vm.expectRevert(Errors.IpRoyaltyVault__SnapshotIntervalTooShort.selector); - ipRoyaltyVault.snapshot(); + function test_IpRoyaltyVault_claimByTokenBatchAsSelf_revert_InvalidTargetIpId() public { + // deploy vault + vm.startPrank(address(licensingModule)); + royaltyModule.onLicenseMinting(address(1), address(royaltyPolicyLAP), uint32(10 * 10 ** 6), ""); + IpRoyaltyVault ipRoyaltyVault = IpRoyaltyVault(royaltyModule.ipRoyaltyVaults(address(1))); + vm.stopPrank(); + + vm.expectRevert(Errors.IpRoyaltyVault__InvalidTargetIpId.selector); + ipRoyaltyVault.claimByTokenBatchAsSelf(1, new address[](0), address(0)); } - function test_IpRoyaltyVault_Snapshot_revert_Paused() public { - // payment is made to vault + function test_IpRoyaltyVault_claimByTokenBatchAsSelf() public { + // deploy two vaults and send 30% of rts to another address + uint256 royaltyAmount = 100000 * 10 ** 6; + USDC.mint(address(2), royaltyAmount); // 100k USDC + LINK.mint(address(2), royaltyAmount); // 100k LINK + vm.startPrank(address(licensingModule)); + royaltyModule.onLicenseMinting(address(2), address(royaltyPolicyLAP), uint32(10 * 10 ** 6), ""); + IpRoyaltyVault ipRoyaltyVault = IpRoyaltyVault(royaltyModule.ipRoyaltyVaults(address(2))); vm.stopPrank(); - vm.prank(u.admin); - royaltyPolicyLAP.pause(); - vm.expectRevert(Errors.IpRoyaltyVault__EnforcedPause.selector); - ipRoyaltyVault.snapshot(); - } + vm.startPrank(address(licensingModule)); + royaltyModule.onLicenseMinting(address(3), address(royaltyPolicyLAP), uint32(10 * 10 ** 6), ""); + IpRoyaltyVault ipRoyaltyVault2 = IpRoyaltyVault(royaltyModule.ipRoyaltyVaults(address(3))); + vm.stopPrank(); + + vm.prank(address(2)); + IERC20(address(ipRoyaltyVault)).transfer(address(ipRoyaltyVault2), 30e6); + vm.stopPrank(); - function test_IpRoyaltyVault_Snapshot() public { // payment is made to vault - uint256 royaltyAmount = 100000 * 10 ** 6; - USDC.mint(address(3), royaltyAmount); // 100k USDC - LINK.mint(address(3), royaltyAmount); // 100k LINK - vm.startPrank(address(3)); - USDC.approve(address(royaltyPolicyLAP), royaltyAmount); - royaltyModule.payRoyaltyOnBehalf(address(2), address(3), address(USDC), royaltyAmount); - LINK.approve(address(royaltyPolicyLAP), royaltyAmount); - royaltyModule.payRoyaltyOnBehalf(address(2), address(3), address(LINK), royaltyAmount); + vm.startPrank(address(2)); + USDC.approve(address(royaltyModule), royaltyAmount); + royaltyModule.payRoyaltyOnBehalf(address(2), address(2), address(USDC), royaltyAmount); + LINK.approve(address(royaltyModule), royaltyAmount); + royaltyModule.payRoyaltyOnBehalf(address(2), address(2), address(LINK), royaltyAmount); vm.stopPrank(); // take snapshot vm.warp(block.timestamp + 7 days + 1); + ipRoyaltyVault.snapshot(); + address[] memory tokens = new address[](2); + tokens[0] = address(USDC); + tokens[1] = address(LINK); + + uint256 claimerUsdcBalanceBefore = USDC.balanceOf(address(ipRoyaltyVault2)); + uint256 claimerLinkBalanceBefore = LINK.balanceOf(address(ipRoyaltyVault2)); + uint256 claimedUsdcBalanceBefore = USDC.balanceOf(address(ipRoyaltyVault)); + uint256 claimedLinkBalanceBefore = LINK.balanceOf(address(ipRoyaltyVault)); uint256 usdcClaimVaultBefore = ipRoyaltyVault.claimVaultAmount(address(USDC)); uint256 linkClaimVaultBefore = ipRoyaltyVault.claimVaultAmount(address(LINK)); - uint256 usdcAncestorsVaultBefore = ipRoyaltyVault.ancestorsVaultAmount(address(USDC)); - uint256 linkAncestorsVaultBefore = ipRoyaltyVault.ancestorsVaultAmount(address(LINK)); - (, , uint32 royaltyStack2) = royaltyPolicyLAP.getRoyaltyData(address(2)); + vm.startPrank(address(100)); - vm.expectEmit(true, true, true, true, address(ipRoyaltyVault)); - emit IIpRoyaltyVault.SnapshotCompleted(1, block.timestamp, royaltyStack2); + uint256 expectedAmount = (royaltyAmount * 30e6) / 100e6; - ipRoyaltyVault.snapshot(); - - assertEq( - ipRoyaltyVault.claimVaultAmount(address(USDC)) + ipRoyaltyVault.ancestorsVaultAmount(address(USDC)), - royaltyAmount - ); - assertEq( - ipRoyaltyVault.claimVaultAmount(address(LINK)) + ipRoyaltyVault.ancestorsVaultAmount(address(LINK)), - royaltyAmount - ); - assertEq( - ipRoyaltyVault.claimVaultAmount(address(USDC)) - usdcClaimVaultBefore, - royaltyAmount - (royaltyAmount * royaltyStack2) / royaltyPolicyLAP.TOTAL_RT_SUPPLY() - ); - assertEq( - ipRoyaltyVault.claimVaultAmount(address(LINK)) - linkClaimVaultBefore, - royaltyAmount - (royaltyAmount * royaltyStack2) / royaltyPolicyLAP.TOTAL_RT_SUPPLY() - ); - assertEq( - ipRoyaltyVault.ancestorsVaultAmount(address(USDC)) - usdcAncestorsVaultBefore, - (royaltyAmount * royaltyStack2) / royaltyPolicyLAP.TOTAL_RT_SUPPLY() - ); - assertEq( - ipRoyaltyVault.ancestorsVaultAmount(address(LINK)) - linkAncestorsVaultBefore, - (royaltyAmount * royaltyStack2) / royaltyPolicyLAP.TOTAL_RT_SUPPLY() - ); - assertEq(ipRoyaltyVault.lastSnapshotTimestamp(), block.timestamp); - assertEq(ipRoyaltyVault.unclaimedRoyaltyTokens(), royaltyStack2); - assertEq(ipRoyaltyVault.unclaimedAtSnapshot(1), royaltyStack2); - assertEq( - ipRoyaltyVault.claimableAtSnapshot(1, address(USDC)), - royaltyAmount - (royaltyAmount * royaltyStack2) / royaltyPolicyLAP.TOTAL_RT_SUPPLY() - ); - assertEq( - ipRoyaltyVault.claimableAtSnapshot(1, address(LINK)), - royaltyAmount - (royaltyAmount * royaltyStack2) / royaltyPolicyLAP.TOTAL_RT_SUPPLY() - ); - - // users claim all USDC - address[] memory tokens = new address[](1); - tokens[0] = address(USDC); - vm.prank(address(2)); - ipRoyaltyVault.claimRevenueByTokenBatch(1, tokens); + vm.expectEmit(true, true, true, true, address(ipRoyaltyVault)); + emit IIpRoyaltyVault.RevenueTokenClaimed(address(ipRoyaltyVault2), address(USDC), expectedAmount); - ipRoyaltyVault.collectRoyaltyTokens(address(5)); - ipRoyaltyVault.collectAccruedTokens(address(5), tokens); - ipRoyaltyVault.collectRoyaltyTokens(address(11)); - ipRoyaltyVault.collectAccruedTokens(address(11), tokens); - ipRoyaltyVault.collectRoyaltyTokens(address(12)); - ipRoyaltyVault.collectAccruedTokens(address(12), tokens); - ipRoyaltyVault.collectRoyaltyTokens(address(6)); - ipRoyaltyVault.collectAccruedTokens(address(6), tokens); - ipRoyaltyVault.collectRoyaltyTokens(address(13)); - ipRoyaltyVault.collectAccruedTokens(address(13), tokens); - ipRoyaltyVault.collectRoyaltyTokens(address(14)); - ipRoyaltyVault.collectAccruedTokens(address(14), tokens); + vm.expectEmit(true, true, true, true, address(ipRoyaltyVault)); + emit IIpRoyaltyVault.RevenueTokenClaimed(address(ipRoyaltyVault2), address(LINK), expectedAmount); - // take snapshot - vm.warp(block.timestamp + 7 days + 1); - ipRoyaltyVault.snapshot(); + ipRoyaltyVault2.claimByTokenBatchAsSelf(1, tokens, address(2)); - // all USDC was claimed but LINK was not - assertEq(ipRoyaltyVault.tokens().length, 1); + assertEq(USDC.balanceOf(address(ipRoyaltyVault2)) - claimerUsdcBalanceBefore, expectedAmount); + assertEq(LINK.balanceOf(address(ipRoyaltyVault2)) - claimerLinkBalanceBefore, expectedAmount); + assertEq(claimedUsdcBalanceBefore - USDC.balanceOf(address(ipRoyaltyVault)), expectedAmount); + assertEq(claimedLinkBalanceBefore - LINK.balanceOf(address(ipRoyaltyVault)), expectedAmount); + assertEq(usdcClaimVaultBefore - ipRoyaltyVault.claimVaultAmount(address(USDC)), expectedAmount); + assertEq(linkClaimVaultBefore - ipRoyaltyVault.claimVaultAmount(address(LINK)), expectedAmount); + assertEq(ipRoyaltyVault.isClaimedAtSnapshot(1, address(ipRoyaltyVault2), address(USDC)), true); + assertEq(ipRoyaltyVault.isClaimedAtSnapshot(1, address(ipRoyaltyVault2), address(LINK)), true); } - function test_IpRoyaltyVault_CollectRoyaltyTokens_AlreadyClaimed() public { - ipRoyaltyVault.collectRoyaltyTokens(address(5)); - - vm.expectRevert(Errors.IpRoyaltyVault__AlreadyClaimed.selector); - ipRoyaltyVault.collectRoyaltyTokens(address(5)); - } + function test_IpRoyaltyVault_claimBySnapshotBatchAsSelf_revert_InvalidTargetIpId() public { + // deploy vault + vm.startPrank(address(licensingModule)); + royaltyModule.onLicenseMinting(address(1), address(royaltyPolicyLAP), uint32(10 * 10 ** 6), ""); + IpRoyaltyVault ipRoyaltyVault = IpRoyaltyVault(royaltyModule.ipRoyaltyVaults(address(1))); + vm.stopPrank(); - function test_IpRoyaltyVault_CollectRoyaltyTokens_ClaimerNotAnAncestor() public { - vm.expectRevert(Errors.IpRoyaltyVault__ClaimerNotAnAncestor.selector); - ipRoyaltyVault.collectRoyaltyTokens(address(0)); + vm.expectRevert(Errors.IpRoyaltyVault__InvalidTargetIpId.selector); + ipRoyaltyVault.claimBySnapshotBatchAsSelf(new uint256[](0), address(USDC), address(0)); } - function test_IpRoyaltyVault_CollectRoyaltyTokens_revert_Paused() public { + function test_IpRoyaltyVault_claimBySnapshotBatchAsSelf() public { + // deploy two vaults and send 30% of rts to another address + uint256 royaltyAmount = 100000 * 10 ** 6; + USDC.mint(address(2), royaltyAmount * 2); // 100k USDC + vm.startPrank(address(licensingModule)); + royaltyModule.onLicenseMinting(address(2), address(royaltyPolicyLAP), uint32(10 * 10 ** 6), ""); + IpRoyaltyVault ipRoyaltyVault = IpRoyaltyVault(royaltyModule.ipRoyaltyVaults(address(2))); vm.stopPrank(); - vm.prank(u.admin); - royaltyPolicyLAP.pause(); - vm.expectRevert(Errors.IpRoyaltyVault__EnforcedPause.selector); - ipRoyaltyVault.collectRoyaltyTokens(address(5)); - } + vm.startPrank(address(licensingModule)); + royaltyModule.onLicenseMinting(address(3), address(royaltyPolicyLAP), uint32(10 * 10 ** 6), ""); + IpRoyaltyVault ipRoyaltyVault2 = IpRoyaltyVault(royaltyModule.ipRoyaltyVaults(address(3))); + vm.stopPrank(); - function test_IpRoyaltyVault_CollectRoyaltyTokens() public { - uint256 parentRoyalty = 5 * 10 ** 5; - uint256 royaltyAmount = 100000 * 10 ** 6; - uint256 accruedCollectableRevenue = (royaltyAmount * 5 * 10 ** 5) / royaltyPolicyLAP.TOTAL_RT_SUPPLY(); + vm.prank(address(2)); + IERC20(address(ipRoyaltyVault)).transfer(address(ipRoyaltyVault2), 30e6); + vm.stopPrank(); // payment is made to vault - USDC.mint(address(3), royaltyAmount); // 100k USDC - vm.startPrank(address(3)); - USDC.approve(address(royaltyPolicyLAP), royaltyAmount); - royaltyModule.payRoyaltyOnBehalf(address(2), address(3), address(USDC), royaltyAmount); + vm.startPrank(address(2)); + USDC.approve(address(royaltyModule), royaltyAmount); + royaltyModule.payRoyaltyOnBehalf(address(2), address(2), address(USDC), royaltyAmount); vm.stopPrank(); // take snapshot vm.warp(block.timestamp + 7 days + 1); ipRoyaltyVault.snapshot(); - uint256 userUsdcBalanceBefore = USDC.balanceOf(address(5)); - uint256 contractUsdcBalanceBefore = USDC.balanceOf(address(ipRoyaltyVault)); - uint256 usdcClaimVaultBefore = ipRoyaltyVault.claimVaultAmount(address(USDC)); - uint256 contractRTBalBefore = IERC20(address(ipRoyaltyVault)).balanceOf(address(ipRoyaltyVault)); - uint256 userRTBalBefore = IERC20(address(ipRoyaltyVault)).balanceOf(address(5)); - uint256 unclaimedRoyaltyTokensBefore = ipRoyaltyVault.unclaimedRoyaltyTokens(); - uint256 ancestorsVaultAmountBefore = ipRoyaltyVault.ancestorsVaultAmount(address(USDC)); - - vm.expectEmit(true, true, true, true, address(ipRoyaltyVault)); - emit IIpRoyaltyVault.RoyaltyTokensCollected(address(5), parentRoyalty); - emit IIpRoyaltyVault.RevenueTokenClaimed(address(5), address(USDC), accruedCollectableRevenue); - - ipRoyaltyVault.collectRoyaltyTokens(address(5)); - - assertEq(ipRoyaltyVault.isCollectedByAncestor(address(5)), true); - assertEq( - contractRTBalBefore - IERC20(address(ipRoyaltyVault)).balanceOf(address(ipRoyaltyVault)), - parentRoyalty - ); - assertEq(IERC20(address(ipRoyaltyVault)).balanceOf(address(5)) - userRTBalBefore, parentRoyalty); - assertEq(unclaimedRoyaltyTokensBefore - ipRoyaltyVault.unclaimedRoyaltyTokens(), parentRoyalty); - } - - function test_IpRoyaltyVault_CollectAccruedTokens_revert_Paused() public { - vm.stopPrank(); - vm.prank(u.admin); - royaltyPolicyLAP.pause(); - - vm.expectRevert(Errors.IpRoyaltyVault__EnforcedPause.selector); - ipRoyaltyVault.collectAccruedTokens(address(5), new address[](0)); - } - - function test_IpRoyaltyVault_CollectAccruedTokens() public { - uint256 parentRoyalty = 5 * 10 ** 5; - uint256 royaltyAmount = 100000 * 10 ** 6; - uint256 accruedCollectableRevenue = (royaltyAmount * 5 * 10 ** 5) / royaltyPolicyLAP.TOTAL_RT_SUPPLY(); - // payment is made to vault - USDC.mint(address(3), royaltyAmount); // 100k USDC - vm.startPrank(address(3)); - USDC.approve(address(royaltyPolicyLAP), royaltyAmount); - royaltyModule.payRoyaltyOnBehalf(address(2), address(3), address(USDC), royaltyAmount); + vm.startPrank(address(2)); + USDC.approve(address(royaltyModule), royaltyAmount); + royaltyModule.payRoyaltyOnBehalf(address(2), address(2), address(USDC), royaltyAmount); vm.stopPrank(); // take snapshot - vm.warp(block.timestamp + 7 days + 1); + vm.warp(block.timestamp + 15 days + 1); ipRoyaltyVault.snapshot(); - // collect royalty tokens - ipRoyaltyVault.collectRoyaltyTokens(address(5)); + uint256[] memory snapshots = new uint256[](2); + snapshots[0] = 1; + snapshots[1] = 2; - address[] memory tokens = new address[](1); - tokens[0] = address(USDC); + uint256 expectedAmount = (royaltyAmount * 2 * 30e6) / 100e6; - uint256 userUsdcBalanceBefore = USDC.balanceOf(address(5)); - uint256 contractUsdcBalanceBefore = USDC.balanceOf(address(ipRoyaltyVault)); - uint256 ancestorsVaultAmountBefore = ipRoyaltyVault.ancestorsVaultAmount(address(USDC)); - uint256 collectableAmountBefore = ipRoyaltyVault.collectableAmount(address(5), address(USDC)); + vm.expectEmit(true, true, true, true, address(ipRoyaltyVault)); + emit IIpRoyaltyVault.RevenueTokenClaimed(address(ipRoyaltyVault2), address(USDC), expectedAmount); - ipRoyaltyVault.collectAccruedTokens(address(5), tokens); + uint256 claimerUsdcBalanceBefore = USDC.balanceOf(address(ipRoyaltyVault2)); + uint256 claimedUsdcBalanceBefore = USDC.balanceOf(address(ipRoyaltyVault)); + uint256 usdcClaimVaultBefore = ipRoyaltyVault.claimVaultAmount(address(USDC)); - uint256 userUsdcBalanceAfter = USDC.balanceOf(address(5)); - uint256 contractUsdcBalanceAfter = USDC.balanceOf(address(ipRoyaltyVault)); - uint256 ancestorsVaultAmountAfter = ipRoyaltyVault.ancestorsVaultAmount(address(USDC)); - uint256 collectableAmountAfter = ipRoyaltyVault.collectableAmount(address(5), address(USDC)); + ipRoyaltyVault2.claimBySnapshotBatchAsSelf(snapshots, address(USDC), address(2)); - assertEq(userUsdcBalanceAfter - userUsdcBalanceBefore, accruedCollectableRevenue); - assertEq(contractUsdcBalanceBefore - contractUsdcBalanceAfter, accruedCollectableRevenue); - assertEq(ancestorsVaultAmountBefore - ancestorsVaultAmountAfter, accruedCollectableRevenue); - assertEq(collectableAmountAfter, 0); + assertEq(USDC.balanceOf(address(ipRoyaltyVault2)) - claimerUsdcBalanceBefore, expectedAmount); + assertEq(claimedUsdcBalanceBefore - USDC.balanceOf(address(ipRoyaltyVault)), expectedAmount); + assertEq(usdcClaimVaultBefore - ipRoyaltyVault.claimVaultAmount(address(USDC)), expectedAmount); } } diff --git a/test/foundry/modules/royalty/LAP/RoyaltyPolicyLAP.t.sol b/test/foundry/modules/royalty/LAP/RoyaltyPolicyLAP.t.sol new file mode 100644 index 00000000..bcc8b29f --- /dev/null +++ b/test/foundry/modules/royalty/LAP/RoyaltyPolicyLAP.t.sol @@ -0,0 +1,490 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.23; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { ERC6551AccountLib } from "erc6551/lib/ERC6551AccountLib.sol"; + +// contracts +import { RoyaltyPolicyLAP } from "../../../../../contracts/modules/royalty/policies/LAP/RoyaltyPolicyLAP.sol"; +// solhint-disable-next-line max-line-length +import { IRoyaltyPolicyLAP } from "../../../../../contracts/interfaces/modules/royalty/policies/LAP/IRoyaltyPolicyLAP.sol"; +import { Errors } from "../../../../../contracts/lib/Errors.sol"; + +// tests +import { BaseTest } from "../../../utils/BaseTest.t.sol"; +import { TestProxyHelper } from "../../../utils/TestProxyHelper.sol"; +import { IIpRoyaltyVault } from "../../../../../contracts/interfaces/modules/royalty/policies/IIpRoyaltyVault.sol"; +import { MockExternalRoyaltyPolicy1 } from "../../../mocks/policy/MockExternalRoyaltyPolicy1.sol"; +import { MockExternalRoyaltyPolicy2 } from "../../../mocks/policy/MockExternalRoyaltyPolicy2.sol"; + +contract TestRoyaltyPolicyLAP is BaseTest { + RoyaltyPolicyLAP internal testRoyaltyPolicyLAP; + + address internal mockExternalRoyaltyPolicy1; + address internal mockExternalRoyaltyPolicy2; + + function setUp() public override { + super.setUp(); + + // register external royalty policies + mockExternalRoyaltyPolicy1 = address(new MockExternalRoyaltyPolicy1()); + mockExternalRoyaltyPolicy2 = address(new MockExternalRoyaltyPolicy2()); + royaltyModule.registerExternalRoyaltyPolicy(mockExternalRoyaltyPolicy1); + royaltyModule.registerExternalRoyaltyPolicy(mockExternalRoyaltyPolicy2); + + vm.startPrank(address(licensingModule)); + _setupTree(); + vm.stopPrank(); + } + + function _setupTree() internal { + // mint license for roots + royaltyModule.onLicenseMinting(address(10), address(royaltyPolicyLAP), uint32(10 * 10 ** 6), ""); + royaltyModule.onLicenseMinting(address(20), address(royaltyPolicyLAP), uint32(10 * 10 ** 6), ""); + royaltyModule.onLicenseMinting(address(30), address(royaltyPolicyLRP), uint32(7 * 10 ** 6), ""); + + // link 40 to parents + address[] memory parents = new address[](3); + address[] memory licenseRoyaltyPolicies = new address[](3); + uint32[] memory parentRoyalties = new uint32[](3); + parents[0] = address(10); + parents[1] = address(20); + parents[2] = address(30); + licenseRoyaltyPolicies[0] = address(royaltyPolicyLAP); + licenseRoyaltyPolicies[1] = address(royaltyPolicyLAP); + licenseRoyaltyPolicies[2] = address(royaltyPolicyLRP); + parentRoyalties[0] = uint32(10 * 10 ** 6); + parentRoyalties[1] = uint32(10 * 10 ** 6); + parentRoyalties[2] = uint32(7 * 10 ** 6); + ipGraph.addParentIp(address(40), parents); + royaltyModule.onLinkToParents(address(40), parents, licenseRoyaltyPolicies, parentRoyalties, ""); + + // mint license for 40 + royaltyModule.onLicenseMinting(address(40), address(royaltyPolicyLRP), uint32(5 * 10 ** 6), ""); + + // link 50 to 40 + parents = new address[](1); + licenseRoyaltyPolicies = new address[](1); + parentRoyalties = new uint32[](1); + parents[0] = address(40); + licenseRoyaltyPolicies[0] = address(royaltyPolicyLRP); + parentRoyalties[0] = uint32(5 * 10 ** 6); + ipGraph.addParentIp(address(50), parents); + royaltyModule.onLinkToParents(address(50), parents, licenseRoyaltyPolicies, parentRoyalties, ""); + + // mint license for 50 + royaltyModule.onLicenseMinting(address(50), address(royaltyPolicyLRP), uint32(15 * 10 ** 6), ""); + + // link 60 to 50 + parents = new address[](1); + licenseRoyaltyPolicies = new address[](1); + parentRoyalties = new uint32[](1); + parents[0] = address(50); + licenseRoyaltyPolicies[0] = address(royaltyPolicyLRP); + parentRoyalties[0] = uint32(15 * 10 ** 6); + ipGraph.addParentIp(address(60), parents); + royaltyModule.onLinkToParents(address(60), parents, licenseRoyaltyPolicies, parentRoyalties, ""); + + // mint license for 60 + royaltyModule.onLicenseMinting(address(60), address(mockExternalRoyaltyPolicy1), uint32(12 * 10 ** 6), ""); + + // link 70 to 60 + parents = new address[](1); + licenseRoyaltyPolicies = new address[](1); + parentRoyalties = new uint32[](1); + parents[0] = address(60); + licenseRoyaltyPolicies[0] = address(mockExternalRoyaltyPolicy1); + parentRoyalties[0] = uint32(12 * 10 ** 6); + ipGraph.addParentIp(address(70), parents); + royaltyModule.onLinkToParents(address(70), parents, licenseRoyaltyPolicies, parentRoyalties, ""); + + // mint license for 10 + 60 + 70 + royaltyModule.onLicenseMinting(address(10), address(royaltyPolicyLAP), uint32(5 * 10 ** 6), ""); + royaltyModule.onLicenseMinting(address(60), address(royaltyPolicyLRP), uint32(20 * 10 ** 6), ""); + royaltyModule.onLicenseMinting(address(70), address(mockExternalRoyaltyPolicy2), uint32(24 * 10 ** 6), ""); + } + + function test_RoyaltyPolicyLAP_constructor_revert_ZeroRoyaltyModule() public { + vm.expectRevert(Errors.RoyaltyPolicyLAP__ZeroRoyaltyModule.selector); + new RoyaltyPolicyLAP(address(0), address(1), address(1)); + } + + function test_RoyaltyPolicyLAP_constructor_revert_ZeroDisputeModule() public { + vm.expectRevert(Errors.RoyaltyPolicyLAP__ZeroDisputeModule.selector); + new RoyaltyPolicyLAP(address(1), address(0), address(1)); + } + + function test_RoyaltyPolicyLAP_constructor_revert_ZeroIPGraphACL() public { + vm.expectRevert(Errors.RoyaltyPolicyLAP__ZeroIPGraphACL.selector); + new RoyaltyPolicyLAP(address(1), address(1), address(0)); + } + + function test_RoyaltyPolicyLAP_constructor() public { + testRoyaltyPolicyLAP = new RoyaltyPolicyLAP( + address(royaltyModule), + address(disputeModule), + address(ipGraphACL) + ); + assertEq(address(testRoyaltyPolicyLAP.ROYALTY_MODULE()), address(royaltyModule)); + assertEq(address(testRoyaltyPolicyLAP.DISPUTE_MODULE()), address(disputeModule)); + } + + function test_RoyaltyPolicyLAP_initialize_revert_ZeroAccessManager() public { + address impl = address( + new RoyaltyPolicyLAP(address(royaltyModule), address(disputeModule), address(ipGraphACL)) + ); + vm.expectRevert(Errors.RoyaltyPolicyLAP__ZeroAccessManager.selector); + RoyaltyPolicyLAP( + TestProxyHelper.deployUUPSProxy(impl, abi.encodeCall(RoyaltyPolicyLAP.initialize, (address(0)))) + ); + } + + function test_RoyaltyPolicyLAP_onLicenseMinting_revert_NotRoyaltyModule() public { + vm.expectRevert(Errors.RoyaltyPolicyLAP__NotRoyaltyModule.selector); + royaltyPolicyLAP.onLicenseMinting(address(1), uint32(0), ""); + } + + function test_RoyaltyPolicyLAP_onLicenseMinting_revert_AboveRoyaltyStackLimit() public { + uint32 excessPercent = royaltyModule.TOTAL_RT_SUPPLY() + 1; + vm.prank(address(royaltyModule)); + vm.expectRevert(Errors.RoyaltyPolicyLAP__AboveRoyaltyStackLimit.selector); + royaltyPolicyLAP.onLicenseMinting(address(100), excessPercent, ""); + } + + function test_RoyaltyPolicyLAP_onLinkToParents_revert_NotRoyaltyModule() public { + vm.expectRevert(Errors.RoyaltyPolicyLAP__NotRoyaltyModule.selector); + royaltyPolicyLAP.onLinkToParents(address(100), new address[](0), new address[](0), new uint32[](0), ""); + } + + function test_RoyaltyPolicyLAP_onLinkToParents_revert_AboveRoyaltyStackLimit() public { + address[] memory parents = new address[](3); + address[] memory licenseRoyaltyPolicies = new address[](3); + uint32[] memory parentRoyalties = new uint32[](3); + parents[0] = address(10); + parents[1] = address(20); + parents[2] = address(30); + licenseRoyaltyPolicies[0] = address(royaltyPolicyLAP); + licenseRoyaltyPolicies[1] = address(royaltyPolicyLAP); + licenseRoyaltyPolicies[2] = address(royaltyPolicyLAP); + parentRoyalties[0] = uint32(10 * 10 ** 6); + parentRoyalties[1] = uint32(15 * 10 ** 6); + parentRoyalties[2] = uint32(200 * 10 ** 6); + ipGraph.addParentIp(address(80), parents); + + vm.startPrank(address(royaltyModule)); + vm.expectRevert(Errors.RoyaltyPolicyLAP__AboveRoyaltyStackLimit.selector); + royaltyPolicyLAP.onLinkToParents(address(80), parents, licenseRoyaltyPolicies, parentRoyalties, ""); + } + + function test_RoyaltyPolicyLAP_onLinkToParents() public { + address[] memory parents = new address[](3); + address[] memory licenseRoyaltyPolicies = new address[](3); + uint32[] memory parentRoyalties = new uint32[](3); + parents[0] = address(10); + parents[1] = address(20); + parents[2] = address(30); + licenseRoyaltyPolicies[0] = address(royaltyPolicyLAP); + licenseRoyaltyPolicies[1] = address(royaltyPolicyLAP); + licenseRoyaltyPolicies[2] = address(royaltyPolicyLAP); + parentRoyalties[0] = uint32(10 * 10 ** 6); + parentRoyalties[1] = uint32(15 * 10 ** 6); + parentRoyalties[2] = uint32(20 * 10 ** 6); + ipGraph.addParentIp(address(80), parents); + + vm.startPrank(address(licensingModule)); + royaltyModule.onLinkToParents(address(80), parents, licenseRoyaltyPolicies, parentRoyalties, ""); + + address ipRoyaltyVault = royaltyModule.ipRoyaltyVaults(address(80)); + + uint256 ipId80Balance = IERC20(ipRoyaltyVault).balanceOf(address(80)); + uint256 ipId80LapBalance = IERC20(ipRoyaltyVault).balanceOf(address(royaltyPolicyLAP)); + + assertEq(ipId80LapBalance, 45 * 10 ** 6); + assertEq(ipId80Balance, 55 * 10 ** 6); + assertEq(royaltyPolicyLAP.royaltyStack(address(80)), 45 * 10 ** 6); + assertEq(royaltyPolicyLAP.unclaimedRoyaltyTokens(address(80)), 45 * 10 ** 6); + } + + function test_RoyaltyPolicyLAP_collectRoyaltyTokens_IpTagged() public { + registerSelectedPILicenseTerms_Commercial({ + selectionName: "cheap_flexible", + transferable: true, + derivatives: true, + reciprocal: false, + commercialRevShare: 10, + mintingFee: 0 + }); + mockNFT.mintId(u.alice, 0); + address expectedAddr = ERC6551AccountLib.computeAddress( + address(erc6551Registry), + address(ipAccountImpl), + ipAccountRegistry.IP_ACCOUNT_SALT(), + block.chainid, + address(mockNFT), + 0 + ); + vm.label(expectedAddr, "IPAccount0"); + vm.startPrank(u.alice); + address ipAddr = ipAssetRegistry.register(block.chainid, address(mockNFT), 0); + licensingModule.attachLicenseTerms(ipAddr, address(pilTemplate), getSelectedPILicenseTermsId("cheap_flexible")); + vm.stopPrank(); + + // raise dispute + vm.startPrank(ipAddr); + USDC.mint(ipAddr, ARBITRATION_PRICE); + USDC.approve(address(arbitrationPolicySP), ARBITRATION_PRICE); + disputeModule.raiseDispute(ipAddr, string("urlExample"), "PLAGIARISM", ""); + vm.stopPrank(); + + // set dispute judgement + vm.startPrank(u.relayer); + disputeModule.setDisputeJudgement(1, true, ""); + + vm.expectRevert(Errors.RoyaltyPolicyLAP__IpTagged.selector); + royaltyPolicyLAP.collectRoyaltyTokens(ipAddr, address(20)); + } + + function test_RoyaltyPolicyLAP_collectRoyaltyTokens_revert_AlreadyClaimed() public { + // link ip 80 + address[] memory parents = new address[](3); + address[] memory licenseRoyaltyPolicies = new address[](3); + uint32[] memory parentRoyalties = new uint32[](3); + parents[0] = address(10); + parents[1] = address(20); + parents[2] = address(30); + licenseRoyaltyPolicies[0] = address(royaltyPolicyLAP); + licenseRoyaltyPolicies[1] = address(royaltyPolicyLAP); + licenseRoyaltyPolicies[2] = address(royaltyPolicyLAP); + parentRoyalties[0] = uint32(12345678); + parentRoyalties[1] = uint32(3); + parentRoyalties[2] = uint32(77654321); + ipGraph.addParentIp(address(2), parents); + + vm.startPrank(address(licensingModule)); + royaltyModule.onLinkToParents(address(2), parents, licenseRoyaltyPolicies, parentRoyalties, ""); + address ipRoyaltyVault = royaltyModule.ipRoyaltyVaults(address(2)); + assertFalse(ipRoyaltyVault == address(0)); + + // make payment + uint256 royaltyAmount = 1234; + USDC.mint(address(2), royaltyAmount); // 1000 USDC + vm.startPrank(address(2)); + USDC.approve(address(royaltyModule), royaltyAmount); + royaltyModule.payRoyaltyOnBehalf(address(2), address(2), address(USDC), royaltyAmount); + + // call snapshot + vm.warp(7 days + 1); + IIpRoyaltyVault(ipRoyaltyVault).snapshot(); + + // call collectRoyaltyTokens + address[] memory tokenList = IIpRoyaltyVault(ipRoyaltyVault).tokens(); + assertEq(tokenList.length, 1); + assertEq(tokenList[0], address(USDC)); + + // LAP claims revenue tokens + uint256[] memory snapshotIds = new uint256[](1); + snapshotIds[0] = 1; + royaltyPolicyLAP.claimBySnapshotBatchAsSelf(snapshotIds, address(USDC), address(2)); + + // one parent collects royalty tokens + royaltyPolicyLAP.collectRoyaltyTokens(address(2), address(20)); + + vm.expectRevert(Errors.RoyaltyPolicyLAP__AlreadyClaimed.selector); + royaltyPolicyLAP.collectRoyaltyTokens(address(2), address(20)); + } + + function test_RoyaltyPolicyLAP_collectRoyaltyTokens_revert_ClaimerNotAnAncestor() public { + vm.expectRevert(Errors.RoyaltyPolicyLAP__ClaimerNotAnAncestor.selector); + royaltyPolicyLAP.collectRoyaltyTokens(address(2), address(1111)); + } + + function test_RoyaltyPolicyLAP_collectRoyaltyTokens_revert_NotAllRevenueTokensHaveBeenClaimed() public { + // link ip 80 + address[] memory parents = new address[](3); + address[] memory licenseRoyaltyPolicies = new address[](3); + uint32[] memory parentRoyalties = new uint32[](3); + parents[0] = address(10); + parents[1] = address(20); + parents[2] = address(30); + licenseRoyaltyPolicies[0] = address(royaltyPolicyLAP); + licenseRoyaltyPolicies[1] = address(royaltyPolicyLAP); + licenseRoyaltyPolicies[2] = address(royaltyPolicyLAP); + parentRoyalties[0] = uint32(12345678); + parentRoyalties[1] = uint32(3); + parentRoyalties[2] = uint32(77654321); + ipGraph.addParentIp(address(2), parents); + + vm.startPrank(address(licensingModule)); + royaltyModule.onLinkToParents(address(2), parents, licenseRoyaltyPolicies, parentRoyalties, ""); + address ipRoyaltyVault = royaltyModule.ipRoyaltyVaults(address(2)); + assertFalse(ipRoyaltyVault == address(0)); + + // make payment + uint256 royaltyAmount = 1234; + USDC.mint(address(2), royaltyAmount); // 1000 USDC + vm.startPrank(address(2)); + USDC.approve(address(royaltyModule), royaltyAmount); + royaltyModule.payRoyaltyOnBehalf(address(2), address(2), address(USDC), royaltyAmount); + + // call snapshot + vm.warp(7 days + 1); + IIpRoyaltyVault(ipRoyaltyVault).snapshot(); + + // call collectRoyaltyTokens + address[] memory tokenList = IIpRoyaltyVault(ipRoyaltyVault).tokens(); + assertEq(tokenList.length, 1); + assertEq(tokenList[0], address(USDC)); + + // one parent collects royalty tokens + vm.expectRevert(Errors.RoyaltyPolicyLAP__NotAllRevenueTokensHaveBeenClaimed.selector); + royaltyPolicyLAP.collectRoyaltyTokens(address(2), address(20)); + } + + function test_RoyaltyPolicyLAP_collectRoyaltyTokens() public { + // link ip 80 + address[] memory parents = new address[](3); + address[] memory licenseRoyaltyPolicies = new address[](3); + uint32[] memory parentRoyalties = new uint32[](3); + parents[0] = address(10); + parents[1] = address(20); + parents[2] = address(30); + licenseRoyaltyPolicies[0] = address(royaltyPolicyLAP); + licenseRoyaltyPolicies[1] = address(royaltyPolicyLAP); + licenseRoyaltyPolicies[2] = address(royaltyPolicyLAP); + parentRoyalties[0] = uint32(12345678); + parentRoyalties[1] = uint32(3); + parentRoyalties[2] = uint32(77654321); + ipGraph.addParentIp(address(2), parents); + + vm.startPrank(address(licensingModule)); + royaltyModule.onLinkToParents(address(2), parents, licenseRoyaltyPolicies, parentRoyalties, ""); + address ipRoyaltyVault = royaltyModule.ipRoyaltyVaults(address(2)); + assertFalse(ipRoyaltyVault == address(0)); + + // make payment + uint256 royaltyAmount = 1234; + USDC.mint(address(2), royaltyAmount); // 1000 USDC + vm.startPrank(address(2)); + USDC.approve(address(royaltyModule), royaltyAmount); + royaltyModule.payRoyaltyOnBehalf(address(2), address(2), address(USDC), royaltyAmount); + + // call snapshot + vm.warp(7 days + 1); + IIpRoyaltyVault(ipRoyaltyVault).snapshot(); + + // call collectRoyaltyTokens + address[] memory tokenList = IIpRoyaltyVault(ipRoyaltyVault).tokens(); + assertEq(tokenList.length, 1); + assertEq(tokenList[0], address(USDC)); + + // LAP claims revenue tokens + uint256[] memory snapshotIds = new uint256[](1); + snapshotIds[0] = 1; + royaltyPolicyLAP.claimBySnapshotBatchAsSelf(snapshotIds, address(USDC), address(2)); + + // one parent collects royalty tokens + royaltyPolicyLAP.collectRoyaltyTokens(address(2), address(20)); + + // new payment and new snapshot + USDC.mint(address(2), royaltyAmount); // 1000 USDC + USDC.approve(address(royaltyModule), royaltyAmount); + royaltyModule.payRoyaltyOnBehalf(address(2), address(2), address(USDC), royaltyAmount); + vm.warp(14 days + 1); + IIpRoyaltyVault(ipRoyaltyVault).snapshot(); + + // LAP claims revenue tokens + uint256[] memory snapshotIds2 = new uint256[](2); + snapshotIds2[0] = 1; + snapshotIds2[1] = 2; + royaltyPolicyLAP.claimBySnapshotBatchAsSelf(snapshotIds2, address(USDC), address(2)); + + // another parent collects royalty tokens + uint32 parent10Royalty = parentRoyalties[0]; + address ancestor10Vault = royaltyModule.ipRoyaltyVaults(address(10)); + uint256 expectedUSDCForAncestor10 = (royaltyAmount * 2 * parent10Royalty) / royaltyModule.TOTAL_RT_SUPPLY(); + + uint256 ipId2RTAncestorVaultBalBefore = IERC20(ipRoyaltyVault).balanceOf(address(ancestor10Vault)); + uint256 USDCAncestorVaultBalBefore = IERC20(USDC).balanceOf(address(ancestor10Vault)); + uint256 revenueTokenBalancesBefore = royaltyPolicyLAP.revenueTokenBalances(address(2), address(USDC)); + bool isCollectedByAncestorBefore = royaltyPolicyLAP.isCollectedByAncestor(address(2), address(10)); + uint256 unclaimedRoyaltyTokensBefore = royaltyPolicyLAP.unclaimedRoyaltyTokens(address(2)); + + vm.expectEmit(true, true, true, true, address(royaltyPolicyLAP)); + emit IRoyaltyPolicyLAP.RoyaltyTokensCollected(address(2), address(10), parent10Royalty); + royaltyPolicyLAP.collectRoyaltyTokens(address(2), address(10)); + + uint256 ipId2RTAncestorVaultBalAfter = IERC20(ipRoyaltyVault).balanceOf(address(ancestor10Vault)); + uint256 USDCAncestorVaultBalAfter = IERC20(USDC).balanceOf(address(ancestor10Vault)); + uint256 revenueTokenBalancesAfter = royaltyPolicyLAP.revenueTokenBalances(address(2), address(USDC)); + bool isCollectedByAncestorAfter = royaltyPolicyLAP.isCollectedByAncestor(address(2), address(10)); + uint256 unclaimedRoyaltyTokensAfter = royaltyPolicyLAP.unclaimedRoyaltyTokens(address(2)); + + assertEq(ipId2RTAncestorVaultBalAfter - ipId2RTAncestorVaultBalBefore, parent10Royalty); + assertEq(USDCAncestorVaultBalAfter - USDCAncestorVaultBalBefore, expectedUSDCForAncestor10); + assertEq(revenueTokenBalancesBefore - revenueTokenBalancesAfter, expectedUSDCForAncestor10); + assertEq(isCollectedByAncestorBefore, false); + assertEq(isCollectedByAncestorAfter, true); + assertEq(unclaimedRoyaltyTokensBefore - unclaimedRoyaltyTokensAfter, parent10Royalty); + } + + function test_RoyaltyPolicyLAP_claimBySnapshotBatchAsSelf() public { + // link ip 80 + address[] memory parents = new address[](3); + address[] memory licenseRoyaltyPolicies = new address[](3); + uint32[] memory parentRoyalties = new uint32[](3); + parents[0] = address(10); + parents[1] = address(20); + parents[2] = address(30); + licenseRoyaltyPolicies[0] = address(royaltyPolicyLAP); + licenseRoyaltyPolicies[1] = address(royaltyPolicyLAP); + licenseRoyaltyPolicies[2] = address(royaltyPolicyLAP); + parentRoyalties[0] = uint32(12345678); + parentRoyalties[1] = uint32(3); + parentRoyalties[2] = uint32(77654321); + ipGraph.addParentIp(address(2), parents); + + vm.startPrank(address(licensingModule)); + royaltyModule.onLinkToParents(address(2), parents, licenseRoyaltyPolicies, parentRoyalties, ""); + address ipRoyaltyVault = royaltyModule.ipRoyaltyVaults(address(2)); + assertFalse(ipRoyaltyVault == address(0)); + + // make payment + uint256 royaltyAmount = 1234; + USDC.mint(address(2), royaltyAmount); // 1000 USDC + vm.startPrank(address(2)); + USDC.approve(address(royaltyModule), royaltyAmount); + royaltyModule.payRoyaltyOnBehalf(address(2), address(2), address(USDC), royaltyAmount); + + // call snapshot + vm.warp(7 days + 1); + IIpRoyaltyVault(ipRoyaltyVault).snapshot(); + + // call collectRoyaltyTokens + address[] memory tokenList = IIpRoyaltyVault(ipRoyaltyVault).tokens(); + assertEq(tokenList.length, 1); + assertEq(tokenList[0], address(USDC)); + + // LAP claims revenue tokens + uint256[] memory snapshotIds = new uint256[](1); + snapshotIds[0] = 1; + + uint256 expectedUSDCForLap = (royaltyAmount * royaltyPolicyLAP.unclaimedRoyaltyTokens(address(2))) / + royaltyModule.TOTAL_RT_SUPPLY(); + + uint256 lapContractUSDCBalBefore = IERC20(USDC).balanceOf(address(royaltyPolicyLAP)); + bool snapshotsClaimedBefore = royaltyPolicyLAP.snapshotsClaimed(address(2), address(USDC), 1); + uint256 snapshotsClaimedCounterBefore = royaltyPolicyLAP.snapshotsClaimedCounter(address(2), address(USDC)); + + royaltyPolicyLAP.claimBySnapshotBatchAsSelf(snapshotIds, address(USDC), address(2)); + + uint256 lapContractUSDCBalAfter = IERC20(USDC).balanceOf(address(royaltyPolicyLAP)); + bool snapshotsClaimedAfter = royaltyPolicyLAP.snapshotsClaimed(address(2), address(USDC), 1); + uint256 snapshotsClaimedCounterAfter = royaltyPolicyLAP.snapshotsClaimedCounter(address(2), address(USDC)); + + assertEq(lapContractUSDCBalAfter - lapContractUSDCBalBefore, expectedUSDCForLap); + assertEq(snapshotsClaimedBefore, false); + assertEq(snapshotsClaimedAfter, true); + assertEq(snapshotsClaimedCounterBefore, 0); + assertEq(snapshotsClaimedCounterAfter, 1); + } +} diff --git a/test/foundry/modules/royalty/LRP/RoyaltyPolicyLRP.t.sol b/test/foundry/modules/royalty/LRP/RoyaltyPolicyLRP.t.sol new file mode 100644 index 00000000..8fdc462a --- /dev/null +++ b/test/foundry/modules/royalty/LRP/RoyaltyPolicyLRP.t.sol @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.23; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +// contracts +import { RoyaltyPolicyLRP } from "../../../../../contracts/modules/royalty/policies/LRP/RoyaltyPolicyLRP.sol"; +import { Errors } from "../../../../../contracts/lib/Errors.sol"; + +// tests +import { BaseTest } from "../../../utils/BaseTest.t.sol"; +import { TestProxyHelper } from "../../../utils/TestProxyHelper.sol"; +import { MockExternalRoyaltyPolicy1 } from "../../../mocks/policy/MockExternalRoyaltyPolicy1.sol"; +import { MockExternalRoyaltyPolicy2 } from "../../../mocks/policy/MockExternalRoyaltyPolicy2.sol"; + +contract TestRoyaltyPolicyLRP is BaseTest { + RoyaltyPolicyLRP internal testRoyaltyPolicyLRP; + + address internal mockExternalRoyaltyPolicy1; + address internal mockExternalRoyaltyPolicy2; + + function setUp() public override { + super.setUp(); + + // register external royalty policies + mockExternalRoyaltyPolicy1 = address(new MockExternalRoyaltyPolicy1()); + mockExternalRoyaltyPolicy2 = address(new MockExternalRoyaltyPolicy2()); + royaltyModule.registerExternalRoyaltyPolicy(mockExternalRoyaltyPolicy1); + royaltyModule.registerExternalRoyaltyPolicy(mockExternalRoyaltyPolicy2); + + vm.startPrank(address(licensingModule)); + _setupTree(); + vm.stopPrank(); + } + + function _setupTree() internal { + // mint license for roots + royaltyModule.onLicenseMinting(address(10), address(royaltyPolicyLAP), uint32(10 * 10 ** 6), ""); + royaltyModule.onLicenseMinting(address(20), address(royaltyPolicyLAP), uint32(10 * 10 ** 6), ""); + royaltyModule.onLicenseMinting(address(30), address(royaltyPolicyLRP), uint32(7 * 10 ** 6), ""); + + // link 40 to parents + address[] memory parents = new address[](3); + address[] memory licenseRoyaltyPolicies = new address[](3); + uint32[] memory parentRoyalties = new uint32[](3); + parents[0] = address(10); + parents[1] = address(20); + parents[2] = address(30); + licenseRoyaltyPolicies[0] = address(royaltyPolicyLAP); + licenseRoyaltyPolicies[1] = address(royaltyPolicyLAP); + licenseRoyaltyPolicies[2] = address(royaltyPolicyLRP); + parentRoyalties[0] = uint32(10 * 10 ** 6); + parentRoyalties[1] = uint32(10 * 10 ** 6); + parentRoyalties[2] = uint32(7 * 10 ** 6); + ipGraph.addParentIp(address(40), parents); + royaltyModule.onLinkToParents(address(40), parents, licenseRoyaltyPolicies, parentRoyalties, ""); + + // mint license for 40 + royaltyModule.onLicenseMinting(address(40), address(royaltyPolicyLRP), uint32(5 * 10 ** 6), ""); + + // link 50 to 40 + parents = new address[](1); + licenseRoyaltyPolicies = new address[](1); + parentRoyalties = new uint32[](1); + parents[0] = address(40); + licenseRoyaltyPolicies[0] = address(royaltyPolicyLRP); + parentRoyalties[0] = uint32(5 * 10 ** 6); + ipGraph.addParentIp(address(50), parents); + royaltyModule.onLinkToParents(address(50), parents, licenseRoyaltyPolicies, parentRoyalties, ""); + + // mint license for 50 + royaltyModule.onLicenseMinting(address(50), address(royaltyPolicyLRP), uint32(15 * 10 ** 6), ""); + + // link 60 to 50 + parents = new address[](1); + licenseRoyaltyPolicies = new address[](1); + parentRoyalties = new uint32[](1); + parents[0] = address(50); + licenseRoyaltyPolicies[0] = address(royaltyPolicyLRP); + parentRoyalties[0] = uint32(15 * 10 ** 6); + ipGraph.addParentIp(address(60), parents); + royaltyModule.onLinkToParents(address(60), parents, licenseRoyaltyPolicies, parentRoyalties, ""); + + // mint license for 60 + royaltyModule.onLicenseMinting(address(60), address(mockExternalRoyaltyPolicy1), uint32(12 * 10 ** 6), ""); + + // link 70 to 60 + parents = new address[](1); + licenseRoyaltyPolicies = new address[](1); + parentRoyalties = new uint32[](1); + parents[0] = address(60); + licenseRoyaltyPolicies[0] = address(mockExternalRoyaltyPolicy1); + parentRoyalties[0] = uint32(12 * 10 ** 6); + ipGraph.addParentIp(address(70), parents); + royaltyModule.onLinkToParents(address(70), parents, licenseRoyaltyPolicies, parentRoyalties, ""); + } + + function test_RoyaltyPolicyLRP_constructor_revert_ZeroRoyaltyModule() public { + vm.expectRevert(Errors.RoyaltyPolicyLRP__ZeroRoyaltyModule.selector); + new RoyaltyPolicyLRP(address(0)); + } + + function test_RoyaltyPolicyLRP_initialize_revert_ZeroAccessManager() public { + address impl = address(new RoyaltyPolicyLRP(address(royaltyModule))); + vm.expectRevert(Errors.RoyaltyPolicyLRP__ZeroAccessManager.selector); + RoyaltyPolicyLRP( + TestProxyHelper.deployUUPSProxy(impl, abi.encodeCall(RoyaltyPolicyLRP.initialize, (address(0)))) + ); + } + + function test_RoyaltyPolicyLRP_constructor() public { + testRoyaltyPolicyLRP = new RoyaltyPolicyLRP(address(royaltyModule)); + assertEq(address(testRoyaltyPolicyLRP.ROYALTY_MODULE()), address(royaltyModule)); + } + + function test_RoyaltyPolicyLRP_onLicenseMinting_revert_NotRoyaltyModule() public { + vm.startPrank(address(1)); + vm.expectRevert(Errors.RoyaltyPolicyLRP__NotRoyaltyModule.selector); + royaltyPolicyLRP.onLicenseMinting(address(80), uint32(10 * 10 ** 6), ""); + } + + function test_RoyaltyPolicyLRP_onLinkToParents() public { + address[] memory parents = new address[](3); + address[] memory licenseRoyaltyPolicies = new address[](3); + uint32[] memory parentRoyalties = new uint32[](3); + parents[0] = address(10); + parents[1] = address(20); + parents[2] = address(30); + licenseRoyaltyPolicies[0] = address(royaltyPolicyLRP); + licenseRoyaltyPolicies[1] = address(royaltyPolicyLRP); + licenseRoyaltyPolicies[2] = address(royaltyPolicyLRP); + parentRoyalties[0] = uint32(10 * 10 ** 6); + parentRoyalties[1] = uint32(15 * 10 ** 6); + parentRoyalties[2] = uint32(20 * 10 ** 6); + ipGraph.addParentIp(address(80), parents); + + vm.startPrank(address(licensingModule)); + royaltyModule.onLinkToParents(address(80), parents, licenseRoyaltyPolicies, parentRoyalties, ""); + + address ipRoyaltyVault = royaltyModule.ipRoyaltyVaults(address(80)); + + uint256 ipId10Balance = IERC20(ipRoyaltyVault).balanceOf(royaltyModule.ipRoyaltyVaults(address(10))); + uint256 ipId20Balance = IERC20(ipRoyaltyVault).balanceOf(royaltyModule.ipRoyaltyVaults(address(20))); + uint256 ipId30Balance = IERC20(ipRoyaltyVault).balanceOf(royaltyModule.ipRoyaltyVaults(address(30))); + uint256 ipId80Balance = IERC20(ipRoyaltyVault).balanceOf(address(80)); + + assertEq(ipId10Balance, 10 * 10 ** 6); + assertEq(ipId20Balance, 15 * 10 ** 6); + assertEq(ipId30Balance, 20 * 10 ** 6); + assertEq(ipId80Balance, 55 * 10 ** 6); + } +} diff --git a/test/foundry/modules/royalty/RoyaltyModule.t.sol b/test/foundry/modules/royalty/RoyaltyModule.t.sol index 1306750e..83ee3939 100644 --- a/test/foundry/modules/royalty/RoyaltyModule.t.sol +++ b/test/foundry/modules/royalty/RoyaltyModule.t.sol @@ -1,17 +1,21 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.23; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { ERC6551AccountLib } from "erc6551/lib/ERC6551AccountLib.sol"; import { PausableUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; // contracts import { Errors } from "../../../../contracts/lib/Errors.sol"; import { RoyaltyModule } from "../../../../contracts/modules/royalty/RoyaltyModule.sol"; -import { RoyaltyPolicyLAP } from "../../../../contracts/modules/royalty/policies/RoyaltyPolicyLAP.sol"; // tests import { BaseTest } from "../../utils/BaseTest.t.sol"; import { TestProxyHelper } from "../../utils/TestProxyHelper.sol"; +import { MockExternalRoyaltyPolicy1 } from "../../mocks/policy/MockExternalRoyaltyPolicy1.sol"; +import { MockExternalRoyaltyPolicy2 } from "../../mocks/policy/MockExternalRoyaltyPolicy2.sol"; +import { MockERC721 } from "../../mocks/token/MockERC721.sol"; +import { MockEvenSplitGroupPool } from "../../mocks/grouping/MockEvenSplitGroupPool.sol"; contract TestRoyaltyModule is BaseTest { event RoyaltyPolicyWhitelistUpdated(address royaltyPolicy, bool allowed); @@ -19,45 +23,41 @@ contract TestRoyaltyModule is BaseTest { event RoyaltyPolicySet(address ipId, address royaltyPolicy, bytes data); event RoyaltyPaid(address receiverIpId, address payerIpId, address sender, address token, uint256 amount); event LicenseMintingFeePaid(address receiverIpId, address payerAddress, address token, uint256 amount); + event RoyaltyVaultAddedToIp(address ipId, address ipRoyaltyVault); + event ExternalRoyaltyPolicyRegistered(address externalRoyaltyPolicy); address internal ipAccount1 = address(0x111000aaa); address internal ipAccount2 = address(0x111000bbb); address internal ipAddr; address internal arbitrationRelayer; - RoyaltyPolicyLAP internal royaltyPolicyLAP2; + address internal mockExternalRoyaltyPolicy1; + address internal mockExternalRoyaltyPolicy2; + + // grouping + MockERC721 internal mockNft = new MockERC721("MockERC721"); + address public ipId1; + address public ipOwner1 = address(0x111); + uint256 public tokenId1 = 1; + MockEvenSplitGroupPool public rewardPool; function setUp() public override { super.setUp(); + USDC.mint(ipAccount1, 1000 * 10 ** 6); // 1000 USDC USDC.mint(ipAccount2, 1000 * 10 ** 6); // 1000 USDC - address impl = address( - new RoyaltyPolicyLAP(address(royaltyModule), address(licensingModule), address(ipGraphACL)) - ); - royaltyPolicyLAP2 = RoyaltyPolicyLAP( - TestProxyHelper.deployUUPSProxy( - impl, - abi.encodeCall(RoyaltyPolicyLAP.initialize, address(protocolAccessManager)) - ) - ); - arbitrationRelayer = u.relayer; - vm.startPrank(u.admin); - // whitelist royalty policy - royaltyModule.whitelistRoyaltyPolicy(address(royaltyPolicyLAP), true); - - // whitelist royalty token - royaltyModule.whitelistRoyaltyToken(address(USDC), true); - vm.stopPrank(); + // register external royalty policies + mockExternalRoyaltyPolicy1 = address(new MockExternalRoyaltyPolicy1()); + mockExternalRoyaltyPolicy2 = address(new MockExternalRoyaltyPolicy2()); + royaltyModule.registerExternalRoyaltyPolicy(mockExternalRoyaltyPolicy1); + royaltyModule.registerExternalRoyaltyPolicy(mockExternalRoyaltyPolicy2); vm.startPrank(address(licensingModule)); - // split made to avoid stack too deep error _setupTree(); vm.stopPrank(); - USDC.mint(ipAccount1, 1000 * 10 ** 6); - registerSelectedPILicenseTerms_Commercial({ selectionName: "cheap_flexible", transferable: true, @@ -88,34 +88,187 @@ contract TestRoyaltyModule is BaseTest { vm.startPrank(ipAddr); disputeModule.setArbitrationPolicy(ipAddr, address(arbitrationPolicySP)); vm.stopPrank(); + + // grouping + mockNft.mintId(ipOwner1, tokenId1); + ipId1 = ipAssetRegistry.register(block.chainid, address(mockNft), tokenId1); + rewardPool = new MockEvenSplitGroupPool(); + vm.prank(admin); + groupingModule.whitelistGroupRewardPool(address(rewardPool)); } function _setupTree() internal { - // init royalty policy for roots - royaltyModule.onLicenseMinting(address(2), address(royaltyPolicyLAP), abi.encode(uint32(7)), ""); - royaltyModule.onLicenseMinting(address(8), address(royaltyPolicyLAP), abi.encode(uint32(8)), ""); - - // init 2nd level with children - address[] memory parents = new address[](2); - uint32[] memory parentRoyalties = new uint32[](2); - bytes[] memory encodedLicenseData = new bytes[](2); - - // 3 is child of 2 and 8 - parents[0] = address(2); - parents[1] = address(8); - parentRoyalties[0] = 7; - parentRoyalties[1] = 8; - for (uint32 i = 0; i < parentRoyalties.length; i++) { - encodedLicenseData[i] = abi.encode(parentRoyalties[i]); - } - royaltyModule.onLinkToParents(address(3), address(royaltyPolicyLAP), parents, encodedLicenseData, ""); + // mint license for roots + royaltyModule.onLicenseMinting(address(10), address(royaltyPolicyLAP), uint32(10 * 10 ** 6), ""); + royaltyModule.onLicenseMinting(address(20), address(royaltyPolicyLAP), uint32(10 * 10 ** 6), ""); + royaltyModule.onLicenseMinting(address(30), address(royaltyPolicyLRP), uint32(7 * 10 ** 6), ""); + + // link 40 to parents + address[] memory parents = new address[](3); + address[] memory licenseRoyaltyPolicies = new address[](3); + uint32[] memory parentRoyalties = new uint32[](3); + parents[0] = address(10); + parents[1] = address(20); + parents[2] = address(30); + licenseRoyaltyPolicies[0] = address(royaltyPolicyLAP); + licenseRoyaltyPolicies[1] = address(royaltyPolicyLAP); + licenseRoyaltyPolicies[2] = address(royaltyPolicyLRP); + parentRoyalties[0] = uint32(10 * 10 ** 6); + parentRoyalties[1] = uint32(10 * 10 ** 6); + parentRoyalties[2] = uint32(7 * 10 ** 6); + ipGraph.addParentIp(address(40), parents); + royaltyModule.onLinkToParents(address(40), parents, licenseRoyaltyPolicies, parentRoyalties, ""); + + // mint license for 40 + royaltyModule.onLicenseMinting(address(40), address(royaltyPolicyLRP), uint32(5 * 10 ** 6), ""); + + // link 50 to 40 + parents = new address[](1); + licenseRoyaltyPolicies = new address[](1); + parentRoyalties = new uint32[](1); + parents[0] = address(40); + licenseRoyaltyPolicies[0] = address(royaltyPolicyLRP); + parentRoyalties[0] = uint32(5 * 10 ** 6); + ipGraph.addParentIp(address(50), parents); + royaltyModule.onLinkToParents(address(50), parents, licenseRoyaltyPolicies, parentRoyalties, ""); + + // mint license for 50 + royaltyModule.onLicenseMinting(address(50), address(royaltyPolicyLRP), uint32(15 * 10 ** 6), ""); + + // link 60 to 50 + parents = new address[](1); + licenseRoyaltyPolicies = new address[](1); + parentRoyalties = new uint32[](1); + parents[0] = address(50); + licenseRoyaltyPolicies[0] = address(royaltyPolicyLRP); + parentRoyalties[0] = uint32(15 * 10 ** 6); + ipGraph.addParentIp(address(60), parents); + royaltyModule.onLinkToParents(address(60), parents, licenseRoyaltyPolicies, parentRoyalties, ""); + + // mint license for 60 + royaltyModule.onLicenseMinting(address(60), address(mockExternalRoyaltyPolicy1), uint32(12 * 10 ** 6), ""); + + // link 70 to 60 + parents = new address[](1); + licenseRoyaltyPolicies = new address[](1); + parentRoyalties = new uint32[](1); + parents[0] = address(60); + licenseRoyaltyPolicies[0] = address(mockExternalRoyaltyPolicy1); + parentRoyalties[0] = uint32(12 * 10 ** 6); + ipGraph.addParentIp(address(70), parents); + royaltyModule.onLinkToParents(address(70), parents, licenseRoyaltyPolicies, parentRoyalties, ""); + + // mint license for 10 + 60 + 70 + royaltyModule.onLicenseMinting(address(10), address(royaltyPolicyLAP), uint32(5 * 10 ** 6), ""); + royaltyModule.onLicenseMinting(address(60), address(royaltyPolicyLRP), uint32(20 * 10 ** 6), ""); + royaltyModule.onLicenseMinting(address(70), address(mockExternalRoyaltyPolicy2), uint32(24 * 10 ** 6), ""); } + function test_RoyaltyModule_initialize_revert_ZeroAccessManager() public { address impl = address( - new RoyaltyModule(address(licensingModule), address(disputeModule), address(licenseRegistry)) + new RoyaltyModule( + address(licensingModule), + address(disputeModule), + address(licenseRegistry), + address(ipAssetRegistry) + ) ); vm.expectRevert(Errors.RoyaltyModule__ZeroAccessManager.selector); - RoyaltyModule(TestProxyHelper.deployUUPSProxy(impl, abi.encodeCall(RoyaltyModule.initialize, address(0)))); + RoyaltyModule( + TestProxyHelper.deployUUPSProxy( + impl, + abi.encodeCall(RoyaltyModule.initialize, (address(0), uint256(8), uint256(1024), uint256(15))) + ) + ); + } + + function test_RoyaltyModule_initialize_revert_ZeroMaxParents() public { + address impl = address( + new RoyaltyModule( + address(licensingModule), + address(disputeModule), + address(licenseRegistry), + address(ipAssetRegistry) + ) + ); + vm.expectRevert(Errors.RoyaltyModule__ZeroMaxParents.selector); + RoyaltyModule( + TestProxyHelper.deployUUPSProxy( + impl, + abi.encodeCall(RoyaltyModule.initialize, (address(1), uint256(0), uint256(1024), uint256(15))) + ) + ); + } + + function test_RoyaltyModule_initialize_revert_ZeroMaxAncestors() public { + address impl = address( + new RoyaltyModule( + address(licensingModule), + address(disputeModule), + address(licenseRegistry), + address(ipAssetRegistry) + ) + ); + vm.expectRevert(Errors.RoyaltyModule__ZeroMaxAncestors.selector); + RoyaltyModule( + TestProxyHelper.deployUUPSProxy( + impl, + abi.encodeCall(RoyaltyModule.initialize, (address(1), uint256(8), uint256(0), uint256(15))) + ) + ); + } + + function test_RoyaltyModule_initialize_revert_ZeroAccumulatedRoyaltyPoliciesLimit() public { + address impl = address( + new RoyaltyModule( + address(licensingModule), + address(disputeModule), + address(licenseRegistry), + address(ipAssetRegistry) + ) + ); + vm.expectRevert(Errors.RoyaltyModule__ZeroAccumulatedRoyaltyPoliciesLimit.selector); + RoyaltyModule( + TestProxyHelper.deployUUPSProxy( + impl, + abi.encodeCall(RoyaltyModule.initialize, (address(1), uint256(8), uint256(1024), uint256(0))) + ) + ); + } + + function test_RoyaltyModule_setIpGraphLimits_revert_ZeroMaxParents() public { + vm.startPrank(u.admin); + vm.expectRevert(Errors.RoyaltyModule__ZeroMaxParents.selector); + + royaltyModule.setIpGraphLimits(0, 1, 10); + } + + function test_RoyaltyModule_setIpGraphLimits_revert_ZeroMaxAncestors() public { + vm.startPrank(u.admin); + vm.expectRevert(Errors.RoyaltyModule__ZeroMaxAncestors.selector); + + royaltyModule.setIpGraphLimits(1, 0, 10); + } + + function test_RoyaltyModule_setIpGraphLimits_revert_ZeroAccumulatedRoyaltyPoliciesLimit() public { + vm.startPrank(u.admin); + vm.expectRevert(Errors.RoyaltyModule__ZeroAccumulatedRoyaltyPoliciesLimit.selector); + + royaltyModule.setIpGraphLimits(1, 1, 0); + } + + function test_RoyaltyModule_setIpGraphLimits() public { + assertEq(royaltyModule.maxParents(), 8); + assertEq(royaltyModule.maxAncestors(), 1024); + assertEq(royaltyModule.maxAccumulatedRoyaltyPolicies(), 15); + + vm.startPrank(u.admin); + royaltyModule.setIpGraphLimits(1, 1, 1); + vm.stopPrank(); + + assertEq(royaltyModule.maxParents(), 1); + assertEq(royaltyModule.maxAncestors(), 1); + assertEq(royaltyModule.maxAccumulatedRoyaltyPolicies(), 1); } function test_RoyaltyModule_whitelistRoyaltyPolicy_revert_ZeroRoyaltyToken() public { @@ -156,136 +309,442 @@ contract TestRoyaltyModule is BaseTest { assertEq(royaltyModule.isWhitelistedRoyaltyToken(address(1)), true); } - function test_RoyaltyModule_onLicenseMinting_revert_NotWhitelistedRoyaltyPolicy() public { + function test_RoyaltyModule_registerExternalRoyaltyPolicy_revert_ZeroExternalRoyaltyPolicy() public { + vm.expectRevert(Errors.RoyaltyModule__PolicyAlreadyWhitelistedOrRegistered.selector); + royaltyModule.registerExternalRoyaltyPolicy(address(royaltyPolicyLAP)); + } + + function test_RoyaltyModule_registerExternalRoyaltyPolicy() public { + address externalRoyaltyPolicy = address(new MockExternalRoyaltyPolicy1()); + assertEq(royaltyModule.isRegisteredExternalRoyaltyPolicy(externalRoyaltyPolicy), false); + + vm.expectEmit(true, true, true, true, address(royaltyModule)); + emit ExternalRoyaltyPolicyRegistered(externalRoyaltyPolicy); + royaltyModule.registerExternalRoyaltyPolicy(externalRoyaltyPolicy); + + assertEq(royaltyModule.isRegisteredExternalRoyaltyPolicy(externalRoyaltyPolicy), true); + } + + function test_RoyaltyModule_onLicenseMinting_revert_RoyaltyModule__NotAllowedCaller() public { + vm.expectRevert(Errors.RoyaltyModule__NotAllowedCaller.selector); + royaltyModule.onLicenseMinting(address(1), address(2), uint32(1), ""); + } + + function test_RoyaltyModule_onLicenseMinting_revert_NotAllowedRoyaltyPolicy() public { address licensor = address(1); - bytes memory licenseData = abi.encode(uint32(15)); + uint32 licensePercent = uint32(15); vm.startPrank(address(licensingModule)); - vm.expectRevert(Errors.RoyaltyModule__NotWhitelistedRoyaltyPolicy.selector); - royaltyModule.onLicenseMinting(licensor, address(1), licenseData, ""); + vm.expectRevert(Errors.RoyaltyModule__NotAllowedRoyaltyPolicy.selector); + royaltyModule.onLicenseMinting(licensor, address(1), licensePercent, ""); } - function test_RoyaltyModule_onLicenseMinting_revert_CanOnlyMintSelectedPolicy() public { - address licensor = address(3); - bytes memory licenseData = abi.encode(uint32(15)); + function test_RoyaltyModule_onLicenseMinting_ZeroRoyaltyPolicy() public { + address licensor = address(1); + uint32 licensePercent = uint32(15); + + vm.startPrank(address(licensingModule)); + vm.expectRevert(Errors.RoyaltyModule__ZeroRoyaltyPolicy.selector); + royaltyModule.onLicenseMinting(licensor, address(0), licensePercent, ""); + } + function test_RoyaltyModule_onLicenseMinting_revert_LastPositionNotAbleToMintLicense() public { vm.startPrank(u.admin); - royaltyModule.whitelistRoyaltyPolicy(address(1), true); + royaltyModule.setIpGraphLimits(1, 1, 15); vm.stopPrank(); + address licensor = address(50); vm.startPrank(address(licensingModule)); - vm.expectRevert(Errors.RoyaltyModule__CanOnlyMintSelectedPolicy.selector); - royaltyModule.onLicenseMinting(licensor, address(1), licenseData, ""); + vm.expectRevert(Errors.RoyaltyModule__LastPositionNotAbleToMintLicense.selector); + royaltyModule.onLicenseMinting(licensor, address(royaltyPolicyLAP), uint32(15), ""); } - function test_RoyaltyModule_onLicenseMinting_Derivative() public { - address licensor = address(3); - bytes memory licenseData = abi.encode(uint32(15)); + function test_RoyaltyModule_onLicenseMinting_revert_RoyaltyModule_AboveRoyaltyTokenSupplyLimit() public { + address licensor = address(1); + uint32 licensePercent = uint32(500 * 10 ** 6); + vm.startPrank(address(licensingModule)); - royaltyModule.onLicenseMinting(licensor, address(royaltyPolicyLAP), licenseData, ""); + vm.expectRevert(Errors.RoyaltyModule__AboveRoyaltyTokenSupplyLimit.selector); + royaltyModule.onLicenseMinting(licensor, address(royaltyPolicyLAP), licensePercent, ""); } - function test_RoyaltyModule_onLicenseMinting_Root() public { + function test_RoyaltyModule_onLicenseMinting_NewVault() public { address licensor = address(2); - bytes memory licenseData = abi.encode(uint32(15)); + uint32 licensePercent = uint32(15); - // mint a license of another policy vm.startPrank(address(licensingModule)); - royaltyModule.onLicenseMinting(licensor, address(royaltyPolicyLAP), licenseData, ""); - vm.stopPrank(); - vm.startPrank(u.admin); - royaltyModule.whitelistRoyaltyPolicy(address(royaltyPolicyLAP), true); - vm.stopPrank(); + assertEq(royaltyModule.ipRoyaltyVaults(licensor), address(0)); + + royaltyModule.onLicenseMinting(licensor, address(royaltyPolicyLAP), licensePercent, ""); + + address newVault = royaltyModule.ipRoyaltyVaults(licensor); + uint256 ipIdRtBalAfter = IERC20(newVault).balanceOf(licensor); + + assertEq(ipIdRtBalAfter, royaltyModule.TOTAL_RT_SUPPLY()); + assertFalse(royaltyModule.ipRoyaltyVaults(licensor) == address(0)); + } + + function test_RoyaltyModule_onLicenseMinting_NewVaultGroup() public { + address groupId = groupingModule.registerGroup(address(rewardPool)); + uint32 licensePercent = uint32(15); + + vm.startPrank(address(licensingModule)); + + assertEq(royaltyModule.ipRoyaltyVaults(groupId), address(0)); + + royaltyModule.onLicenseMinting(groupId, address(royaltyPolicyLAP), licensePercent, ""); + + address newVault = royaltyModule.ipRoyaltyVaults(groupId); + uint256 groupPoolRtBalAfter = IERC20(newVault).balanceOf(address(rewardPool)); + + assertEq(groupPoolRtBalAfter, royaltyModule.TOTAL_RT_SUPPLY()); + assertFalse(royaltyModule.ipRoyaltyVaults(groupId) == address(0)); + } + + function test_RoyaltyModule_onLicenseMinting_ExistingVault() public { + address licensor = address(2); + uint32 licensePercent = uint32(15); + + vm.startPrank(address(licensingModule)); + royaltyModule.onLicenseMinting(licensor, address(royaltyPolicyLAP), licensePercent, ""); + + address ipRoyaltyVaultBefore = royaltyModule.ipRoyaltyVaults(licensor); + uint256 ipIdRtBalBefore = IERC20(ipRoyaltyVaultBefore).balanceOf(licensor); + + royaltyModule.onLicenseMinting(licensor, address(royaltyPolicyLAP), licensePercent, ""); + + address ipRoyaltyVaultAfter = royaltyModule.ipRoyaltyVaults(licensor); + address newVault = royaltyModule.ipRoyaltyVaults(licensor); + uint256 ipIdRtBalAfter = IERC20(newVault).balanceOf(licensor); + + assertEq(ipIdRtBalBefore - ipIdRtBalAfter, 0); + assertEq(ipRoyaltyVaultBefore, ipRoyaltyVaultAfter); + } + + function test_RoyaltyModule_onLinkToParents_revert_NotAllowedCaller() public { + vm.expectRevert(Errors.RoyaltyModule__NotAllowedCaller.selector); + royaltyModule.onLinkToParents(address(1), new address[](0), new address[](0), new uint32[](0), ""); + } + + function test_RoyaltyModule_onLinkToParents_revert_ZeroRoyaltyPolicy() public { + address[] memory parents = new address[](3); + address[] memory licenseRoyaltyPolicies = new address[](3); + uint32[] memory parentRoyalties = new uint32[](3); + + // link 80 to 10 + 60 + 70 + parents = new address[](3); + licenseRoyaltyPolicies = new address[](3); + parentRoyalties = new uint32[](3); + parents[0] = address(10); + parents[1] = address(60); + parents[2] = address(70); + licenseRoyaltyPolicies[0] = address(0); + licenseRoyaltyPolicies[1] = address(royaltyPolicyLRP); + licenseRoyaltyPolicies[2] = address(mockExternalRoyaltyPolicy2); + parentRoyalties[0] = uint32(5 * 10 ** 6); + parentRoyalties[1] = uint32(17 * 10 ** 6); + parentRoyalties[2] = uint32(24 * 10 ** 6); vm.startPrank(address(licensingModule)); - royaltyModule.onLicenseMinting(licensor, address(royaltyPolicyLAP), licenseData, ""); - } - - function test_RoyaltyModule_onLinkToParents_revert_NotWhitelistedRoyaltyPolicy() public { - address newChild = address(9); - address[] memory parents = new address[](2); - uint32[] memory parentRoyalties = new uint32[](2); - bytes[] memory encodedLicenseData = new bytes[](2); - parents[0] = address(2); - parents[1] = address(8); - parentRoyalties[0] = 7; - parentRoyalties[1] = 8; - for (uint32 i = 0; i < parentRoyalties.length; i++) { - encodedLicenseData[i] = abi.encode(parentRoyalties[i]); - } + vm.expectRevert(Errors.RoyaltyModule__ZeroRoyaltyPolicy.selector); + royaltyModule.onLinkToParents(address(80), parents, licenseRoyaltyPolicies, parentRoyalties, ""); + } + + function test_RoyaltyModule_onLinkToParents_revert_UnlinkableToParents() public { + address[] memory parents = new address[](3); + address[] memory licenseRoyaltyPolicies = new address[](3); + uint32[] memory parentRoyalties = new uint32[](3); + + // link 80 to 10 + 60 + 70 + parents = new address[](3); + licenseRoyaltyPolicies = new address[](3); + parentRoyalties = new uint32[](3); + parents[0] = address(10); + parents[1] = address(60); + parents[2] = address(70); + licenseRoyaltyPolicies[0] = address(royaltyPolicyLAP); + licenseRoyaltyPolicies[1] = address(royaltyPolicyLRP); + licenseRoyaltyPolicies[2] = address(mockExternalRoyaltyPolicy2); + parentRoyalties[0] = uint32(5 * 10 ** 6); + parentRoyalties[1] = uint32(17 * 10 ** 6); + parentRoyalties[2] = uint32(24 * 10 ** 6); + vm.startPrank(address(licensingModule)); - vm.expectRevert(Errors.RoyaltyModule__NotWhitelistedRoyaltyPolicy.selector); - royaltyModule.onLinkToParents(newChild, address(1), parents, encodedLicenseData, ""); + royaltyModule.onLicenseMinting(address(80), address(royaltyPolicyLAP), uint32(10 * 10 ** 6), ""); + vm.expectRevert(Errors.RoyaltyModule__UnlinkableToParents.selector); + royaltyModule.onLinkToParents(address(80), parents, licenseRoyaltyPolicies, parentRoyalties, ""); } function test_RoyaltyModule_onLinkToParents_revert_NoParentsOnLinking() public { - address newChild = address(9); - address[] memory parents = new address[](0); - uint32[] memory parentRoyalties = new uint32[](2); - bytes[] memory encodedLicenseData = new bytes[](2); - parentRoyalties[0] = 7; - parentRoyalties[1] = 8; - for (uint32 i = 0; i < parentRoyalties.length; i++) { - encodedLicenseData[i] = abi.encode(parentRoyalties[i]); - } + address[] memory parents = new address[](3); + address[] memory licenseRoyaltyPolicies = new address[](3); + uint32[] memory parentRoyalties = new uint32[](3); + + // link 80 to 10 + 60 + 70 + parents = new address[](3); + licenseRoyaltyPolicies = new address[](3); + parentRoyalties = new uint32[](3); + parents[0] = address(10); + parents[1] = address(60); + parents[2] = address(70); + licenseRoyaltyPolicies[0] = address(royaltyPolicyLAP); + licenseRoyaltyPolicies[1] = address(royaltyPolicyLRP); + licenseRoyaltyPolicies[2] = address(mockExternalRoyaltyPolicy2); + parentRoyalties[0] = uint32(5 * 10 ** 6); + parentRoyalties[1] = uint32(17 * 10 ** 6); + parentRoyalties[2] = uint32(24 * 10 ** 6); + vm.startPrank(address(licensingModule)); vm.expectRevert(Errors.RoyaltyModule__NoParentsOnLinking.selector); - royaltyModule.onLinkToParents(newChild, address(royaltyPolicyLAP), parents, encodedLicenseData, ""); - } - - function test_RoyaltyModule_onLinkToParents_revert_IncompatibleRoyaltyPolicy() public { - address newChild = address(9); - address[] memory parents = new address[](2); - uint32[] memory parentRoyalties = new uint32[](1); - bytes[] memory encodedLicenseData = new bytes[](2); - parents[0] = address(3); - parentRoyalties[0] = 3; - for (uint32 i = 0; i < parentRoyalties.length; i++) { - encodedLicenseData[i] = abi.encode(parentRoyalties[i]); - } + royaltyModule.onLinkToParents(address(80), new address[](0), licenseRoyaltyPolicies, parentRoyalties, ""); + } + + function test_RoyaltyModule_onLinkToParents_revert_RoyaltyModule_AboveParentLimit() public { + address[] memory parents = new address[](3); + address[] memory licenseRoyaltyPolicies = new address[](3); + uint32[] memory parentRoyalties = new uint32[](3); + + // link 80 to 10 + 60 + 70 + parents = new address[](10); + licenseRoyaltyPolicies = new address[](3); + parentRoyalties = new uint32[](3); + parents[0] = address(10); + parents[1] = address(60); + parents[2] = address(70); + licenseRoyaltyPolicies[0] = address(royaltyPolicyLAP); + licenseRoyaltyPolicies[1] = address(royaltyPolicyLRP); + licenseRoyaltyPolicies[2] = address(mockExternalRoyaltyPolicy2); + parentRoyalties[0] = uint32(5 * 10 ** 6); + parentRoyalties[1] = uint32(17 * 10 ** 6); + parentRoyalties[2] = uint32(24 * 10 ** 6); + + vm.startPrank(address(licensingModule)); + vm.expectRevert(Errors.RoyaltyModule__AboveParentLimit.selector); + royaltyModule.onLinkToParents(address(80), parents, licenseRoyaltyPolicies, parentRoyalties, ""); + } + + function test_RoyaltyModule_onLinkToParents_revert_AboveAncestorsLimit() public { + address[] memory parents = new address[](3); + address[] memory licenseRoyaltyPolicies = new address[](3); + uint32[] memory parentRoyalties = new uint32[](3); + vm.startPrank(u.admin); - royaltyModule.whitelistRoyaltyPolicy(address(royaltyPolicyLAP2), true); + royaltyModule.setIpGraphLimits(3, 2, 15); vm.stopPrank(); + // link 80 to 10 + 60 + 70 + parents = new address[](3); + licenseRoyaltyPolicies = new address[](3); + parentRoyalties = new uint32[](3); + parents[0] = address(10); + parents[1] = address(60); + parents[2] = address(70); + licenseRoyaltyPolicies[0] = address(royaltyPolicyLAP); + licenseRoyaltyPolicies[1] = address(royaltyPolicyLRP); + licenseRoyaltyPolicies[2] = address(mockExternalRoyaltyPolicy2); + parentRoyalties[0] = uint32(5 * 10 ** 6); + parentRoyalties[1] = uint32(17 * 10 ** 6); + parentRoyalties[2] = uint32(24 * 10 ** 6); + vm.startPrank(address(licensingModule)); - vm.expectRevert(Errors.RoyaltyModule__IncompatibleRoyaltyPolicy.selector); - royaltyModule.onLinkToParents(newChild, address(royaltyPolicyLAP2), parents, encodedLicenseData, ""); + ipGraph.addParentIp(address(80), parents); + vm.expectRevert(Errors.RoyaltyModule__AboveAncestorsLimit.selector); + royaltyModule.onLinkToParents(address(80), parents, licenseRoyaltyPolicies, parentRoyalties, ""); } - function test_RoyaltyModule_onLinkToParents() public { - address newChild = address(9); - - // new child is linked to 7 and 8 - address[] memory parents = new address[](2); - uint32[] memory parentRoyalties = new uint32[](2); - bytes[] memory encodedLicenseData = new bytes[](2); - parents[0] = address(2); - parents[1] = address(8); - parentRoyalties[0] = 7; - parentRoyalties[1] = 8; - for (uint32 i = 0; i < parentRoyalties.length; i++) { - encodedLicenseData[i] = abi.encode(parentRoyalties[i]); - } + function test_RoyaltyModule_onLinkToParents_revert_RoyaltyModule_AboveRoyaltyTokenSupplyLimit() public { + address[] memory parents = new address[](3); + address[] memory licenseRoyaltyPolicies = new address[](3); + uint32[] memory parentRoyalties = new uint32[](3); + + // link 80 to 10 + 60 + 70 + parents = new address[](3); + licenseRoyaltyPolicies = new address[](3); + parentRoyalties = new uint32[](3); + parents[0] = address(10); + parents[1] = address(60); + parents[2] = address(70); + licenseRoyaltyPolicies[0] = address(royaltyPolicyLAP); + licenseRoyaltyPolicies[1] = address(royaltyPolicyLRP); + licenseRoyaltyPolicies[2] = address(mockExternalRoyaltyPolicy2); + parentRoyalties[0] = uint32(500 * 10 ** 6); + parentRoyalties[1] = uint32(17 * 10 ** 6); + parentRoyalties[2] = uint32(24 * 10 ** 6); + vm.startPrank(address(licensingModule)); - royaltyModule.onLinkToParents(newChild, address(royaltyPolicyLAP), parents, encodedLicenseData, ""); + ipGraph.addParentIp(address(80), parents); - assertEq(royaltyModule.royaltyPolicies(newChild), address(royaltyPolicyLAP)); + vm.expectRevert(Errors.RoyaltyModule__AboveRoyaltyTokenSupplyLimit.selector); + royaltyModule.onLinkToParents(address(80), parents, licenseRoyaltyPolicies, parentRoyalties, ""); } - function test_RoyaltyModule_payRoyaltyOnBehalf_revert_NoRoyaltyPolicySet() public { - vm.expectRevert(Errors.RoyaltyModule__NoRoyaltyPolicySet.selector); + function test_RoyaltyModule_onLinkToParents_revert_RoyaltyModule_NotWhitelistedOrRegisteredRoyaltyPolicy() public { + address[] memory parents = new address[](3); + address[] memory licenseRoyaltyPolicies = new address[](3); + uint32[] memory parentRoyalties = new uint32[](3); + + address nonRegisteredRoyaltyPolicy = address(new MockExternalRoyaltyPolicy1()); + + // link 80 to 10 + 60 + 70 + parents = new address[](3); + licenseRoyaltyPolicies = new address[](3); + parentRoyalties = new uint32[](3); + parents[0] = address(10); + parents[1] = address(60); + parents[2] = address(70); + licenseRoyaltyPolicies[0] = address(royaltyPolicyLAP); + licenseRoyaltyPolicies[1] = address(royaltyPolicyLRP); + licenseRoyaltyPolicies[2] = address(nonRegisteredRoyaltyPolicy); + parentRoyalties[0] = uint32(5 * 10 ** 6); + parentRoyalties[1] = uint32(17 * 10 ** 6); + parentRoyalties[2] = uint32(1 * 10 ** 6); - royaltyModule.payRoyaltyOnBehalf(ipAccount1, ipAccount2, address(USDC), 100); + vm.startPrank(address(licensingModule)); + ipGraph.addParentIp(address(80), parents); + + vm.expectRevert(Errors.RoyaltyModule__NotWhitelistedOrRegisteredRoyaltyPolicy.selector); + royaltyModule.onLinkToParents(address(80), parents, licenseRoyaltyPolicies, parentRoyalties, ""); } - function test_RoyaltyModule_payRoyaltyOnBehalf_revert_NotWhitelistedRoyaltyToken() public { - uint256 royaltyAmount = 100 * 10 ** 6; - address receiverIpId = address(2); - address payerIpId = address(3); + function test_RoyaltyModule_onLinkToParents_revert_AboveAccumulatedRoyaltyPoliciesLimit() public { + vm.startPrank(u.admin); + royaltyModule.setIpGraphLimits(8, 1024, 3); + vm.stopPrank(); - vm.expectRevert(Errors.RoyaltyModule__NotWhitelistedRoyaltyToken.selector); - royaltyModule.payRoyaltyOnBehalf(receiverIpId, payerIpId, address(1), royaltyAmount); + address[] memory parents = new address[](3); + address[] memory licenseRoyaltyPolicies = new address[](3); + uint32[] memory parentRoyalties = new uint32[](3); + + // link 80 to 10 + 60 + 70 + parents = new address[](3); + licenseRoyaltyPolicies = new address[](3); + parentRoyalties = new uint32[](3); + parents[0] = address(10); + parents[1] = address(60); + parents[2] = address(70); + licenseRoyaltyPolicies[0] = address(royaltyPolicyLAP); + licenseRoyaltyPolicies[1] = address(royaltyPolicyLRP); + licenseRoyaltyPolicies[2] = address(mockExternalRoyaltyPolicy2); + parentRoyalties[0] = uint32(5 * 10 ** 6); + parentRoyalties[1] = uint32(17 * 10 ** 6); + parentRoyalties[2] = uint32(24 * 10 ** 6); + + vm.startPrank(address(licensingModule)); + ipGraph.addParentIp(address(80), parents); + + vm.expectRevert(Errors.RoyaltyModule__AboveAccumulatedRoyaltyPoliciesLimit.selector); + royaltyModule.onLinkToParents(address(80), parents, licenseRoyaltyPolicies, parentRoyalties, ""); + } + + function test_RoyaltyModule_onLinkToParents() public { + address[] memory parents = new address[](3); + address[] memory licenseRoyaltyPolicies = new address[](3); + uint32[] memory parentRoyalties = new uint32[](3); + + // link 80 to 10 + 60 + 70 + parents = new address[](3); + licenseRoyaltyPolicies = new address[](3); + parentRoyalties = new uint32[](3); + parents[0] = address(10); + parents[1] = address(60); + parents[2] = address(70); + licenseRoyaltyPolicies[0] = address(royaltyPolicyLAP); + licenseRoyaltyPolicies[1] = address(royaltyPolicyLRP); + licenseRoyaltyPolicies[2] = address(mockExternalRoyaltyPolicy2); + parentRoyalties[0] = uint32(5 * 10 ** 6); + parentRoyalties[1] = uint32(17 * 10 ** 6); + parentRoyalties[2] = uint32(24 * 10 ** 6); + + vm.startPrank(address(licensingModule)); + ipGraph.addParentIp(address(80), parents); + + assertEq(royaltyModule.ipRoyaltyVaults(address(80)), address(0)); + + royaltyModule.onLinkToParents(address(80), parents, licenseRoyaltyPolicies, parentRoyalties, ""); + + address ipRoyaltyVault80 = royaltyModule.ipRoyaltyVaults(address(80)); + uint256 ipId80RtLAPBalAfter = IERC20(ipRoyaltyVault80).balanceOf(address(royaltyPolicyLAP)); + uint256 ipId80RtLRPBalAfter = IERC20(ipRoyaltyVault80).balanceOf(address(royaltyPolicyLRP)); + uint256 ipId80RtLRPParentVaultBalAfter = IERC20(ipRoyaltyVault80).balanceOf( + royaltyModule.ipRoyaltyVaults(address(60)) + ); + uint256 ipId80RtExternal1BalAfter = IERC20(ipRoyaltyVault80).balanceOf(mockExternalRoyaltyPolicy1); + uint256 ipId80RtExternal2BalAfter = IERC20(ipRoyaltyVault80).balanceOf(mockExternalRoyaltyPolicy2); + uint256 ipId80IpIdRtBalAfter = IERC20(ipRoyaltyVault80).balanceOf(address(80)); + + assertFalse(royaltyModule.ipRoyaltyVaults(address(80)) == address(0)); + assertEq(ipId80RtLAPBalAfter, 45 * 10 ** 6); + assertEq(ipId80RtLRPBalAfter, 0); + assertEq(ipId80RtLRPParentVaultBalAfter, 17 * 10 ** 6); + assertEq(ipId80RtExternal1BalAfter, 0); + assertEq(ipId80RtExternal2BalAfter, 10 * 10 ** 6); + assertEq(ipId80IpIdRtBalAfter, 28 * 10 ** 6); + + address[] memory accRoyaltyPolicies80After = royaltyModule.accumulatedRoyaltyPolicies(address(80)); + assertEq(accRoyaltyPolicies80After[0], address(royaltyPolicyLAP)); + assertEq(accRoyaltyPolicies80After[1], address(royaltyPolicyLRP)); + assertEq(accRoyaltyPolicies80After[2], address(mockExternalRoyaltyPolicy1)); + assertEq(accRoyaltyPolicies80After[3], address(mockExternalRoyaltyPolicy2)); + + address[] memory accRoyaltyPolicies10After = royaltyModule.accumulatedRoyaltyPolicies(address(10)); + assertEq(accRoyaltyPolicies10After[0], address(royaltyPolicyLAP)); + + address[] memory accRoyaltyPolicies60After = royaltyModule.accumulatedRoyaltyPolicies(address(60)); + assertEq(accRoyaltyPolicies60After[0], address(royaltyPolicyLAP)); + assertEq(accRoyaltyPolicies60After[1], address(royaltyPolicyLRP)); + assertEq(accRoyaltyPolicies60After[2], address(mockExternalRoyaltyPolicy1)); + + address[] memory accRoyaltyPolicies70After = royaltyModule.accumulatedRoyaltyPolicies(address(70)); + assertEq(accRoyaltyPolicies70After[0], address(royaltyPolicyLAP)); + assertEq(accRoyaltyPolicies70After[1], address(royaltyPolicyLRP)); + assertEq(accRoyaltyPolicies70After[2], address(mockExternalRoyaltyPolicy1)); + assertEq(accRoyaltyPolicies70After[3], address(mockExternalRoyaltyPolicy2)); + } + + function test_RoyaltyModule_onLinkToParents_group() public { + address[] memory parents = new address[](3); + address[] memory licenseRoyaltyPolicies = new address[](3); + uint32[] memory parentRoyalties = new uint32[](3); + + // link group ip to 10 + 60 + 70 + parents = new address[](3); + licenseRoyaltyPolicies = new address[](3); + parentRoyalties = new uint32[](3); + parents[0] = address(10); + parents[1] = address(60); + parents[2] = address(70); + licenseRoyaltyPolicies[0] = address(royaltyPolicyLAP); + licenseRoyaltyPolicies[1] = address(royaltyPolicyLRP); + licenseRoyaltyPolicies[2] = address(mockExternalRoyaltyPolicy2); + parentRoyalties[0] = uint32(5 * 10 ** 6); + parentRoyalties[1] = uint32(17 * 10 ** 6); + parentRoyalties[2] = uint32(24 * 10 ** 6); + + vm.startPrank(address(licensingModule)); + address groupId = groupingModule.registerGroup(address(rewardPool)); + ipGraph.addParentIp(groupId, parents); + + assertEq(royaltyModule.ipRoyaltyVaults(groupId), address(0)); + + royaltyModule.onLinkToParents(groupId, parents, licenseRoyaltyPolicies, parentRoyalties, ""); + + address ipRoyaltyVault80 = royaltyModule.ipRoyaltyVaults(groupId); + uint256 ipId80RtLAPBalAfter = IERC20(ipRoyaltyVault80).balanceOf(address(royaltyPolicyLAP)); + uint256 ipId80RtLRPBalAfter = IERC20(ipRoyaltyVault80).balanceOf(address(royaltyPolicyLRP)); + uint256 ipId80RtLRPParentVaultBalAfter = IERC20(ipRoyaltyVault80).balanceOf( + royaltyModule.ipRoyaltyVaults(address(60)) + ); + uint256 ipId80RtExternal1BalAfter = IERC20(ipRoyaltyVault80).balanceOf(mockExternalRoyaltyPolicy1); + uint256 ipId80RtExternal2BalAfter = IERC20(ipRoyaltyVault80).balanceOf(mockExternalRoyaltyPolicy2); + uint256 ipId80GroupPoolRtBalAfter = IERC20(ipRoyaltyVault80).balanceOf(address(rewardPool)); + + assertEq(ipId80RtLAPBalAfter, 45 * 10 ** 6); + assertEq(ipId80RtLRPBalAfter, 0); + assertEq(ipId80RtLRPParentVaultBalAfter, 17 * 10 ** 6); + assertEq(ipId80RtExternal1BalAfter, 0); + assertEq(ipId80RtExternal2BalAfter, 10 * 10 ** 6); + assertEq(ipId80GroupPoolRtBalAfter, 28 * 10 ** 6); } function test_RoyaltyModule_payRoyaltyOnBehalf_revert_IpIsTagged() public { @@ -306,18 +765,6 @@ contract TestRoyaltyModule is BaseTest { royaltyModule.payRoyaltyOnBehalf(ipAccount1, ipAddr, address(USDC), 100); } - function test_RoyaltyModule_payRoyaltyOnBehalf_revert_NotWhitelistedRoyaltyPolicy() public { - uint256 royaltyAmount = 100 * 10 ** 6; - address receiverIpId = address(2); - address payerIpId = address(3); - - vm.startPrank(u.admin); - royaltyModule.whitelistRoyaltyPolicy(address(royaltyPolicyLAP), false); - - vm.expectRevert(Errors.RoyaltyModule__NotWhitelistedRoyaltyPolicy.selector); - royaltyModule.payRoyaltyOnBehalf(receiverIpId, payerIpId, address(USDC), royaltyAmount); - } - function test_RoyaltyModule_payRoyaltyOnBehalf_revert_paused() public { uint256 royaltyAmount = 100 * 10 ** 6; address receiverIpId = address(7); @@ -329,33 +776,20 @@ contract TestRoyaltyModule is BaseTest { royaltyModule.payRoyaltyOnBehalf(receiverIpId, payerIpId, address(USDC), royaltyAmount); } - function test_RoyaltyModule_payRoyaltyOnBehalf_revert_IpIsExpired() public { - uint256 royaltyAmount = 100 * 10 ** 6; - address receiverIpId = address(2); - address payerIpId = address(3); - - (, address ipRoyaltyVault, ) = royaltyPolicyLAP.getRoyaltyData(receiverIpId); - - vm.startPrank(payerIpId); - USDC.mint(payerIpId, royaltyAmount); - USDC.approve(address(royaltyPolicyLAP), royaltyAmount); - - vm.warp(block.timestamp + licenseRegistry.getExpireTime(receiverIpId) + 1); - - vm.expectRevert(Errors.RoyaltyModule__IpIsExpired.selector); - royaltyModule.payRoyaltyOnBehalf(receiverIpId, payerIpId, address(USDC), royaltyAmount); - } - function test_RoyaltyModule_payRoyaltyOnBehalf() public { uint256 royaltyAmount = 100 * 10 ** 6; address receiverIpId = address(2); address payerIpId = address(3); - (, address ipRoyaltyVault, ) = royaltyPolicyLAP.getRoyaltyData(receiverIpId); + // deploy vault + vm.startPrank(address(licensingModule)); + royaltyModule.onLicenseMinting(receiverIpId, address(royaltyPolicyLAP), uint32(10 * 10 ** 6), ""); + address ipRoyaltyVault = royaltyModule.ipRoyaltyVaults(receiverIpId); + vm.stopPrank(); vm.startPrank(payerIpId); USDC.mint(payerIpId, royaltyAmount); - USDC.approve(address(royaltyPolicyLAP), royaltyAmount); + USDC.approve(address(royaltyModule), royaltyAmount); uint256 payerIpIdUSDCBalBefore = USDC.balanceOf(payerIpId); uint256 ipRoyaltyVaultUSDCBalBefore = USDC.balanceOf(ipRoyaltyVault); @@ -372,64 +806,38 @@ contract TestRoyaltyModule is BaseTest { assertEq(ipRoyaltyVaultUSDCBalAfter - ipRoyaltyVaultUSDCBalBefore, royaltyAmount); } - function test_RoyaltyModule_payLicenseMintingFee_revert_NotWhitelistedRoyaltyToken() public { - uint256 royaltyAmount = 100 * 10 ** 6; - address receiverIpId = address(2); - address payerAddress = address(3); - address licenseRoyaltyPolicy = address(royaltyPolicyLAP); - address token = address(1); - vm.startPrank(address(licensingModule)); - vm.expectRevert(Errors.RoyaltyModule__NotWhitelistedRoyaltyToken.selector); - royaltyModule.payLicenseMintingFee(receiverIpId, payerAddress, licenseRoyaltyPolicy, token, royaltyAmount); - } - - function test_RoyaltyModule_payLicenseMintingFee_revert_NotWhitelistedRoyaltyPolicy() public { - uint256 royaltyAmount = 100 * 10 ** 6; - address receiverIpId = address(2); - address payerAddress = address(3); - address licenseRoyaltyPolicy = address(1); - address token = address(USDC); - vm.startPrank(u.admin); - royaltyModule.whitelistRoyaltyPolicy(address(royaltyPolicyLAP), false); - vm.startPrank(address(licensingModule)); - vm.expectRevert(Errors.RoyaltyModule__NotWhitelistedRoyaltyPolicy.selector); - royaltyModule.payLicenseMintingFee(receiverIpId, payerAddress, licenseRoyaltyPolicy, token, royaltyAmount); - } - - function test_RoyaltyModule_payLicenseMintingFee_revert_IpIsExpired() public { - uint256 royaltyAmount = 100 * 10 ** 6; - address receiverIpId = address(2); - address payerAddress = address(3); - address licenseRoyaltyPolicy = address(royaltyPolicyLAP); - address token = address(USDC); - - (, address ipRoyaltyVault, ) = royaltyPolicyLAP.getRoyaltyData(receiverIpId); - - vm.startPrank(payerAddress); - USDC.mint(payerAddress, royaltyAmount); - USDC.approve(address(royaltyPolicyLAP), royaltyAmount); - vm.stopPrank; + function test_RoyaltyModule_payLicenseMintingFee_revert_IpIsTagged() public { + // raise dispute + vm.startPrank(ipAccount1); + USDC.approve(address(arbitrationPolicySP), ARBITRATION_PRICE); + disputeModule.raiseDispute(ipAddr, string("urlExample"), "PLAGIARISM", ""); + vm.stopPrank(); - vm.warp(block.timestamp + licenseRegistry.getExpireTime(receiverIpId) + 1); + // set dispute judgement + vm.startPrank(arbitrationRelayer); + disputeModule.setDisputeJudgement(1, true, ""); vm.startPrank(address(licensingModule)); - vm.expectRevert(Errors.RoyaltyModule__IpIsExpired.selector); - royaltyModule.payLicenseMintingFee(receiverIpId, payerAddress, licenseRoyaltyPolicy, token, royaltyAmount); + + vm.expectRevert(Errors.RoyaltyModule__IpIsTagged.selector); + royaltyModule.payLicenseMintingFee(ipAddr, ipAccount1, address(USDC), 100); } function test_RoyaltyModule_payLicenseMintingFee() public { uint256 royaltyAmount = 100 * 10 ** 6; address receiverIpId = address(2); address payerAddress = address(3); - address licenseRoyaltyPolicy = address(royaltyPolicyLAP); address token = address(USDC); - (, address ipRoyaltyVault, ) = royaltyPolicyLAP.getRoyaltyData(receiverIpId); + vm.startPrank(address(licensingModule)); + royaltyModule.onLicenseMinting(receiverIpId, address(royaltyPolicyLAP), uint32(10 * 10 ** 6), ""); + address ipRoyaltyVault = royaltyModule.ipRoyaltyVaults(receiverIpId); + vm.stopPrank(); vm.startPrank(payerAddress); USDC.mint(payerAddress, royaltyAmount); - USDC.approve(address(royaltyPolicyLAP), royaltyAmount); - vm.stopPrank; + USDC.approve(address(royaltyModule), royaltyAmount); + vm.stopPrank(); uint256 payerAddressUSDCBalBefore = USDC.balanceOf(payerAddress); uint256 ipRoyaltyVaultUSDCBalBefore = USDC.balanceOf(ipRoyaltyVault); @@ -438,7 +846,7 @@ contract TestRoyaltyModule is BaseTest { emit LicenseMintingFeePaid(receiverIpId, payerAddress, address(USDC), royaltyAmount); vm.startPrank(address(licensingModule)); - royaltyModule.payLicenseMintingFee(receiverIpId, payerAddress, licenseRoyaltyPolicy, token, royaltyAmount); + royaltyModule.payLicenseMintingFee(receiverIpId, payerAddress, token, royaltyAmount); uint256 payerAddressUSDCBalAfter = USDC.balanceOf(payerAddress); uint256 ipRoyaltyVaultUSDCBalAfter = USDC.balanceOf(ipRoyaltyVault); diff --git a/test/foundry/modules/royalty/RoyaltyPolicyLAP.t.sol b/test/foundry/modules/royalty/RoyaltyPolicyLAP.t.sol deleted file mode 100644 index 4feaac2f..00000000 --- a/test/foundry/modules/royalty/RoyaltyPolicyLAP.t.sol +++ /dev/null @@ -1,311 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.23; - -import { RoyaltyPolicyLAP } from "../../../../contracts/modules/royalty/policies/RoyaltyPolicyLAP.sol"; -import { Errors } from "../../../../contracts/lib/Errors.sol"; - -import { BaseTest } from "../../utils/BaseTest.t.sol"; -import { IAccessManaged } from "@openzeppelin/contracts/access/manager/IAccessManaged.sol"; - -contract TestRoyaltyPolicyLAP is BaseTest { - event SnapshotIntervalSet(uint256 interval); - event IpRoyaltyVaultBeaconSet(address beacon); - - RoyaltyPolicyLAP internal testRoyaltyPolicyLAP; - - address[] internal MAX_ANCESTORS_ = new address[](14); - uint32[] internal MAX_ANCESTORS_ROYALTY_ = new uint32[](14); - address[] internal parentsIpIds100; - - function setUp() public override { - super.setUp(); - - vm.startPrank(u.admin); - // whitelist royalty policy - royaltyModule.whitelistRoyaltyPolicy(address(royaltyPolicyLAP), true); - vm.stopPrank(); - - vm.startPrank(address(royaltyModule)); - _setupTree(); - vm.stopPrank(); - } - - function _setupTree() internal { - // init royalty policy for roots - royaltyPolicyLAP.onLicenseMinting(address(7), abi.encode(uint32(7)), ""); - royaltyPolicyLAP.onLicenseMinting(address(8), abi.encode(uint32(8)), ""); - royaltyPolicyLAP.onLicenseMinting(address(9), abi.encode(uint32(9)), ""); - royaltyPolicyLAP.onLicenseMinting(address(10), abi.encode(uint32(10)), ""); - royaltyPolicyLAP.onLicenseMinting(address(11), abi.encode(uint32(11)), ""); - royaltyPolicyLAP.onLicenseMinting(address(12), abi.encode(uint32(12)), ""); - royaltyPolicyLAP.onLicenseMinting(address(13), abi.encode(uint32(13)), ""); - royaltyPolicyLAP.onLicenseMinting(address(14), abi.encode(uint32(14)), ""); - - // init 2nd level with children - address[] memory parents = new address[](2); - uint32[] memory parentRoyalties = new uint32[](2); - bytes[] memory encodedLicenseData = new bytes[](2); - - // 3 is child of 7 and 8 - parents[0] = address(7); - parents[1] = address(8); - parentRoyalties[0] = 7; - parentRoyalties[1] = 8; - - for (uint32 i = 0; i < parentRoyalties.length; i++) { - encodedLicenseData[i] = abi.encode(parentRoyalties[i]); - } - ipGraph.addParentIp(address(3), parents); - royaltyPolicyLAP.onLinkToParents(address(3), parents, encodedLicenseData, ""); - - // 4 is child of 9 and 10 - parents[0] = address(9); - parents[1] = address(10); - parentRoyalties[0] = 9; - parentRoyalties[1] = 10; - - for (uint32 i = 0; i < parentRoyalties.length; i++) { - encodedLicenseData[i] = abi.encode(parentRoyalties[i]); - } - ipGraph.addParentIp(address(4), parents); - royaltyPolicyLAP.onLinkToParents(address(4), parents, encodedLicenseData, ""); - - // 5 is child of 11 and 12 - parents[0] = address(11); - parents[1] = address(12); - parentRoyalties[0] = 11; - parentRoyalties[1] = 12; - - for (uint32 i = 0; i < parentRoyalties.length; i++) { - encodedLicenseData[i] = abi.encode(parentRoyalties[i]); - } - ipGraph.addParentIp(address(5), parents); - royaltyPolicyLAP.onLinkToParents(address(5), parents, encodedLicenseData, ""); - - // 6 is child of 13 and 14 - parents[0] = address(13); - parents[1] = address(14); - parentRoyalties[0] = 13; - parentRoyalties[1] = 14; - - for (uint32 i = 0; i < parentRoyalties.length; i++) { - encodedLicenseData[i] = abi.encode(parentRoyalties[i]); - } - ipGraph.addParentIp(address(6), parents); - royaltyPolicyLAP.onLinkToParents(address(6), parents, encodedLicenseData, ""); - - // init 3rd level with children - // 1 is child of 3 and 4 - parents[0] = address(3); - parents[1] = address(4); - parentRoyalties[0] = 3; - parentRoyalties[1] = 4; - - for (uint32 i = 0; i < parentRoyalties.length; i++) { - encodedLicenseData[i] = abi.encode(parentRoyalties[i]); - } - ipGraph.addParentIp(address(1), parents); - royaltyPolicyLAP.onLinkToParents(address(1), parents, encodedLicenseData, ""); - - // 2 is child of 5 and 6 - parents[0] = address(5); - parents[1] = address(6); - parentRoyalties[0] = 5; - parentRoyalties[1] = 6; - - for (uint32 i = 0; i < parentRoyalties.length; i++) { - encodedLicenseData[i] = abi.encode(parentRoyalties[i]); - } - ipGraph.addParentIp(address(2), parents); - royaltyPolicyLAP.onLinkToParents(address(2), parents, encodedLicenseData, ""); - - // ancestors of parent 1 - MAX_ANCESTORS_[0] = address(1); - MAX_ANCESTORS_[1] = address(3); - MAX_ANCESTORS_[2] = address(7); - MAX_ANCESTORS_[3] = address(8); - MAX_ANCESTORS_[4] = address(4); - MAX_ANCESTORS_[5] = address(9); - MAX_ANCESTORS_[6] = address(10); - // ancestors of parent 2 - MAX_ANCESTORS_[7] = address(2); - MAX_ANCESTORS_[8] = address(5); - MAX_ANCESTORS_[9] = address(11); - MAX_ANCESTORS_[10] = address(12); - MAX_ANCESTORS_[11] = address(6); - MAX_ANCESTORS_[12] = address(13); - MAX_ANCESTORS_[13] = address(14); - - MAX_ANCESTORS_ROYALTY_[0] = 1; - MAX_ANCESTORS_ROYALTY_[1] = 3; - MAX_ANCESTORS_ROYALTY_[2] = 7; - MAX_ANCESTORS_ROYALTY_[3] = 8; - MAX_ANCESTORS_ROYALTY_[4] = 4; - MAX_ANCESTORS_ROYALTY_[5] = 9; - MAX_ANCESTORS_ROYALTY_[6] = 10; - MAX_ANCESTORS_ROYALTY_[7] = 2; - MAX_ANCESTORS_ROYALTY_[8] = 5; - MAX_ANCESTORS_ROYALTY_[9] = 11; - MAX_ANCESTORS_ROYALTY_[10] = 12; - MAX_ANCESTORS_ROYALTY_[11] = 6; - MAX_ANCESTORS_ROYALTY_[12] = 13; - MAX_ANCESTORS_ROYALTY_[13] = 14; - - parentsIpIds100 = new address[](2); - parentsIpIds100[0] = address(1); - parentsIpIds100[1] = address(2); - } - - function test_RoyaltyPolicyLAP_setSnapshotInterval_revert_NotOwner() public { - vm.expectRevert(abi.encodeWithSelector(IAccessManaged.AccessManagedUnauthorized.selector, address(this))); - royaltyPolicyLAP.setSnapshotInterval(100); - } - - function test_RoyaltyPolicyLAP_setSnapshotInterval() public { - vm.startPrank(u.admin); - - vm.expectEmit(true, true, true, true, address(royaltyPolicyLAP)); - emit SnapshotIntervalSet(100); - - royaltyPolicyLAP.setSnapshotInterval(100); - assertEq(royaltyPolicyLAP.getSnapshotInterval(), 100); - } - - function test_RoyaltyPolicyLAP_setIpRoyaltyVaultBeacon_revert_NotOwner() public { - vm.expectRevert(abi.encodeWithSelector(IAccessManaged.AccessManagedUnauthorized.selector, address(this))); - royaltyPolicyLAP.setIpRoyaltyVaultBeacon(address(1)); - } - - function testRoyaltyPolicyLAP_setIpRoyaltyVaultBeacon_revert_ZeroIpRoyaltyVaultBeacon() public { - vm.startPrank(u.admin); - vm.expectRevert(Errors.RoyaltyPolicyLAP__ZeroIpRoyaltyVaultBeacon.selector); - royaltyPolicyLAP.setIpRoyaltyVaultBeacon(address(0)); - } - - function test_RoyaltyPolicyLAP_setIpRoyaltyVaultBeacon() public { - vm.startPrank(u.admin); - - vm.expectEmit(true, true, true, true, address(royaltyPolicyLAP)); - emit IpRoyaltyVaultBeaconSet(address(1)); - - royaltyPolicyLAP.setIpRoyaltyVaultBeacon(address(1)); - assertEq(royaltyPolicyLAP.getIpRoyaltyVaultBeacon(), address(1)); - } - - function test_RoyaltyPolicyLAP_onLicenseMinting_revert_NotRoyaltyModule() public { - vm.expectRevert(Errors.RoyaltyPolicyLAP__NotRoyaltyModule.selector); - royaltyPolicyLAP.onLicenseMinting(address(1), abi.encode(uint32(0)), abi.encode(uint32(0))); - } - - function test_RoyaltyPolicyLAP_onLicenseMinting_revert_AboveRoyaltyStackLimit() public { - uint256 excessPercent = royaltyPolicyLAP.TOTAL_RT_SUPPLY() + 1; - vm.prank(address(royaltyModule)); - vm.expectRevert(Errors.RoyaltyPolicyLAP__AboveRoyaltyStackLimit.selector); - royaltyPolicyLAP.onLicenseMinting(address(100), abi.encode(excessPercent), ""); - } - - function test_RoyaltyPolicyLAP_onLicenseMinting_revert_LastPositionNotAbleToMintLicense() public { - uint256 maxAncestors = royaltyPolicyLAP.MAX_ANCESTORS(); - uint256 percentPerAncestor = royaltyPolicyLAP.TOTAL_RT_SUPPLY() / maxAncestors; - uint32 startPoint = 1000; - uint32 endPoint = startPoint + uint32(maxAncestors); - - vm.startPrank(address(royaltyModule)); - - address[] memory parent = new address[](1); - bytes[] memory encodedLicenseData = new bytes[](1); - for (uint32 i = 1 + startPoint; i <= endPoint; i++) { - parent[0] = address(uint160(i - 1)); - encodedLicenseData[0] = abi.encode(uint32(percentPerAncestor)); - - address childAddress = address(uint160(i)); - ipGraph.addParentIp(childAddress, parent); - royaltyPolicyLAP.onLinkToParents(childAddress, parent, encodedLicenseData, ""); - } - - vm.expectRevert(Errors.RoyaltyPolicyLAP__LastPositionNotAbleToMintLicense.selector); - royaltyPolicyLAP.onLicenseMinting(address(uint160(endPoint)), abi.encode(uint32(0)), ""); - vm.stopPrank(); - } - - function test_RoyaltyPolicyLAP_onLicenseMinting() public { - vm.prank(address(royaltyModule)); - royaltyPolicyLAP.onLicenseMinting(address(100), abi.encode(uint32(0)), ""); - - (, address ipRoyaltyVault, uint32 royaltyStack) = royaltyPolicyLAP.getRoyaltyData(address(100)); - - assertEq(royaltyStack, 0); - assertFalse(ipRoyaltyVault == address(0)); - } - - function test_RoyaltyPolicyLAP_onLinkToParents_revert_NotRoyaltyModule() public { - bytes[] memory encodedLicenseData = new bytes[](2); - for (uint32 i = 0; i < parentsIpIds100.length; i++) { - encodedLicenseData[i] = abi.encode(parentsIpIds100[i]); - } - - vm.expectRevert(Errors.RoyaltyPolicyLAP__NotRoyaltyModule.selector); - royaltyPolicyLAP.onLinkToParents(address(100), parentsIpIds100, encodedLicenseData, ""); - } - - function test_RoyaltyPolicyLAP_onLinkToParents_revert_AboveParentLimit() public { - bytes[] memory encodedLicenseData = new bytes[](3); - for (uint32 i = 0; i < 3; i++) { - encodedLicenseData[i] = abi.encode(1); - } - - address[] memory excessParents = new address[](3); - excessParents[0] = address(1); - excessParents[1] = address(2); - excessParents[2] = address(3); - - vm.prank(address(royaltyModule)); - vm.expectRevert(Errors.RoyaltyPolicyLAP__AboveParentLimit.selector); - royaltyPolicyLAP.onLinkToParents(address(100), excessParents, encodedLicenseData, ""); - } - - function test_RoyaltyPolicyLAP_onLinkToParents() public { - bytes[] memory encodedLicenseData = new bytes[](2); - for (uint32 i = 0; i < parentsIpIds100.length; i++) { - encodedLicenseData[i] = abi.encode(parentsIpIds100[i]); - } - ipGraph.addParentIp(address(100), parentsIpIds100); - vm.prank(address(royaltyModule)); - royaltyPolicyLAP.onLinkToParents(address(100), parentsIpIds100, encodedLicenseData, ""); - - (, address ipRoyaltyVault, uint32 royaltyStack) = royaltyPolicyLAP.getRoyaltyData(address(100)); - - assertEq(royaltyStack, 105); - assertFalse(ipRoyaltyVault == address(0)); - } - - function test_RoyaltyPolicyLAP_onRoyaltyPayment_NotRoyaltyModule() public { - vm.stopPrank(); - vm.expectRevert(Errors.RoyaltyPolicyLAP__NotRoyaltyModule.selector); - royaltyPolicyLAP.onRoyaltyPayment(address(1), address(1), address(1), 0); - } - - function test_RoyaltyPolicyLAP_onRoyaltyPayment() public { - (, address ipRoyaltyVault2, ) = royaltyPolicyLAP.getRoyaltyData(address(2)); - uint256 royaltyAmount = 1000 * 10 ** 6; - USDC.mint(address(1), royaltyAmount); - vm.stopPrank(); - - vm.startPrank(address(1)); - USDC.approve(address(royaltyPolicyLAP), royaltyAmount); - vm.stopPrank(); - - vm.startPrank(address(royaltyModule)); - - uint256 ipRoyaltyVault2USDCBalBefore = USDC.balanceOf(ipRoyaltyVault2); - uint256 splitMainUSDCBalBefore = USDC.balanceOf(address(1)); - - royaltyPolicyLAP.onRoyaltyPayment(address(1), address(2), address(USDC), royaltyAmount); - - uint256 ipRoyaltyVault2USDCBalAfter = USDC.balanceOf(ipRoyaltyVault2); - uint256 splitMainUSDCBalAfter = USDC.balanceOf(address(1)); - - assertEq(ipRoyaltyVault2USDCBalAfter - ipRoyaltyVault2USDCBalBefore, royaltyAmount); - assertEq(splitMainUSDCBalBefore - splitMainUSDCBalAfter, royaltyAmount); - } -} diff --git a/test/foundry/modules/royalty/VaultController.t.sol b/test/foundry/modules/royalty/VaultController.t.sol new file mode 100644 index 00000000..b6e71e0c --- /dev/null +++ b/test/foundry/modules/royalty/VaultController.t.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.23; + +import { UpgradeableBeacon } from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; +import { VaultController } from "../../../../contracts/modules/royalty/policies/VaultController.sol"; + +// contracts +import { IpRoyaltyVault } from "../../../../contracts/modules/royalty/policies/IpRoyaltyVault.sol"; +import { Errors } from "../../../../contracts/lib/Errors.sol"; + +// tests +import { BaseTest } from "../../utils/BaseTest.t.sol"; + +contract TestRoyaltyModule is BaseTest { + function setUp() public override { + super.setUp(); + vm.startPrank(u.admin); + } + + function test_VaultController_setSnapshotInterval() public { + uint256 timestampInterval = 100; + royaltyModule.setSnapshotInterval(timestampInterval); + assertEq(royaltyModule.snapshotInterval(), timestampInterval); + } + + function test_VaultController_setIpRoyaltyVaultBeacon_revert_ZeroIpRoyaltyVaultBeacon() public { + vm.expectRevert(Errors.VaultController__ZeroIpRoyaltyVaultBeacon.selector); + royaltyModule.setIpRoyaltyVaultBeacon(address(0)); + } + + function test_VaultController_setIpRoyaltyVaultBeacon() public { + address beacon = address(0x1); + royaltyModule.setIpRoyaltyVaultBeacon(beacon); + assertEq(royaltyModule.ipRoyaltyVaultBeacon(), beacon); + } + + function test_VaultController_upgradeVaults() public { + address newVault = address(new IpRoyaltyVault(address(1), address(2))); + + (bytes32 operationId, uint32 nonce) = protocolAccessManager.schedule( + address(royaltyModule), + abi.encodeCall(VaultController.upgradeVaults, (newVault)), + 0 // earliest time possible, upgraderExecDelay + ); + vm.warp(upgraderExecDelay + 1); + + royaltyModule.upgradeVaults(newVault); + assertEq(UpgradeableBeacon(royaltyModule.ipRoyaltyVaultBeacon()).implementation(), newVault); + } +} diff --git a/test/foundry/upgrades/Upgrades.t.sol b/test/foundry/upgrades/Upgrades.t.sol index 4aeada7b..9fa10c59 100644 --- a/test/foundry/upgrades/Upgrades.t.sol +++ b/test/foundry/upgrades/Upgrades.t.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.23; import { ProtocolAdmin } from "contracts/lib/ProtocolAdmin.sol"; -import { RoyaltyPolicyLAP } from "contracts/modules/royalty/policies/RoyaltyPolicyLAP.sol"; +import { VaultController } from "contracts/modules/royalty/policies/VaultController.sol"; import { BaseTest } from "../utils/BaseTest.t.sol"; @@ -24,21 +24,21 @@ contract UpgradesTest is BaseTest { address newVault = address(new MockIpRoyaltyVaultV2(address(royaltyPolicyLAP), address(disputeModule))); (bool immediate, uint32 delay) = protocolAccessManager.canCall( u.bob, - address(royaltyPolicyLAP), - RoyaltyPolicyLAP.upgradeVaults.selector + address(royaltyModule), + VaultController.upgradeVaults.selector ); assertFalse(immediate); assertEq(delay, execDelay); vm.prank(u.bob); (bytes32 operationId, uint32 nonce) = protocolAccessManager.schedule( - address(royaltyPolicyLAP), - abi.encodeCall(RoyaltyPolicyLAP.upgradeVaults, (newVault)), + address(royaltyModule), + abi.encodeCall(VaultController.upgradeVaults, (newVault)), 0 // earliest time possible, upgraderExecDelay ); vm.warp(upgraderExecDelay + 1); vm.prank(u.bob); - royaltyPolicyLAP.upgradeVaults(newVault); + royaltyModule.upgradeVaults(newVault); assertEq(ipRoyaltyVaultBeacon.implementation(), newVault); } @@ -107,16 +107,13 @@ contract UpgradesTest is BaseTest { (bool immediate, uint32 delay) = protocolAccessManager.canCall( multisig, - address(royaltyPolicyLAP), - RoyaltyPolicyLAP.upgradeVaults.selector + address(royaltyModule), + VaultController.upgradeVaults.selector ); assertFalse(immediate); assertEq(delay, 600); assertEq( - protocolAccessManager.getTargetFunctionRole( - address(royaltyPolicyLAP), - RoyaltyPolicyLAP.upgradeVaults.selector - ), + protocolAccessManager.getTargetFunctionRole(address(royaltyModule), VaultController.upgradeVaults.selector), ProtocolAdmin.UPGRADER_ROLE ); @@ -270,6 +267,21 @@ contract UpgradesTest is BaseTest { ProtocolAdmin.UPGRADER_ROLE ); + (immediate, delay) = protocolAccessManager.canCall( + multisig, + address(royaltyPolicyLRP), + UUPSUpgradeable.upgradeToAndCall.selector + ); + assertFalse(immediate); + assertEq(delay, execDelay); + assertEq( + protocolAccessManager.getTargetFunctionRole( + address(royaltyPolicyLRP), + UUPSUpgradeable.upgradeToAndCall.selector + ), + ProtocolAdmin.UPGRADER_ROLE + ); + (immediate, delay) = protocolAccessManager.canCall( multisig, address(coreMetadataModule), diff --git a/test/foundry/utils/LicensingHelper.t.sol b/test/foundry/utils/LicensingHelper.t.sol index de50b44e..168abeca 100644 --- a/test/foundry/utils/LicensingHelper.t.sol +++ b/test/foundry/utils/LicensingHelper.t.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.23; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { IRoyaltyPolicyLAP } from "../../../contracts/interfaces/modules/royalty/policies/IRoyaltyPolicyLAP.sol"; +import { IRoyaltyPolicyLAP } from "../../../contracts/interfaces/modules/royalty/policies/LAP/IRoyaltyPolicyLAP.sol"; import { PILTerms } from "../../../contracts/interfaces/modules/licensing/IPILicenseTemplate.sol"; import { PILicenseTemplate } from "../../../contracts/modules/licensing/PILicenseTemplate.sol"; import { PILFlavors } from "../../../contracts/lib/PILFlavors.sol";