From 81c9c75e98b62458ac4f1c3fea9d05958b38d398 Mon Sep 17 00:00:00 2001 From: Spablob <99089658+Spablob@users.noreply.github.com> Date: Mon, 1 Apr 2024 07:11:39 +0100 Subject: [PATCH] Replace 0xSplits with new ERC20 Royalty Vault (#26) * add oppenzeppelin v4 contracts * update total token supply comments * remove ancestors vault contract * remove liquid split related interfaces and test adjustments * Add ip pool and LAP royalty policy contract and tests adjustments * rename to IpRoyaltyVault * change oppenzeppellin v4 contracts to upgradeable * add beacon upgradeability to IpRoyaltyVault * add amount to event and adjust claimed event --------- Co-authored-by: Jongwon Park --- .github/workflows/foundry_ci.yml | 2 +- .solhintignore | 1 + Makefile | 3 +- .../royalty/policies/IAncestorsVaultLAP.sol | 24 - .../royalty/policies/IIpRoyaltyVault.sol | 64 +++ .../royalty/policies/ILiquidSplitClone.sol | 25 - .../royalty/policies/ILiquidSplitFactory.sol | 18 - .../royalty/policies/ILiquidSplitMain.sol | 24 - .../royalty/policies/IRoyaltyPolicyLAP.sol | 61 +-- contracts/lib/Errors.sol | 14 +- .../licensing/PILPolicyFrameworkManager.sol | 4 +- contracts/modules/royalty/RoyaltyModule.sol | 2 +- .../royalty/policies/AncestorsVaultLAP.sol | 115 ---- .../royalty/policies/IpRoyaltyVault.sol | 237 ++++++++ .../royalty/policies/RoyaltyPolicyLAP.sol | 271 +++------- package.json | 123 ++--- script/foundry/deployment/Main.s.sol | 176 +----- .../foundry/integration/BaseIntegration.t.sol | 3 + .../integration/flows/royalty/Royalty.t.sol | 132 ++--- .../PILPolicyFramework.derivation.t.sol | 3 - .../modules/royalty/AncestorsVaultLAP.t.sol | 507 ------------------ .../modules/royalty/IpRoyaltyVault.t.sol | 410 ++++++++++++++ .../modules/royalty/RoyaltyModule.t.sol | 24 +- .../modules/royalty/RoyaltyPolicyLAP.t.sol | 147 +---- test/foundry/utils/BaseTest.t.sol | 7 - test/foundry/utils/DeployHelper.t.sol | 24 +- yarn.lock | 5 + 27 files changed, 1021 insertions(+), 1405 deletions(-) create mode 100644 .solhintignore delete mode 100644 contracts/interfaces/modules/royalty/policies/IAncestorsVaultLAP.sol create mode 100644 contracts/interfaces/modules/royalty/policies/IIpRoyaltyVault.sol delete mode 100644 contracts/interfaces/modules/royalty/policies/ILiquidSplitClone.sol delete mode 100644 contracts/interfaces/modules/royalty/policies/ILiquidSplitFactory.sol delete mode 100644 contracts/interfaces/modules/royalty/policies/ILiquidSplitMain.sol delete mode 100644 contracts/modules/royalty/policies/AncestorsVaultLAP.sol create mode 100644 contracts/modules/royalty/policies/IpRoyaltyVault.sol delete mode 100644 test/foundry/modules/royalty/AncestorsVaultLAP.t.sol create mode 100644 test/foundry/modules/royalty/IpRoyaltyVault.t.sol diff --git a/.github/workflows/foundry_ci.yml b/.github/workflows/foundry_ci.yml index 366721e6..74fdaccd 100644 --- a/.github/workflows/foundry_ci.yml +++ b/.github/workflows/foundry_ci.yml @@ -51,7 +51,7 @@ jobs: - name: Upgrade Safety test run: | forge clean && forge build - npx @openzeppelin/upgrades-core validate out/build-info + # npx @openzeppelin/upgrades-core validate out/build-info - name: Run Forge tests run: | diff --git a/.solhintignore b/.solhintignore new file mode 100644 index 00000000..e1ca8973 --- /dev/null +++ b/.solhintignore @@ -0,0 +1 @@ +/contracts/modules/royalty/policies/oppenzeppelin \ No newline at end of file diff --git a/Makefile b/Makefile index 2c00d380..896a835e 100644 --- a/Makefile +++ b/Makefile @@ -36,8 +36,7 @@ slither :; slither ./contracts # glob doesn't work for nested folders, so we do it manually format: - npx prettier --write contracts/*.sol - npx prettier --write contracts/**/*.sol + npx prettier --write contracts # generate forge coverage on pinned mainnet fork # process lcov file, ignore test, script, and contracts/mocks folders diff --git a/contracts/interfaces/modules/royalty/policies/IAncestorsVaultLAP.sol b/contracts/interfaces/modules/royalty/policies/IAncestorsVaultLAP.sol deleted file mode 100644 index 9495723f..00000000 --- a/contracts/interfaces/modules/royalty/policies/IAncestorsVaultLAP.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.23; - -import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; - -import { IRoyaltyPolicyLAP } from "./IRoyaltyPolicyLAP.sol"; - -/// @title Liquid absolute percentage policy ancestor vault interface -interface IAncestorsVaultLAP { - /// @notice Event emitted when a claim is made - /// @param ipId The ipId address - /// @param claimerIpId The claimer ipId address - /// @param tokens The ERC20 tokens to withdraw - event Claimed(address ipId, address claimerIpId, ERC20[] tokens); - - /// @notice Returns the canonical RoyaltyPolicyLAP - function ROYALTY_POLICY_LAP() external view returns (IRoyaltyPolicyLAP); - - /// @notice Claims all available royalty nfts and accrued royalties for an ancestor of a given ipId - /// @param ipId The ipId of the ancestors vault to claim from - /// @param claimerIpId The claimer ipId is the ancestor address that wants to claim - /// @param tokens The ERC20 tokens to withdraw - function claim(address ipId, address claimerIpId, ERC20[] calldata tokens) external; -} diff --git a/contracts/interfaces/modules/royalty/policies/IIpRoyaltyVault.sol b/contracts/interfaces/modules/royalty/policies/IIpRoyaltyVault.sol new file mode 100644 index 00000000..950f82c3 --- /dev/null +++ b/contracts/interfaces/modules/royalty/policies/IIpRoyaltyVault.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +/// @title Ip royalty vault 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 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); + + /// @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 + function initialize( + string memory name, + string memory symbol, + uint32 supply, + uint32 unclaimedTokens, + address ipIdAddress + ) 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 + function addIpRoyaltyVaultTokens(address token) external; + + /// @notice A function to snapshot the claimable revenue and royalty token amounts + /// @return The snapshot id + function snapshot() external returns (uint256); + + /// @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) external view returns (uint256); + + /// @notice Allows token holders to claim revenue token based on the token balance at certain snapshot + /// @param snapshotId The snapshot id + /// @param tokens The list of revenue tokens to claim + function claimRevenueByTokenBatch(uint256 snapshotId, address[] calldata tokens) external; + + /// @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; + + /// @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 Returns the list of revenue tokens in the vault + /// @return The list of revenue tokens + function getVaultTokens() external view returns (address[] memory); +} diff --git a/contracts/interfaces/modules/royalty/policies/ILiquidSplitClone.sol b/contracts/interfaces/modules/royalty/policies/ILiquidSplitClone.sol deleted file mode 100644 index 860628e8..00000000 --- a/contracts/interfaces/modules/royalty/policies/ILiquidSplitClone.sol +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.23; - -/// @title LiquidSplitClone interface -interface ILiquidSplitClone { - /// @notice Distributes funds to the accounts in the LiquidSplitClone contract - /// @param token The token to distribute - /// @param accounts The accounts to distribute to - /// @param distributorAddress The distributor address - function distributeFunds(address token, address[] calldata accounts, address distributorAddress) external; - - /// @notice Transfers rnft tokens - /// @param from The address to transfer from - /// @param to The address to transfer to - /// @param id The token id - /// @param amount The amount to transfer - /// @param data Custom data - function safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes calldata data) external; - - /// @notice Returns the balance of the account - /// @param account The account to check - /// @param id The token id - /// @return balance The balance of the account - function balanceOf(address account, uint256 id) external view returns (uint256); -} diff --git a/contracts/interfaces/modules/royalty/policies/ILiquidSplitFactory.sol b/contracts/interfaces/modules/royalty/policies/ILiquidSplitFactory.sol deleted file mode 100644 index 7bbccbe8..00000000 --- a/contracts/interfaces/modules/royalty/policies/ILiquidSplitFactory.sol +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.23; - -/// @title LiquidSplitFactory interface -interface ILiquidSplitFactory { - /// @notice Creates a new LiquidSplitClone contract - /// @param accounts The accounts to initialize the LiquidSplitClone contract with - /// @param initAllocations The initial allocations - /// @param _distributorFee The distributor fee - /// @param owner The owner of the LiquidSplitClone contract - /// @return address The address of the new LiquidSplitClone contract - function createLiquidSplitClone( - address[] calldata accounts, - uint32[] calldata initAllocations, - uint32 _distributorFee, - address owner - ) external returns (address); -} diff --git a/contracts/interfaces/modules/royalty/policies/ILiquidSplitMain.sol b/contracts/interfaces/modules/royalty/policies/ILiquidSplitMain.sol deleted file mode 100644 index e450f6d9..00000000 --- a/contracts/interfaces/modules/royalty/policies/ILiquidSplitMain.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.23; - -import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; - -/// @title LiquidSplitMain interface -interface ILiquidSplitMain { - /// @notice Allows an account to withdraw their accrued and distributed pending amount - /// @param account The account to withdraw from - /// @param withdrawETH The amount of ETH to withdraw - /// @param tokens The tokens to withdraw - function withdraw(address account, uint256 withdrawETH, ERC20[] calldata tokens) external; - - /// @notice Gets the ETH balance of an account - /// @param account The account to get the ETH balance of - /// @return balance The ETH balance of the account - function getETHBalance(address account) external view returns (uint256); - - /// @notice Gets the ERC20 balance of an account - /// @param account The account to get the ERC20 balance of - /// @param token The token to get the balance of - /// @return balance The ERC20 balance of the account - function getERC20Balance(address account, ERC20 token) external view returns (uint256); -} diff --git a/contracts/interfaces/modules/royalty/policies/IRoyaltyPolicyLAP.sol b/contracts/interfaces/modules/royalty/policies/IRoyaltyPolicyLAP.sol index 5dad8048..5ab55359 100644 --- a/contracts/interfaces/modules/royalty/policies/IRoyaltyPolicyLAP.sol +++ b/contracts/interfaces/modules/royalty/policies/IRoyaltyPolicyLAP.sol @@ -1,30 +1,26 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.23; -import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; - 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 splitClone The split clone address - /// @param ancestorsVault The ancestors vault address + /// @param ipRoyaltyVault The ip royalty vault address /// @param royaltyStack The royalty stack /// @param targetAncestors The ip ancestors array /// @param targetRoyaltyAmount The ip royalty amount array event PolicyInitialized( address ipId, - address splitClone, - address ancestorsVault, + address ipRoyaltyVault, uint32 royaltyStack, address[] targetAncestors, uint32[] targetRoyaltyAmount ); - /// @notice Returns the percentage scale - 1000 rnfts represents 100% - function TOTAL_RNFT_SUPPLY() external view returns (uint32); + /// @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); @@ -39,58 +35,17 @@ interface IRoyaltyPolicyLAP is IRoyaltyPolicy { /// @notice Returns the LicensingModule address function LICENSING_MODULE() external view returns (address); - /// @notice Returns the 0xSplits LiquidSplitFactory address - function LIQUID_SPLIT_FACTORY() external view returns (address); - - /// @notice Returns the 0xSplits LiquidSplitMain address - function LIQUID_SPLIT_MAIN() external view returns (address); - - /// @notice Returns the Ancestors Vault Implementation address - function ancestorsVaultImpl() external view returns (address); - - /// @notice Distributes funds internally so that accounts holding the royalty nfts at distribution moment can - /// claim afterwards - /// @dev This call will revert if the caller holds all the royalty nfts of the ipId - in that case can call - /// claimFromIpPoolAsTotalRnftOwner() instead - /// @param ipId The ipId whose received funds will be distributed - /// @param token The token to distribute - /// @param accounts The accounts to distribute to - /// @param distributorAddress The distributor address (if any) - function distributeIpPoolFunds( - address ipId, - address token, - address[] calldata accounts, - address distributorAddress - ) external; - - /// @notice Claims the available royalties for a given address - /// @dev If there are no funds available in split main contract but there are funds in the split clone contract - /// then a distributeIpPoolFunds() call should precede this call - /// @param account The account to claim for - /// @param tokens The tokens to withdraw - function claimFromIpPool(address account, ERC20[] calldata tokens) external; - - /// @notice Claims the available royalties for a given address that holds all the royalty nfts of an ipId - /// @dev This call will revert if the caller does not hold all the royalty nfts of the ipId - /// @param ipId The ipId whose received funds will be distributed - /// @param token The token to withdraw - function claimFromIpPoolAsTotalRnftOwner(address ipId, address token) external; - - /// @notice Claims available royalty nfts and accrued royalties for an ancestor of a given ipId - /// @param ipId The ipId of the ancestors vault to claim from - /// @param claimerIpId The claimer ipId is the ancestor address that wants to claim - /// @param tokens The ERC20 tokens to withdraw - function claimFromAncestorsVault(address ipId, address claimerIpId, ERC20[] calldata tokens) external; + /// @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 splitClone The address of the liquid split clone contract for a given ipId - /// @return ancestorsVault The address of the ancestors vault contract for a given ipId + /// @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 /// @return targetAncestors The ip ancestors array /// @return targetRoyaltyAmount The ip royalty amount array function getRoyaltyData( address ipId - ) external view returns (bool, address, address, uint32, address[] memory, uint32[] memory); + ) external view returns (bool, address, uint32, address[] memory, uint32[] memory); } diff --git a/contracts/lib/Errors.sol b/contracts/lib/Errors.sol index 20223f3d..3f6d9f1d 100644 --- a/contracts/lib/Errors.sol +++ b/contracts/lib/Errors.sol @@ -238,12 +238,14 @@ library Errors { error RoyaltyPolicyLAP__NotFullOwnership(); error RoyaltyPolicyLAP__UnlinkableToParents(); error RoyaltyPolicyLAP__LastPositionNotAbleToMintLicense(); - - error AncestorsVaultLAP__ZeroRoyaltyPolicyLAP(); - error AncestorsVaultLAP__AlreadyClaimed(); - error AncestorsVaultLAP__InvalidVault(); - error AncestorsVaultLAP__ClaimerNotAnAncestor(); - error AncestorsVaultLAP__ERC20BalanceNotZero(); + error RoyaltyPolicyLAP__ZeroIpRoyaltyVaultBeacon(); + + error IpRoyaltyVault__ZeroIpId(); + error IpRoyaltyVault__ZeroRoyaltyPolicyLAP(); + error IpRoyaltyVault__NotRoyaltyPolicyLAP(); + error IpRoyaltyVault__SnapshotIntervalTooShort(); + error IpRoyaltyVault__AlreadyClaimed(); + error IpRoyaltyVault__ClaimerNotAnAncestor(); //////////////////////////////////////////////////////////////////////////// // ModuleRegistry // diff --git a/contracts/modules/licensing/PILPolicyFrameworkManager.sol b/contracts/modules/licensing/PILPolicyFrameworkManager.sol index e6deb82a..503e1276 100644 --- a/contracts/modules/licensing/PILPolicyFrameworkManager.sol +++ b/contracts/modules/licensing/PILPolicyFrameworkManager.sol @@ -310,7 +310,7 @@ contract PILPolicyFrameworkManager is /// @param policy The policy to encode function _policyCommercialTraitsToJson(PILPolicy memory policy) internal pure returns (string memory) { /* solhint-disable */ - // NOTE: TOTAL_RNFT_SUPPLY = 1000 in trait with max_value. For numbers, don't add any display_type, so that + // NOTE: TOTAL_RT_SUPPLY = 100*10**18 in trait with max_value. For numbers, don't add any display_type, so that // they will show up in the "Ranking" section of the OpenSea UI. return string( @@ -337,7 +337,7 @@ contract PILPolicyFrameworkManager is /// @param policy The policy to encode function _policyDerivativeTraitsToJson(PILPolicy memory policy) internal pure returns (string memory) { /* solhint-disable */ - // NOTE: TOTAL_RNFT_SUPPLY = 1000 in trait with max_value. For numbers, don't add any display_type, so that + // NOTE: TOTAL_RT_SUPPLY = 100*10**18 in trait with max_value. For numbers, don't add any display_type, so that // they will show up in the "Ranking" section of the OpenSea UI. return string( diff --git a/contracts/modules/royalty/RoyaltyModule.sol b/contracts/modules/royalty/RoyaltyModule.sol index 8e9d8b03..9a34d305 100644 --- a/contracts/modules/royalty/RoyaltyModule.sol +++ b/contracts/modules/royalty/RoyaltyModule.sol @@ -177,7 +177,7 @@ contract RoyaltyModule is 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 the receiver ip can have a zero royalty policy since that could mean it is an ip a root + // 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(); diff --git a/contracts/modules/royalty/policies/AncestorsVaultLAP.sol b/contracts/modules/royalty/policies/AncestorsVaultLAP.sol deleted file mode 100644 index 653d1880..00000000 --- a/contracts/modules/royalty/policies/AncestorsVaultLAP.sol +++ /dev/null @@ -1,115 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.23; - -import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; -import { ERC1155Holder } from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; -import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; - -import { IAncestorsVaultLAP } from "../../../interfaces/modules/royalty/policies/IAncestorsVaultLAP.sol"; -import { ILiquidSplitClone } from "../../../interfaces/modules/royalty/policies/ILiquidSplitClone.sol"; -import { ILiquidSplitMain } from "../../../interfaces/modules/royalty/policies/ILiquidSplitMain.sol"; -import { IRoyaltyPolicyLAP } from "../../../interfaces/modules/royalty/policies/IRoyaltyPolicyLAP.sol"; -import { ArrayUtils } from "../../../lib/ArrayUtils.sol"; -import { Errors } from "../../../lib/Errors.sol"; - -/// @title Liquid Absolute Percentage Policy Ancestors Vault -/// @notice The ancestors vault allows parents and grandparents to claim their share of -/// the royalty nfts of their children and grandchildren along with any accrued royalties. -contract AncestorsVaultLAP is IAncestorsVaultLAP, ERC1155Holder, ReentrancyGuard { - using SafeERC20 for IERC20; - - /// @notice The liquid split royalty policy address - IRoyaltyPolicyLAP public immutable ROYALTY_POLICY_LAP; - - /// @notice Indicates if a given ancestor address has already claimed - mapping(address ipId => mapping(address claimerIpId => bool)) public isClaimed; - - constructor(address royaltyPolicyLAP) { - if (royaltyPolicyLAP == address(0)) revert Errors.AncestorsVaultLAP__ZeroRoyaltyPolicyLAP(); - - ROYALTY_POLICY_LAP = IRoyaltyPolicyLAP(royaltyPolicyLAP); - } - - /// @notice Claims all available royalty nfts and accrued royalties for an ancestor of a given ipId - /// @param ipId The ipId of the ancestors vault to claim from - /// @param claimerIpId The claimer ipId is the ancestor address that wants to claim - /// @param tokens The ERC20 tokens to withdraw - function claim(address ipId, address claimerIpId, ERC20[] calldata tokens) external nonReentrant { - ( - , - address splitClone, - address ancestorsVault, - , - address[] memory ancestors, - uint32[] memory ancestorsRoyalties - ) = ROYALTY_POLICY_LAP.getRoyaltyData(ipId); - - if (isClaimed[ipId][claimerIpId]) revert Errors.AncestorsVaultLAP__AlreadyClaimed(); - if (address(this) != ancestorsVault) revert Errors.AncestorsVaultLAP__InvalidVault(); - - // transfer the rnfts to the claimer accrued royalties to the claimer IpId address - _transferRnftsAndAccruedTokens(claimerIpId, splitClone, ancestors, ancestorsRoyalties, tokens); - - isClaimed[ipId][claimerIpId] = true; - - emit Claimed(ipId, claimerIpId, tokens); - } - - /// @dev Transfers the Royalty NFTs and accrued tokens to the claimer - /// @param claimerIpId The claimer ipId - /// @param splitClone The split clone address - /// @param ancestors The ancestors of the IP - /// @param ancestorsRoyalties The royalties of each of the ancestors - /// @param tokens The ERC20 tokens to withdraw - function _transferRnftsAndAccruedTokens( - address claimerIpId, - address splitClone, - address[] memory ancestors, - uint32[] memory ancestorsRoyalties, - ERC20[] calldata tokens - ) internal { - (uint32 index, bool isIn) = ArrayUtils.indexOf(ancestors, claimerIpId); - if (!isIn) revert Errors.AncestorsVaultLAP__ClaimerNotAnAncestor(); - - // transfer the rnfts from the ancestors vault to the claimer split clone - // the rnfts that are meant for the ancestors were transferred to the ancestors vault at its deployment - // and each ancestor can claim their share of the rnfts only once - ILiquidSplitClone rnft = ILiquidSplitClone(splitClone); - uint256 totalUnclaimedRnfts = rnft.balanceOf(address(this), 0); - uint32 rnftAmountToTransfer = ancestorsRoyalties[index]; - rnft.safeTransferFrom(address(this), claimerIpId, 0, rnftAmountToTransfer, ""); - - // transfer the accrued tokens to the claimer IpId address - _claimAccruedTokens(rnftAmountToTransfer, totalUnclaimedRnfts, claimerIpId, tokens); - } - - /// @dev Claims the accrued tokens (if any) - /// @param rnftClaimAmount The amount of rnfts to claim - /// @param totalUnclaimedRnfts The total unclaimed rnfts - /// @param claimerIpId The claimer ipId - /// @param tokens The ERC20 tokens to withdraw - function _claimAccruedTokens( - uint256 rnftClaimAmount, - uint256 totalUnclaimedRnfts, - address claimerIpId, - ERC20[] calldata tokens - ) internal { - ILiquidSplitMain splitMain = ILiquidSplitMain(ROYALTY_POLICY_LAP.LIQUID_SPLIT_MAIN()); - - for (uint256 i = 0; i < tokens.length; ++i) { - // When withdrawing ERC20, 0xSplits sets the value to 1 to have warm storage access. - // But this still means 0 amount left. So, in the check below, we use `> 1`. - if (splitMain.getERC20Balance(address(this), tokens[i]) > 1) - revert Errors.AncestorsVaultLAP__ERC20BalanceNotZero(); - - IERC20 IToken = IERC20(tokens[i]); - uint256 tokenBalance = IToken.balanceOf(address(this)); - // when totalUnclaimedRnfts is 0, claim() call will revert as expected behaviour so no need to check for it - uint256 tokenClaimAmount = (tokenBalance * rnftClaimAmount) / totalUnclaimedRnfts; - - IToken.safeTransfer(claimerIpId, tokenClaimAmount); - } - } -} diff --git a/contracts/modules/royalty/policies/IpRoyaltyVault.sol b/contracts/modules/royalty/policies/IpRoyaltyVault.sol new file mode 100644 index 00000000..e4b4ed3c --- /dev/null +++ b/contracts/modules/royalty/policies/IpRoyaltyVault.sol @@ -0,0 +1,237 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +// solhint-disable-next-line max-line-length +import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable-v4/security/ReentrancyGuardUpgradeable.sol"; +// solhint-disable-next-line max-line-length +import { ERC20SnapshotUpgradeable } from "@openzeppelin/contracts-upgradeable-v4/token/ERC20/extensions/ERC20SnapshotUpgradeable.sol"; +// solhint-disable-next-line max-line-length +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 { IIpRoyaltyVault } from "../../../interfaces/modules/royalty/policies/IIpRoyaltyVault.sol"; +import { ArrayUtils } from "../../../lib/ArrayUtils.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 +contract IpRoyaltyVault is IIpRoyaltyVault, ERC20SnapshotUpgradeable, ReentrancyGuardUpgradeable { + using EnumerableSet for EnumerableSet.AddressSet; + using SafeERC20Upgradeable for IERC20Upgradeable; + + /// @notice LAP royalty policy address + /// @custom:oz-upgrades-unsafe-allow state-variable-immutable + IRoyaltyPolicyLAP public immutable ROYALTY_POLICY_LAP; + + /// @notice Ip id to whom this royalty vault belongs to + address public ipId; + + /// @notice Amount of unclaimed royalty tokens + uint32 public unclaimedRoyaltyTokens; + + /// @notice Last snapshotted timestamp + uint256 public lastSnapshotTimestamp; + + /// @notice Amount of revenue token in the ancestors vault + mapping(address token => uint256 amount) public ancestorsVaultAmount; + + /// @notice Indicates if a given ancestor address has already claimed + mapping(address ancestorIpId => bool) public isClaimedByAncestor; + + /// @notice Amount of revenue token in the claim vault + mapping(address token => uint256 amount) public claimVaultAmount; + + /// @notice Amount of tokens claimable at a given snapshot + mapping(uint256 snapshotId => mapping(address token => uint256 amount)) public claimableAtSnapshot; + + /// @notice Amount of unclaimed tokens at the snapshot + mapping(uint256 snapshotId => uint32 tokenAmount) public unclaimedAtSnapshot; + + /// @notice Indicates whether the claimer has claimed the revenue tokens at a given snapshot + mapping(uint256 snapshotId => mapping(address claimer => mapping(address token => bool))) + public isClaimedAtSnapshot; + + /// @notice Royalty tokens of the vault + EnumerableSet.AddressSet private _tokens; + + /// @notice Constructor + /// @param royaltyPolicyLAP The address of the royalty policy LAP + /// @custom:oz-upgrades-unsafe-allow constructor + constructor(address royaltyPolicyLAP) { + if (royaltyPolicyLAP == address(0)) revert Errors.IpRoyaltyVault__ZeroRoyaltyPolicyLAP(); + ROYALTY_POLICY_LAP = IRoyaltyPolicyLAP(royaltyPolicyLAP); + _disableInitializers(); + } + + // TODO: adjust/review for upgradeability + /// @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 + function initialize( + string memory name, + string memory symbol, + uint32 supply, + uint32 unclaimedTokens, + address ipIdAddress + ) external initializer { + if (ipIdAddress == address(0)) revert Errors.IpRoyaltyVault__ZeroIpId(); + + ipId = ipIdAddress; + lastSnapshotTimestamp = block.timestamp; + unclaimedRoyaltyTokens = unclaimedTokens; + + _mint(address(this), unclaimedTokens); + _mint(ipIdAddress, supply - unclaimedTokens); + + __ReentrancyGuard_init(); + __ERC20Snapshot_init(); + __ERC20_init(name, symbol); + } + + /// @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 + function addIpRoyaltyVaultTokens(address token) external { + if (msg.sender != address(ROYALTY_POLICY_LAP)) revert Errors.IpRoyaltyVault__NotRoyaltyPolicyLAP(); + _tokens.add(token); + } + + /// @notice Snapshots the claimable revenue and royalty token amounts + /// @return snapshotId The snapshot id + function snapshot() external returns (uint256) { + if (block.timestamp - lastSnapshotTimestamp < ROYALTY_POLICY_LAP.getSnapshotInterval()) + revert Errors.IpRoyaltyVault__SnapshotIntervalTooShort(); + + uint256 snapshotId = _snapshot(); + lastSnapshotTimestamp = block.timestamp; + + uint32 unclaimedTokens = unclaimedRoyaltyTokens; + unclaimedAtSnapshot[snapshotId] = unclaimedTokens; + + address[] memory tokens = _tokens.values(); + + for (uint256 i = 0; i < tokens.length; i++) { + uint256 tokenBalance = IERC20Upgradeable(tokens[i]).balanceOf(address(this)); + if (tokenBalance == 0) { + _tokens.remove(tokens[i]); + continue; + } + + uint256 newRevenue = tokenBalance - claimVaultAmount[tokens[i]] - ancestorsVaultAmount[tokens[i]]; + if (newRevenue == 0) continue; + + uint256 ancestorsTokens = (newRevenue * unclaimedTokens) / totalSupply(); + ancestorsVaultAmount[tokens[i]] += ancestorsTokens; + + uint256 claimableTokens = newRevenue - ancestorsTokens; + claimableAtSnapshot[snapshotId][tokens[i]] = claimableTokens; + claimVaultAmount[tokens[i]] += claimableTokens; + } + + emit SnapshotCompleted(snapshotId, block.timestamp, unclaimedTokens); + + return snapshotId; + } + + /// @notice Calculates 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) external view returns (uint256) { + return _claimableRevenue(account, snapshotId, token); + } + + /// @notice Allows token holders to claim revenue token based on the token balance at certain snapshot + /// @param snapshotId The snapshot id + /// @param tokens The list of revenue tokens to claim + function claimRevenueByTokenBatch(uint256 snapshotId, address[] calldata tokens) external nonReentrant { + for (uint256 i = 0; i < tokens.length; i++) { + uint256 claimableToken = _claimableRevenue(msg.sender, snapshotId, tokens[i]); + if (claimableToken == 0) continue; + + isClaimedAtSnapshot[snapshotId][msg.sender][tokens[i]] = true; + claimVaultAmount[tokens[i]] -= claimableToken; + IERC20Upgradeable(tokens[i]).safeTransfer(msg.sender, claimableToken); + } + } + + /// @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 { + uint256 claimableToken; + for (uint256 i = 0; i < snapshotIds.length; i++) { + claimableToken += _claimableRevenue(msg.sender, snapshotIds[i], token); + isClaimedAtSnapshot[snapshotIds[i]][msg.sender][token] = true; + } + + claimVaultAmount[token] -= claimableToken; + IERC20Upgradeable(token).safeTransfer(msg.sender, 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 { + (, , , address[] memory ancestors, uint32[] memory ancestorsRoyalties) = ROYALTY_POLICY_LAP.getRoyaltyData( + ipId + ); + + if (isClaimedByAncestor[ancestorIpId]) revert Errors.IpRoyaltyVault__AlreadyClaimed(); + + // check if the address being claimed to is an ancestor + (uint32 index, bool isIn) = ArrayUtils.indexOf(ancestors, ancestorIpId); + if (!isIn) revert Errors.IpRoyaltyVault__ClaimerNotAnAncestor(); + + // transfer royalty tokens to the ancestor + IERC20Upgradeable(address(this)).safeTransfer(ancestorIpId, ancestorsRoyalties[index]); + + // collect accrued revenue tokens (if any) + _collectAccruedTokens(ancestorsRoyalties[index], ancestorIpId); + + isClaimedByAncestor[ancestorIpId] = true; + unclaimedRoyaltyTokens -= ancestorsRoyalties[index]; + + emit RoyaltyTokensCollected(ancestorIpId, ancestorsRoyalties[index]); + } + + /// @notice Returns the list of revenue tokens in the vault + /// @return The list of revenue tokens + function getVaultTokens() external view returns (address[] memory) { + return _tokens.values(); + } + + /// @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) { + 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; + } + + /// @dev Collect the accrued tokens (if any) + /// @param royaltyTokensToClaim The amount of royalty tokens being claimed by the ancestor + /// @param ancestorIpId The ip id of the ancestor to whom the royalty tokens belong to + function _collectAccruedTokens(uint256 royaltyTokensToClaim, address ancestorIpId) internal { + address[] memory tokens = _tokens.values(); + + for (uint256 i = 0; i < tokens.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[tokens[i]] * royaltyTokensToClaim) / unclaimedRoyaltyTokens; + if (collectAmount == 0) continue; + + ancestorsVaultAmount[tokens[i]] -= collectAmount; + IERC20Upgradeable(tokens[i]).safeTransfer(ancestorIpId, collectAmount); + } + } +} diff --git a/contracts/modules/royalty/policies/RoyaltyPolicyLAP.sol b/contracts/modules/royalty/policies/RoyaltyPolicyLAP.sol index 899e179c..ef323338 100644 --- a/contracts/modules/royalty/policies/RoyaltyPolicyLAP.sol +++ b/contracts/modules/royalty/policies/RoyaltyPolicyLAP.sol @@ -1,56 +1,45 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.23; -import { Clones } from "@openzeppelin/contracts/proxy/Clones.sol"; -import { ERC1155Holder } from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.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 { IAncestorsVaultLAP } from "../../../interfaces/modules/royalty/policies/IAncestorsVaultLAP.sol"; +import { IIpRoyaltyVault } from "../../../interfaces/modules/royalty/policies/IIpRoyaltyVault.sol"; import { GovernableUpgradeable } from "../../../../contracts/governance/GovernableUpgradeable.sol"; import { IRoyaltyPolicyLAP } from "../../../interfaces/modules/royalty/policies/IRoyaltyPolicyLAP.sol"; import { ArrayUtils } from "../../../lib/ArrayUtils.sol"; -import { ILiquidSplitFactory } from "../../../interfaces/modules/royalty/policies/ILiquidSplitFactory.sol"; -import { ILiquidSplitMain } from "../../../interfaces/modules/royalty/policies/ILiquidSplitMain.sol"; -import { ILiquidSplitClone } from "../../../interfaces/modules/royalty/policies/ILiquidSplitClone.sol"; import { Errors } from "../../../lib/Errors.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, - GovernableUpgradeable, - ERC1155Holder, - ReentrancyGuardUpgradeable, - UUPSUpgradeable -{ +contract RoyaltyPolicyLAP is IRoyaltyPolicyLAP, GovernableUpgradeable, ReentrancyGuardUpgradeable, UUPSUpgradeable { using SafeERC20 for IERC20; /// @notice The state data of the LAP royalty policy /// @param isUnlinkableToParents Indicates if the ipId is unlinkable to new parents - /// @param splitClone The address of the liquid split clone contract for a given ipId - /// @param ancestorsVault The address of the ancestors vault contract for a given ipId + /// @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 /// @param ancestorsAddresses The ancestors addresses array /// @param ancestorsRoyalties The ancestors royalties array struct LAPRoyaltyData { bool isUnlinkableToParents; - address splitClone; - address ancestorsVault; + address ipRoyaltyVault; uint32 royaltyStack; address[] ancestorsAddresses; uint32[] ancestorsRoyalties; } /// @dev Storage structure for the RoyaltyPolicyLAP - /// @param ancestorsVaultImpl The Ancestors Vault implementation address + /// @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 ancestorsVaultImpl; + address ipRoyaltyVaultBeacon; + uint256 snapshotInterval; mapping(address ipId => LAPRoyaltyData) royaltyData; } @@ -58,8 +47,8 @@ contract RoyaltyPolicyLAP is bytes32 private constant RoyaltyPolicyLAPStorageLocation = 0x0c915ba68e2c4e37f19454bb13066f18f9db418fcefbf3c585b4b7d0fb0e0600; - /// @notice Returns the percentage scale - 1000 rnfts represents 100% - uint32 public constant TOTAL_RNFT_SUPPLY = 1000; + /// @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; @@ -76,14 +65,6 @@ contract RoyaltyPolicyLAP is /// @custom:oz-upgrades-unsafe-allow state-variable-immutable address public immutable LICENSING_MODULE; - /// @notice Returns the 0xSplits LiquidSplitFactory address - /// @custom:oz-upgrades-unsafe-allow state-variable-immutable - address public immutable LIQUID_SPLIT_FACTORY; - - /// @notice Returns the 0xSplits LiquidSplitMain address - /// @custom:oz-upgrades-unsafe-allow state-variable-immutable - address public immutable LIQUID_SPLIT_MAIN; - /// @dev Restricts the calls to the royalty module modifier onlyRoyaltyModule() { if (msg.sender != ROYALTY_MODULE) revert Errors.RoyaltyPolicyLAP__NotRoyaltyModule(); @@ -93,23 +74,17 @@ contract RoyaltyPolicyLAP is /// @notice Constructor /// @param royaltyModule The RoyaltyModule address /// @param licensingModule The LicensingModule address - /// @param liquidSplitFactory The 0xSplits LiquidSplitFactory address - /// @param liquidSplitMain The 0xSplits LiquidSplitMain address /// @custom:oz-upgrades-unsafe-allow constructor - constructor(address royaltyModule, address licensingModule, address liquidSplitFactory, address liquidSplitMain) { + constructor(address royaltyModule, address licensingModule) { if (royaltyModule == address(0)) revert Errors.RoyaltyPolicyLAP__ZeroRoyaltyModule(); if (licensingModule == address(0)) revert Errors.RoyaltyPolicyLAP__ZeroLicensingModule(); - if (liquidSplitFactory == address(0)) revert Errors.RoyaltyPolicyLAP__ZeroLiquidSplitFactory(); - if (liquidSplitMain == address(0)) revert Errors.RoyaltyPolicyLAP__ZeroLiquidSplitMain(); ROYALTY_MODULE = royaltyModule; LICENSING_MODULE = licensingModule; - LIQUID_SPLIT_FACTORY = liquidSplitFactory; - LIQUID_SPLIT_MAIN = liquidSplitMain; _disableInitializers(); } - /// @notice initializer for this implementation contract + /// @notice Initializer for this implementation contract /// @param governance The governance address function initialize(address governance) external initializer { __GovernableUpgradeable_init(governance); @@ -117,14 +92,21 @@ contract RoyaltyPolicyLAP is __UUPSUpgradeable_init(); } - /// @dev Set the ancestors vault implementation address + /// @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 onlyProtocolAdmin { + RoyaltyPolicyLAPStorage storage $ = _getRoyaltyPolicyLAPStorage(); + $.snapshotInterval = timestampInterval; + } + + /// @dev Set the ip royalty vault beacon /// @dev Enforced to be only callable by the protocol admin in governance - /// @param implementation The ancestors vault implementation address - function setAncestorsVaultImplementation(address implementation) external onlyProtocolAdmin { - if (implementation == address(0)) revert Errors.RoyaltyPolicyLAP__ZeroAncestorsVaultImpl(); + /// @param beacon The ip royalty vault beacon address + function setIpRoyaltyVaultBeacon(address beacon) public onlyProtocolAdmin { + if (beacon == address(0)) revert Errors.RoyaltyPolicyLAP__ZeroIpRoyaltyVaultBeacon(); RoyaltyPolicyLAPStorage storage $ = _getRoyaltyPolicyLAPStorage(); - if ($.ancestorsVaultImpl != address(0)) revert Errors.RoyaltyPolicyLAP__ImplementationAlreadySet(); - $.ancestorsVaultImpl = implementation; + $.ipRoyaltyVaultBeacon = beacon; } /// @notice Executes royalty related logic on minting a license @@ -142,18 +124,16 @@ contract RoyaltyPolicyLAP is LAPRoyaltyData memory data = $.royaltyData[ipId]; - if (data.royaltyStack + newLicenseRoyalty > TOTAL_RNFT_SUPPLY) + if (data.royaltyStack + newLicenseRoyalty > TOTAL_RT_SUPPLY) revert Errors.RoyaltyPolicyLAP__AboveRoyaltyStackLimit(); - if (data.splitClone == address(0)) { + 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. - address[] memory rootParents = new address[](0); - bytes[] memory rootParentRoyalties = new bytes[](0); - _initPolicy(ipId, rootParents, rootParentRoyalties); + _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 @@ -180,10 +160,51 @@ contract RoyaltyPolicyLAP is _initPolicy(ipId, parentIpIds, licenseData); } - /// @notice Returns the Ancestors Vault Implementation address - function ancestorsVaultImpl() external view returns (address) { + /// @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; + IIpRoyaltyVault(destination).addIpRoyaltyVaultTokens(token); + 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 + /// @return ancestorsAddresses The ancestors addresses array + /// @return ancestorsRoyalties The ancestors royalties array + function getRoyaltyData( + address ipId + ) external view returns (bool, address, uint32, address[] memory, uint32[] memory) { + RoyaltyPolicyLAPStorage storage $ = _getRoyaltyPolicyLAPStorage(); + LAPRoyaltyData memory data = $.royaltyData[ipId]; + return ( + data.isUnlinkableToParents, + data.ipRoyaltyVault, + data.royaltyStack, + data.ancestorsAddresses, + data.ancestorsRoyalties + ); + } + + /// @notice Returns the snapshot interval + /// @return snapshotInterval The minimum timestamp interval between snapshots + function getSnapshotInterval() external view returns (uint256) { RoyaltyPolicyLAPStorage storage $ = _getRoyaltyPolicyLAPStorage(); - return $.ancestorsVaultImpl; + 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. @@ -217,127 +238,20 @@ contract RoyaltyPolicyLAP is $.royaltyData[parentIpIds[i]].isUnlinkableToParents = true; } - // deploy ancestors vault if not root ip - // 0xSplit requires two addresses to allow a split so for root ip address(this) is used as the second address - address ancestorsVault = parentIpIds.length > 0 ? Clones.clone($.ancestorsVaultImpl) : address(this); - - // deploy split clone - address splitClone = _deploySplitClone(ipId, ancestorsVault, royaltyStack); - - // ancestorsVault is adjusted as address(this) was just used for the split clone deployment - ancestorsVault = ancestorsVault == address(this) ? address(0) : ancestorsVault; + // 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, - splitClone: splitClone, - ancestorsVault: ancestorsVault, + ipRoyaltyVault: ipRoyaltyVault, royaltyStack: royaltyStack, ancestorsAddresses: newAncestors, ancestorsRoyalties: newAncestorsRoyalties }); - emit PolicyInitialized(ipId, splitClone, ancestorsVault, royaltyStack, newAncestors, newAncestorsRoyalties); - } - - /// @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].splitClone; - IERC20(token).safeTransferFrom(caller, destination, amount); - } - - /// @notice Distributes funds internally so that accounts holding the royalty nfts at distribution moment can - /// claim afterwards - /// @dev This call will revert if the caller holds all the royalty nfts of the ipId - in that case can call - /// claimFromIpPoolAsTotalRnftOwner() instead - /// @param ipId The ipId whose received funds will be distributed - /// @param token The token to distribute - /// @param accounts The accounts to distribute to - /// @param distributorAddress The distributor address (if any) - function distributeIpPoolFunds( - address ipId, - address token, - address[] calldata accounts, - address distributorAddress - ) external { - RoyaltyPolicyLAPStorage storage $ = _getRoyaltyPolicyLAPStorage(); - ILiquidSplitClone($.royaltyData[ipId].splitClone).distributeFunds(token, accounts, distributorAddress); - } - - /// @notice Claims the available royalties for a given address - /// @dev If there are no funds available in split main contract but there are funds in the split clone contract - /// then a distributeIpPoolFunds() call should precede this call - /// @param account The account to claim for - /// @param tokens The tokens to withdraw - function claimFromIpPool(address account, ERC20[] calldata tokens) external { - ILiquidSplitMain(LIQUID_SPLIT_MAIN).withdraw(account, 0, tokens); - } - - /// @notice Claims the available royalties for a given address that holds all the royalty nfts of an ipId - /// @dev This call will revert if the caller does not hold all the royalty nfts of the ipId - /// @param ipId The ipId whose received funds will be distributed - /// @param token The token to withdraw - function claimFromIpPoolAsTotalRnftOwner(address ipId, address token) external nonReentrant { - RoyaltyPolicyLAPStorage storage $ = _getRoyaltyPolicyLAPStorage(); - ILiquidSplitClone splitClone = ILiquidSplitClone($.royaltyData[ipId].splitClone); - ILiquidSplitMain splitMain = ILiquidSplitMain(LIQUID_SPLIT_MAIN); - - if (splitClone.balanceOf(msg.sender, 0) < TOTAL_RNFT_SUPPLY) revert Errors.RoyaltyPolicyLAP__NotFullOwnership(); - - splitClone.safeTransferFrom(msg.sender, address(this), 0, 1, "0x0"); - - address[] memory accounts = new address[](2); - accounts[0] = msg.sender; - accounts[1] = address(this); - - ERC20[] memory tokens = new ERC20[](1); - - splitClone.distributeFunds(token, accounts, address(0)); - tokens[0] = ERC20(token); - - splitMain.withdraw(msg.sender, 0, tokens); - splitMain.withdraw(address(this), 0, tokens); - - splitClone.safeTransferFrom(address(this), msg.sender, 0, 1, "0x0"); - - IERC20(token).safeTransfer(msg.sender, IERC20(token).balanceOf(address(this))); - } - - /// @notice Claims all available royalty nfts and accrued royalties for an ancestor of a given ipId - /// @param ipId The ipId of the ancestors vault to claim from - /// @param claimerIpId The claimer ipId is the ancestor address that wants to claim - /// @param tokens The ERC20 tokens to withdraw - function claimFromAncestorsVault(address ipId, address claimerIpId, ERC20[] calldata tokens) external { - RoyaltyPolicyLAPStorage storage $ = _getRoyaltyPolicyLAPStorage(); - IAncestorsVaultLAP($.royaltyData[ipId].ancestorsVault).claim(ipId, claimerIpId, tokens); - } - - /// @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 splitClone The address of the liquid split clone contract for a given ipId - /// @return ancestorsVault The address of the ancestors vault contract for a given ipId - /// @return royaltyStack The royalty stack of a given ipId is the sum of the royalties to be paid to each ancestors - /// @return ancestorsAddresses The ancestors addresses array - /// @return ancestorsRoyalties The ancestors royalties array - function getRoyaltyData( - address ipId - ) external view returns (bool, address, address, uint32, address[] memory, uint32[] memory) { - RoyaltyPolicyLAPStorage storage $ = _getRoyaltyPolicyLAPStorage(); - LAPRoyaltyData memory data = $.royaltyData[ipId]; - return ( - data.isUnlinkableToParents, - data.splitClone, - data.ancestorsVault, - data.royaltyStack, - data.ancestorsAddresses, - data.ancestorsRoyalties - ); + emit PolicyInitialized(ipId, ipRoyaltyVault, royaltyStack, newAncestors, newAncestorsRoyalties); } /// @dev Gets the new ancestors data @@ -361,7 +275,7 @@ contract RoyaltyPolicyLAP is ) = _getExpectedOutputs(parentIpIds, parentRoyalties); if (newAncestorsCount > MAX_ANCESTORS) revert Errors.RoyaltyPolicyLAP__AboveAncestorsLimit(); - if (newRoyaltyStack > TOTAL_RNFT_SUPPLY) revert Errors.RoyaltyPolicyLAP__AboveRoyaltyStackLimit(); + if (newRoyaltyStack > TOTAL_RT_SUPPLY) revert Errors.RoyaltyPolicyLAP__AboveRoyaltyStackLimit(); return (newRoyaltyStack, newAncestors, newAncestorsRoyalty); } @@ -390,6 +304,8 @@ contract RoyaltyPolicyLAP is uint32[] memory newAncestorsRoyalty_ = new uint32[](MAX_ANCESTORS); address[] memory newAncestors_ = new address[](MAX_ANCESTORS); + RoyaltyPolicyLAPStorage storage $ = _getRoyaltyPolicyLAPStorage(); + for (uint256 i = 0; i < parentIpIds.length; i++) { if (i == 0) { newAncestors_[ancestorsCount] = parentIpIds[i]; @@ -408,7 +324,6 @@ contract RoyaltyPolicyLAP is royaltyStack += parentRoyalties[i]; } } - RoyaltyPolicyLAPStorage storage $ = _getRoyaltyPolicyLAPStorage(); address[] memory parentAncestors = $.royaltyData[parentIpIds[i]].ancestorsAddresses; uint32[] memory parentAncestorsRoyalties = $.royaltyData[parentIpIds[i]].ancestorsRoyalties; @@ -442,30 +357,6 @@ contract RoyaltyPolicyLAP is } } - /// @dev Deploys a liquid split clone contract - /// @param ipId The ipId - /// @param ancestorsVault The ancestors vault address - /// @param royaltyStack The number of rnfts that the ipId has to give to its parents and/or grandparents - /// @return The address of the deployed liquid split clone contract - function _deploySplitClone(address ipId, address ancestorsVault, uint32 royaltyStack) internal returns (address) { - address[] memory accounts = new address[](2); - accounts[0] = ipId; - accounts[1] = ancestorsVault; - - uint32[] memory initAllocations = new uint32[](2); - initAllocations[0] = TOTAL_RNFT_SUPPLY - royaltyStack; - initAllocations[1] = royaltyStack; - - address splitClone = ILiquidSplitFactory(LIQUID_SPLIT_FACTORY).createLiquidSplitClone( - accounts, - initAllocations, - 0, // distributorFee - address(0) // splitOwner - ); - - return splitClone; - } - function _getRoyaltyPolicyLAPStorage() private pure returns (RoyaltyPolicyLAPStorage storage $) { assembly { $.slot := RoyaltyPolicyLAPStorageLocation diff --git a/package.json b/package.json index 59af6645..aec4ff5f 100644 --- a/package.json +++ b/package.json @@ -1,63 +1,64 @@ { - "name": "@story-protocol/protocol-core", - "version": "v1.0.0-beta-rc6", - "description": "Story Protocol core smart contracts", - "main": "", - "directories": { - "lib": "lib", - "test": "test" - }, - "author": "StoryProtocol", - "license": "UNLICENSED", - "scripts": { - "lint": "npm run lint:js && npm run lint:sol", - "lint:fix": "npm run lint:js:fix && npm run lint:sol:fix", - "lint:js": "prettier --log-level warn --ignore-path .gitignore '**/*.{js,ts}' --check && eslint --ignore-path .gitignore .", - "lint:js:fix": "prettier --log-level warn --ignore-path .gitignore '**/*.{js,ts}' --write && eslint --ignore-path .gitignore . --fix", - "lint:sol": "prettier --log-level warn --ignore-path .gitignore '{contracts,test}/**/*.sol' --check && solhint '{contracts,test}/**/*.sol'", - "lint:sol:fix": "prettier --log-level warn --ignore-path .gitignore '{contracts,test}/**/*.sol' --write", - "solhint": "solhint '{contracts,test}/**/*.sol'", - "test": "npx hardhat test", - "prepare": "husky install", - "docgen": "hardhat docgen" - }, - "devDependencies": { - "@nomicfoundation/hardhat-ethers": "^3.0.5", - "@nomicfoundation/hardhat-foundry": "^1.1.1", - "@nomicfoundation/hardhat-verify": "^2.0.3", - "@openzeppelin/hardhat-upgrades": "^3.0.2", - "@tenderly/hardhat-tenderly": "^2.2.1", - "@typechain/ethers-v6": "^0.5.1", - "@typechain/hardhat": "^9.1.0", - "base64-sol": "^1.1.0", - "chai": "^5.0.3", - "dotenv": "^16.4.1", - "ds-test": "https://github.com/dapphub/ds-test", - "eslint": "^8.56.0", - "eslint-plugin-prettier": "^5.1.3", - "ethers": "^6", - "forge-std": "github:foundry-rs/forge-std#v1.7.6", - "hardhat": "^2.19.4", - "hardhat-contract-sizer": "^2.10.0", - "hardhat-deploy": "^0.11.45", - "hardhat-deploy-ethers": "^0.4.1", - "hardhat-gas-reporter": "^1.0.10", - "husky": "^8.0.0", - "minimatch": "^9.0.3", - "mocha": "^10.2.0", - "prettier": "^3.0.0", - "prettier-plugin-solidity": "^1.1.0", - "solhint": "^4.1.1", - "solhint-community": "^3.7.0", - "solhint-plugin-prettier": "^0.1.0", - "solidity-coverage": "^0.8.6", - "solidity-docgen": "^0.6.0-beta.36", - "ts-node": "^10.9.2", - "typechain": "^8.3.2" - }, - "dependencies": { - "@openzeppelin/contracts": "5.0.2", - "@openzeppelin/contracts-upgradeable": "5.0.2", - "erc6551": "^0.3.1" - } + "name": "@story-protocol/protocol-core", + "version": "v1.0.0-beta-rc6", + "description": "Story Protocol core smart contracts", + "main": "", + "directories": { + "lib": "lib", + "test": "test" + }, + "author": "StoryProtocol", + "license": "UNLICENSED", + "scripts": { + "lint": "npm run lint:js && npm run lint:sol", + "lint:fix": "npm run lint:js:fix && npm run lint:sol:fix", + "lint:js": "prettier --log-level warn --ignore-path .gitignore '**/*.{js,ts}' --check && eslint --ignore-path .gitignore .", + "lint:js:fix": "prettier --log-level warn --ignore-path .gitignore '**/*.{js,ts}' --write && eslint --ignore-path .gitignore . --fix", + "lint:sol": "prettier --log-level warn --ignore-path .gitignore '{contracts,test}/**/*.sol' --check && solhint '{contracts,test}/**/*.sol'", + "lint:sol:fix": "prettier --log-level warn --ignore-path .gitignore '{contracts,test}/**/*.sol' --write", + "solhint": "solhint '{contracts,test}/**/*.sol'", + "test": "npx hardhat test", + "prepare": "husky install", + "docgen": "hardhat docgen" + }, + "devDependencies": { + "@nomicfoundation/hardhat-ethers": "^3.0.5", + "@nomicfoundation/hardhat-foundry": "^1.1.1", + "@nomicfoundation/hardhat-verify": "^2.0.3", + "@openzeppelin/hardhat-upgrades": "^3.0.2", + "@tenderly/hardhat-tenderly": "^2.2.1", + "@typechain/ethers-v6": "^0.5.1", + "@typechain/hardhat": "^9.1.0", + "base64-sol": "^1.1.0", + "chai": "^5.0.3", + "dotenv": "^16.4.1", + "ds-test": "https://github.com/dapphub/ds-test", + "eslint": "^8.56.0", + "eslint-plugin-prettier": "^5.1.3", + "ethers": "^6", + "forge-std": "github:foundry-rs/forge-std#v1.7.6", + "hardhat": "^2.19.4", + "hardhat-contract-sizer": "^2.10.0", + "hardhat-deploy": "^0.11.45", + "hardhat-deploy-ethers": "^0.4.1", + "hardhat-gas-reporter": "^1.0.10", + "husky": "^8.0.0", + "minimatch": "^9.0.3", + "mocha": "^10.2.0", + "prettier": "^3.0.0", + "prettier-plugin-solidity": "^1.1.0", + "solhint": "^4.1.1", + "solhint-community": "^3.7.0", + "solhint-plugin-prettier": "^0.1.0", + "solidity-coverage": "^0.8.6", + "solidity-docgen": "^0.6.0-beta.36", + "ts-node": "^10.9.2", + "typechain": "^8.3.2" + }, + "dependencies": { + "@openzeppelin/contracts": "5.0.2", + "@openzeppelin/contracts-upgradeable": "5.0.2", + "@openzeppelin/contracts-upgradeable-v4": "npm:@openzeppelin/contracts-upgradeable@4.9.6", + "erc6551": "^0.3.1" + } } diff --git a/script/foundry/deployment/Main.s.sol b/script/foundry/deployment/Main.s.sol index 00b54947..e06a704b 100644 --- a/script/foundry/deployment/Main.s.sol +++ b/script/foundry/deployment/Main.s.sol @@ -28,7 +28,6 @@ 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 { AncestorsVaultLAP } from "contracts/modules/royalty/policies/AncestorsVaultLAP.sol"; import { RoyaltyPolicyLAP } from "contracts/modules/royalty/policies/RoyaltyPolicyLAP.sol"; import { DisputeModule } from "contracts/modules/dispute/DisputeModule.sol"; import { ArbitrationPolicySP } from "contracts/modules/dispute/policies/ArbitrationPolicySP.sol"; @@ -73,7 +72,6 @@ contract Main is Script, BroadcastManager, JsonDeploymentHandler, StorageLayoutC // Policy ArbitrationPolicySP internal arbitrationPolicySP; - AncestorsVaultLAP internal ancestorsVaultImpl; RoyaltyPolicyLAP internal royaltyPolicyLAP; PILPolicyFrameworkManager internal pilPfm; @@ -94,10 +92,6 @@ contract Main is Script, BroadcastManager, JsonDeploymentHandler, StorageLayoutC mapping(string frameworkName => address frameworkAddr) internal frameworkAddrs; - // 0xSplits Liquid Split (Sepolia) - address internal constant LIQUID_SPLIT_FACTORY = 0xF678Bae6091Ab6933425FE26Afc20Ee5F324c4aE; - address internal constant LIQUID_SPLIT_MAIN = 0x57CBFA83f000a38C5b5881743E298819c503A559; - uint256 internal constant ARBITRATION_PRICE = 1000 * 10 ** 6; // 1000 MockToken uint256 internal constant MAX_ROYALTY_APPROVAL = 10000 ether; @@ -106,7 +100,7 @@ contract Main is Script, BroadcastManager, JsonDeploymentHandler, StorageLayoutC /// @dev To use, run the following command (e.g. for Sepolia): /// forge script script/foundry/deployment/Main.s.sol:Main --rpc-url $RPC_URL --broadcast --verify -vvvv - function run() virtual override public { + function run() public virtual override { // This will run OZ storage layout check for all contracts. Requires --ffi flag. super.run(); _beginBroadcast(); // BroadcastManager.s.sol @@ -114,7 +108,7 @@ contract Main is Script, BroadcastManager, JsonDeploymentHandler, StorageLayoutC bool configByMultisig; try vm.envBool("DEPLOYMENT_CONFIG_BY_MULTISIG") returns (bool mult) { configByMultisig = mult; - } catch { + } catch { configByMultisig = false; } console2.log("configByMultisig:", configByMultisig); @@ -131,11 +125,6 @@ contract Main is Script, BroadcastManager, JsonDeploymentHandler, StorageLayoutC } function _deployProtocolContracts(address accessControlDeployer) private { - require( - LIQUID_SPLIT_FACTORY != address(0) && LIQUID_SPLIT_MAIN != address(0), - "DeployMain: Liquid Split Addresses Not Set" - ); - string memory contractKey; // Mock Assets (deploy first) @@ -162,13 +151,7 @@ contract Main is Script, BroadcastManager, JsonDeploymentHandler, StorageLayoutC address impl = address(new AccessController()); accessController = AccessController( - TestProxyHelper.deployUUPSProxy( - impl, - abi.encodeCall( - AccessController.initialize, - address(governance) - ) - ) + TestProxyHelper.deployUUPSProxy(impl, abi.encodeCall(AccessController.initialize, address(governance))) ); impl = address(0); // Make sure we don't deploy wrong impl _postdeploy(contractKey, address(accessController)); @@ -182,13 +165,7 @@ contract Main is Script, BroadcastManager, JsonDeploymentHandler, StorageLayoutC _predeploy(contractKey); impl = address(new ModuleRegistry()); moduleRegistry = ModuleRegistry( - TestProxyHelper.deployUUPSProxy( - impl, - abi.encodeCall( - ModuleRegistry.initialize, - address(governance) - ) - ) + TestProxyHelper.deployUUPSProxy(impl, abi.encodeCall(ModuleRegistry.initialize, address(governance))) ); impl = address(0); // Make sure we don't deploy wrong impl _postdeploy(contractKey, address(moduleRegistry)); @@ -211,35 +188,16 @@ contract Main is Script, BroadcastManager, JsonDeploymentHandler, StorageLayoutC _predeploy(contractKey); impl = address(new RoyaltyModule()); royaltyModule = RoyaltyModule( - TestProxyHelper.deployUUPSProxy( - impl, - abi.encodeCall( - RoyaltyModule.initialize, ( - address(governance) - ) - ) - ) + TestProxyHelper.deployUUPSProxy(impl, abi.encodeCall(RoyaltyModule.initialize, (address(governance)))) ); impl = address(0); _postdeploy(contractKey, address(royaltyModule)); contractKey = "DisputeModule"; _predeploy(contractKey); - impl = address( - new DisputeModule( - address(accessController), - address(ipAssetRegistry) - ) - ); + impl = address(new DisputeModule(address(accessController), address(ipAssetRegistry))); disputeModule = DisputeModule( - TestProxyHelper.deployUUPSProxy( - impl, - abi.encodeCall( - DisputeModule.initialize, ( - address(governance) - ) - ) - ) + TestProxyHelper.deployUUPSProxy(impl, abi.encodeCall(DisputeModule.initialize, (address(governance)))) ); impl = address(0); _postdeploy(contractKey, address(disputeModule)); @@ -251,7 +209,8 @@ contract Main is Script, BroadcastManager, JsonDeploymentHandler, StorageLayoutC TestProxyHelper.deployUUPSProxy( impl, abi.encodeCall( - LicenseRegistry.initialize, ( + LicenseRegistry.initialize, + ( address(governance), "https://github.com/storyprotocol/protocol-core/blob/main/assets/license-image.gif" ) @@ -274,13 +233,7 @@ contract Main is Script, BroadcastManager, JsonDeploymentHandler, StorageLayoutC ) ); licensingModule = LicensingModule( - TestProxyHelper.deployUUPSProxy( - impl, - abi.encodeCall( - LicensingModule.initialize, - address(governance) - ) - ) + TestProxyHelper.deployUUPSProxy(impl, abi.encodeCall(LicensingModule.initialize, address(governance))) ); impl = address(0); // Make sure we don't deploy wrong impl _postdeploy(contractKey, address(licensingModule)); @@ -296,55 +249,23 @@ contract Main is Script, BroadcastManager, JsonDeploymentHandler, StorageLayoutC contractKey = "ArbitrationPolicySP"; _predeploy(contractKey); - impl = address( - new ArbitrationPolicySP( - address(disputeModule), - address(erc20), - ARBITRATION_PRICE - ) - ); + impl = address(new ArbitrationPolicySP(address(disputeModule), address(erc20), ARBITRATION_PRICE)); arbitrationPolicySP = ArbitrationPolicySP( - TestProxyHelper.deployUUPSProxy( - impl, - abi.encodeCall( - ArbitrationPolicySP.initialize, ( - address(governance) - ) - ) - ) + TestProxyHelper.deployUUPSProxy(impl, abi.encodeCall(ArbitrationPolicySP.initialize, (address(governance)))) ); impl = address(0); _postdeploy(contractKey, address(arbitrationPolicySP)); contractKey = "RoyaltyPolicyLAP"; _predeploy(contractKey); - impl = address( - new RoyaltyPolicyLAP( - address(royaltyModule), - address(licensingModule), - LIQUID_SPLIT_FACTORY, - LIQUID_SPLIT_MAIN - ) - ); - + impl = address(new RoyaltyPolicyLAP(address(royaltyModule), address(licensingModule))); + royaltyPolicyLAP = RoyaltyPolicyLAP( - TestProxyHelper.deployUUPSProxy( - impl, - abi.encodeCall( - RoyaltyPolicyLAP.initialize, ( - address(governance) - ) - ) - ) + TestProxyHelper.deployUUPSProxy(impl, abi.encodeCall(RoyaltyPolicyLAP.initialize, (address(governance)))) ); impl = address(0); _postdeploy(contractKey, address(royaltyPolicyLAP)); - contractKey = "AncestorsVaultLAP"; - _predeploy(contractKey); - ancestorsVaultImpl = new AncestorsVaultLAP(address(royaltyPolicyLAP)); - _postdeploy(contractKey, address(ancestorsVaultImpl)); - _predeploy("PILPolicyFrameworkManager"); impl = address( new PILPolicyFrameworkManager( @@ -358,10 +279,7 @@ contract Main is Script, BroadcastManager, JsonDeploymentHandler, StorageLayoutC impl, abi.encodeCall( PILPolicyFrameworkManager.initialize, - ( - "pil", - "https://github.com/storyprotocol/protocol-core/blob/main/PIL-Beta-2024-02.pdf" - ) + ("pil", "https://github.com/storyprotocol/protocol-core/blob/main/PIL-Beta-2024-02.pdf") ) ) ); @@ -431,8 +349,8 @@ contract Main is Script, BroadcastManager, JsonDeploymentHandler, StorageLayoutC // whitelist royaltyModule.whitelistRoyaltyPolicy(address(royaltyPolicyLAP), true); royaltyModule.whitelistRoyaltyToken(address(erc20), true); - - royaltyPolicyLAP.setAncestorsVaultImplementation(address(ancestorsVaultImpl)); + // policy + royaltyPolicyLAP.setSnapshotInterval(7 days); } function _configureDisputeModule() private { @@ -646,64 +564,6 @@ contract Main is Script, BroadcastManager, JsonDeploymentHandler, StorageLayoutC licensingModule.linkIpToParents(licenseIds, ipId, ""); } - /*/////////////////////////////////////////////////////////////// - ROYALTY PAYMENT AND CLAIMS - ///////////////////////////////////////////////////////////////*/ - - // IPAccount1 and IPAccount3 have commercial policy, of which IPAccount5 has used to mint licenses. - // Thus, any payment to IPAccount5 will get split to IPAccount1 and IPAccount3. - - // Deployer pays IPAccount5 - { - royaltyModule.payRoyaltyOnBehalf(ipAcct[5], ipAcct[5], address(erc20), 1 ether); - } - - // Distribute the accrued revenue from the 0xSplitWallet associated with IPAccount3 to - // 0xSplits Main, which will get distributed to IPAccount3 AND its claimer based on revenue - // sharing terms specified in the royalty policy. - { - (, , address ipAcct5_ancestorVault, , ,) = royaltyPolicyLAP.getRoyaltyData(ipAcct[5]); - - address[] memory accounts = new address[](2); - // If you face InvalidSplit__AccountsOutOfOrder, shuffle the order of accounts (swap index 0 and 1) - accounts[1] = ipAcct[5]; - accounts[0] = ipAcct5_ancestorVault; - - royaltyPolicyLAP.distributeIpPoolFunds(ipAcct[5], address(erc20), accounts, deployer); - } - - // IPAccount1 claims its rNFTs and tokens, only done once since it's a direct chain - { - ERC20[] memory tokens = new ERC20[](1); - tokens[0] = erc20; - - (, , address ancestorVault_ipAcct5, , ,) = royaltyPolicyLAP.getRoyaltyData(ipAcct[5]); - - // First, release the money from the IPAccount5's 0xSplitWallet (that just received money) to the main - // 0xSplitMain that acts as a ledger for revenue distribution. - // vm.expectEmit(LIQUID_SPLIT_MAIN); - // TODO: check Withdrawal(699999999999999998) (Royalty stack is 300, or 30% [absolute] sent to ancestors) - royaltyPolicyLAP.claimFromIpPool({ account: ipAcct[5], tokens: tokens }); - royaltyPolicyLAP.claimFromIpPool({ account: ancestorVault_ipAcct5, tokens: tokens }); - - // Bob (owner of IPAccount1) calls the claim her portion of rNFTs and tokens. He can only call - // `claimFromAncestorsVault` once. Afterwards, she will automatically receive money on revenue distribution. - - address[] memory ancestors = new address[](2); - uint32[] memory ancestorsRoyalties = new uint32[](2); - ancestors[0] = ipAcct[1]; // grandparent - ancestors[1] = ipAcct[3]; // parent (claimer) - ancestorsRoyalties[0] = 200; - ancestorsRoyalties[1] = 100; - - // IPAccount1 wants to claim from IPAccount5 - royaltyPolicyLAP.claimFromAncestorsVault({ - ipId: ipAcct[5], - claimerIpId: ipAcct[1], - tokens: tokens - }); - } - /*/////////////////////////////////////////////////////////////// DISPUTE MODULE INTERACTIONS ///////////////////////////////////////////////////////////////*/ diff --git a/test/foundry/integration/BaseIntegration.t.sol b/test/foundry/integration/BaseIntegration.t.sol index 14530ec3..9bd62d28 100644 --- a/test/foundry/integration/BaseIntegration.t.sol +++ b/test/foundry/integration/BaseIntegration.t.sol @@ -32,6 +32,9 @@ contract BaseIntegration is BaseTest { postDeploymentSetup(); dealMockAssets(); + + vm.prank(u.admin); + royaltyPolicyLAP.setSnapshotInterval(7 days); } /*////////////////////////////////////////////////////////////////////////// diff --git a/test/foundry/integration/flows/royalty/Royalty.t.sol b/test/foundry/integration/flows/royalty/Royalty.t.sol index de9f5bb0..262a8993 100644 --- a/test/foundry/integration/flows/royalty/Royalty.t.sol +++ b/test/foundry/integration/flows/royalty/Royalty.t.sol @@ -4,10 +4,10 @@ pragma solidity 0.8.23; // external import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; -import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; - -// contract +import { ERC20, IERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 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"; // test import { BaseIntegration } from "test/foundry/integration/BaseIntegration.t.sol"; @@ -20,7 +20,7 @@ contract Flows_Integration_Disputes is BaseIntegration { address internal royaltyPolicyAddr; // must be assigned AFTER super.setUp() address internal mintingFeeToken; // must be assigned AFTER super.setUp() - uint32 internal defaultCommRevShare = 100; + uint32 internal defaultCommRevShare = 10 * 10 ** 6; // 10% uint256 internal mintingFee = 7 ether; function setUp() public override { @@ -153,89 +153,93 @@ contract Flows_Integration_Disputes is BaseIntegration { vm.stopPrank(); } - // Distribute the accrued revenue from the 0xSplitWallet associated with IPAccount3 to 0xSplits Main, - // which will get distributed to IPAccount3 AND its claimer based on revenue sharing terms specified in the - // royalty policy. Anyone can call this function. (Below, Dan calls as an example.) + // Owner of IPAccount2, Bob, claims his RTs from IPAccount3 vault { - vm.startPrank(u.dan); + vm.startPrank(u.bob); + + ERC20[] memory tokens = new ERC20[](1); + tokens[0] = mockToken; - (, , address ipAcct3_ancestorVault, , , ) = royaltyPolicyLAP.getRoyaltyData(ipAcct[3]); + (, address ipRoyaltyVault, , , ) = royaltyPolicyLAP.getRoyaltyData(ipAcct[3]); - address[] memory accounts = new address[](2); - // If you face InvalidSplit__AccountsOutOfOrder, shuffle the order of accounts (swap index 0 and 1) - accounts[1] = ipAcct[3]; - accounts[0] = ipAcct3_ancestorVault; + vm.warp(block.timestamp + 7 days + 1); + IpRoyaltyVault(ipRoyaltyVault).snapshot(); - royaltyPolicyLAP.distributeIpPoolFunds(ipAcct[3], address(mockToken), accounts, address(u.dan)); + vm.expectEmit(ipRoyaltyVault); + emit IERC20.Transfer({ from: ipRoyaltyVault, to: ipAcct[2], value: 10_000_000 }); // 10% - vm.stopPrank(); + vm.expectEmit(ipRoyaltyVault); + emit IIpRoyaltyVault.RoyaltyTokensCollected(ipAcct[2], 10_000_000); + + IpRoyaltyVault(ipRoyaltyVault).collectRoyaltyTokens(ipAcct[2]); } - // IPAccount2 claims its rNFTs and tokens, only done once since it's a direct chain + // Owner of IPAccount1, Alice, claims her RTs from IPAccount2 and IPAccount3 vaults { + vm.startPrank(u.alice); + ERC20[] memory tokens = new ERC20[](1); tokens[0] = mockToken; - (, , address ancestorVault_ipAcct3, , , ) = royaltyPolicyLAP.getRoyaltyData(ipAcct[3]); + (, address ipRoyaltyVault2, , , ) = royaltyPolicyLAP.getRoyaltyData(ipAcct[2]); + (, address ipRoyaltyVault3, , , ) = royaltyPolicyLAP.getRoyaltyData(ipAcct[3]); + + vm.warp(block.timestamp + 7 days + 1); + IpRoyaltyVault(ipRoyaltyVault2).snapshot(); + IpRoyaltyVault(ipRoyaltyVault3).snapshot(); + + vm.expectEmit(ipRoyaltyVault2); + emit IERC20.Transfer({ from: ipRoyaltyVault2, to: ipAcct[1], value: 10_000_000 }); // 10% + vm.expectEmit(ipRoyaltyVault2); + emit IIpRoyaltyVault.RoyaltyTokensCollected(ipAcct[1], 10_000_000); + IpRoyaltyVault(ipRoyaltyVault2).collectRoyaltyTokens(ipAcct[1]); + + vm.expectEmit(ipRoyaltyVault3); + // reason for 20%: absolute stack, so 10% from IPAccount2 and 10% from IPAccount3 + emit IERC20.Transfer({ from: ipRoyaltyVault3, to: ipAcct[1], value: 20_000_000 }); // 20% + vm.expectEmit(ipRoyaltyVault3); + emit IIpRoyaltyVault.RoyaltyTokensCollected(ipAcct[1], 20_000_000); + IpRoyaltyVault(ipRoyaltyVault3).collectRoyaltyTokens(ipAcct[1]); + } - // First, release the money from the IPAccount3's 0xSplitWallet (that just received money) to the main - // 0xSplitMain that acts as a ledger for revenue distribution. - // vm.expectEmit(LIQUID_SPLIT_MAIN); - // TODO: check Withdrawal(699999999999999998) (Royalty stack is 300, or 30% [absolute] sent to ancestors) - royaltyPolicyLAP.claimFromIpPool({ account: ipAcct[3], tokens: tokens }); - royaltyPolicyLAP.claimFromIpPool({ account: ancestorVault_ipAcct3, tokens: tokens }); + // 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(); - // Bob (owner of IPAccount2) calls the claim her portion of rNFTs and tokens. He can only call - // `claimFromAncestorsVault` once. Afterwards, she will automatically receive money on revenue distribution. + address[] memory tokens = new address[](2); + tokens[0] = address(mockToken); + tokens[1] = address(LINK); - address[] memory ancestors = new address[](2); - uint32[] memory ancestorsRoyalties = new uint32[](2); - ancestors[0] = ipAcct[1]; // grandparent - ancestors[1] = ipAcct[2]; // parent (claimer) - ancestorsRoyalties[0] = defaultCommRevShare * 2; - ancestorsRoyalties[1] = defaultCommRevShare; + IpRoyaltyVault(ipRoyaltyVault).claimRevenueByTokenBatch(1, tokens); - // IPAccount2 wants to claim from IPAccount3 - royaltyPolicyLAP.claimFromAncestorsVault({ ipId: ipAcct[3], claimerIpId: ipAcct[2], tokens: tokens }); + vm.stopPrank(); } - // IPAccount1, which is both the grandparent and parent of IPAccount3, claims its rNFTs and tokens. + // Owner of IPAccount1, Alice, takes snapshot on IPAccount2 vault and claims her revenue from both + // IPAccount2 and IPAccount3 vaults { - ERC20[] memory tokens = new ERC20[](1); - tokens[0] = mockToken; - - (, address splitClone_ipAcct1, , , , ) = royaltyPolicyLAP.getRoyaltyData(ipAcct[1]); - (, , address ancestorVault_ipAcct3, , , ) = royaltyPolicyLAP.getRoyaltyData(ipAcct[3]); + vm.startPrank(u.alice); - uint256 balanceBefore_SplitClone_ipAcct1 = mockToken.balanceOf(ipAcct[1]); - uint256 balanceBefore_AncestorVault_ipAcct3 = mockToken.balanceOf(ancestorVault_ipAcct3); + (, address ipRoyaltyVault2, , , ) = royaltyPolicyLAP.getRoyaltyData(ipAcct[2]); + (, address ipRoyaltyVault3, , , ) = royaltyPolicyLAP.getRoyaltyData(ipAcct[3]); - address[] memory ancestors = new address[](2); - uint32[] memory ancestorsRoyalties = new uint32[](2); - ancestors[0] = ipAcct[1]; // grandparent (claimer) - ancestors[1] = ipAcct[2]; // parent - ancestorsRoyalties[0] = defaultCommRevShare * 2; - ancestorsRoyalties[1] = defaultCommRevShare; + address[] memory tokens = new address[](2); + tokens[0] = address(mockToken); + tokens[1] = address(LINK); - // IPAccount1 wants to claim from IPAccount3 (gets RNFTs and tokens) - royaltyPolicyLAP.claimFromAncestorsVault({ ipId: ipAcct[3], claimerIpId: ipAcct[1], tokens: tokens }); + IpRoyaltyVault(ipRoyaltyVault3).claimRevenueByTokenBatch(1, tokens); - uint256 balanceAfter_SplitClone_ipAcct1 = mockToken.balanceOf(ipAcct[1]); - uint256 balanceAfter_AncestorVault_ipAcct3 = mockToken.balanceOf(ancestorVault_ipAcct3); + // take snapshot + vm.warp(block.timestamp + 7 days + 1); + IpRoyaltyVault(ipRoyaltyVault2).snapshot(); - // IPAccount1's split clone should receive 30% of the total payment to IPAccount3 - assertApproxEqAbs( - balanceAfter_SplitClone_ipAcct1 - balanceBefore_SplitClone_ipAcct1, - // should be 200 * 2 * 1 ether / 1000 - (defaultCommRevShare * 2 * totalPaymentToIpAcct3) / 1000, - 100 - ); - // All money in ancestor vault of IPAccount3 must be sent to IPAccount1's split clone - assertEq( - balanceAfter_SplitClone_ipAcct1 - balanceBefore_SplitClone_ipAcct1, - balanceBefore_AncestorVault_ipAcct3 - ); - assertEq(balanceAfter_AncestorVault_ipAcct3, 0); + IpRoyaltyVault(ipRoyaltyVault2).claimRevenueByTokenBatch(1, tokens); } } } diff --git a/test/foundry/modules/licensing/PILPolicyFramework.derivation.t.sol b/test/foundry/modules/licensing/PILPolicyFramework.derivation.t.sol index a1d181ff..f2a330f6 100644 --- a/test/foundry/modules/licensing/PILPolicyFramework.derivation.t.sol +++ b/test/foundry/modules/licensing/PILPolicyFramework.derivation.t.sol @@ -53,9 +53,6 @@ contract PILPolicyFrameworkCompatibilityTest is BaseTest { ipId2 = ipAccountRegistry.registerIpAccount(block.chainid, address(mockNFT), 2); vm.label(ipId1, "IP1"); vm.label(ipId2, "IP2"); - - vm.label(LIQUID_SPLIT_FACTORY, "LIQUID_SPLIT_FACTORY"); - vm.label(LIQUID_SPLIT_MAIN, "LIQUID_SPLIT_MAIN"); } ///////////////////////////////////////////////////////////// diff --git a/test/foundry/modules/royalty/AncestorsVaultLAP.t.sol b/test/foundry/modules/royalty/AncestorsVaultLAP.t.sol deleted file mode 100644 index 6e90393b..00000000 --- a/test/foundry/modules/royalty/AncestorsVaultLAP.t.sol +++ /dev/null @@ -1,507 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.23; - -import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; - -import { AncestorsVaultLAP } from "../../../../contracts/modules/royalty/policies/AncestorsVaultLAP.sol"; -import { ILiquidSplitClone } from "../../../../contracts/interfaces/modules/royalty/policies/ILiquidSplitClone.sol"; -import { ILiquidSplitMain } from "../../../../contracts/interfaces/modules/royalty/policies/ILiquidSplitMain.sol"; -import { Errors } from "../../../../contracts/lib/Errors.sol"; - -import { BaseTest } from "../../utils/BaseTest.t.sol"; - -contract TestAncestorsVaultLAP is BaseTest { - event Claimed(address ipId, address claimerIpId, ERC20[] tokens); - - struct InitParams { - address[] targetAncestors; - uint32[] targetRoyaltyAmount; - address[] parentAncestors1; - address[] parentAncestors2; - uint32[] parentAncestorsRoyalties1; - uint32[] parentAncestorsRoyalties2; - } - - InitParams internal initParamsMax; - bytes internal MAX_ANCESTORS; - address[] internal MAX_ANCESTORS_ = new address[](14); - uint32[] internal MAX_ANCESTORS_ROYALTY_ = new uint32[](14); - address[] internal parentsIpIds100; - - AncestorsVaultLAP internal ancestorsVault100; - AncestorsVaultLAP internal ancestorsVault1; - address internal splitClone100; - address internal ancestorsVaultAddr100; - address internal ancestorsVaultAddr1; - uint32 internal royaltyStack100; - uint256 internal ethAccrued; - uint256 internal usdcAccrued; - uint256 internal linkAccrued; - address internal ipIdToClaim; - ERC20[] internal tokens = new ERC20[](2); - uint32 internal expectedRnft7; - address internal claimerIpId7; - address internal claimerIpId10; - address internal splitClone7; - - function setUp() public override { - super.setUp(); - buildDeployModuleCondition( - DeployModuleCondition({ disputeModule: false, royaltyModule: true, licensingModule: false }) - ); - buildDeployPolicyCondition(DeployPolicyCondition({ arbitrationPolicySP: false, royaltyPolicyLAP: true })); - - deployConditionally(); - postDeploymentSetup(); - - vm.startPrank(address(royaltyModule)); - _setupMaxUniqueTree(); - - // setup ancestors vault 100 data - parentsIpIds100 = new address[](2); - parentsIpIds100[0] = address(1); - parentsIpIds100[1] = address(2); - - bytes[] memory encodedLicenseData = new bytes[](2); - for (uint32 i = 0; i < parentsIpIds100.length; i++) { - encodedLicenseData[i] = abi.encode(parentsIpIds100[i]); - } - - royaltyPolicyLAP.onLinkToParents(address(100), parentsIpIds100, encodedLicenseData, MAX_ANCESTORS); - vm.stopPrank(); - - (, splitClone100, ancestorsVaultAddr100, royaltyStack100, , ) = royaltyPolicyLAP.getRoyaltyData(address(100)); - ancestorsVault100 = AncestorsVaultLAP(ancestorsVaultAddr100); - - (, , ancestorsVaultAddr1, , , ) = royaltyPolicyLAP.getRoyaltyData(address(1)); - ancestorsVault1 = AncestorsVaultLAP(ancestorsVaultAddr1); - - ipIdToClaim = address(100); - tokens[0] = USDC; - tokens[1] = LINK; - - expectedRnft7 = 7; - claimerIpId7 = address(7); - claimerIpId10 = address(10); - - (, splitClone7, , , , ) = royaltyPolicyLAP.getRoyaltyData(claimerIpId7); - - // send tokens to ancestors vault - ethAccrued = 1 ether; - usdcAccrued = 1000 * 10 ** 6; - linkAccrued = 100 * 10 ** 18; - vm.deal(address(ancestorsVault100), ethAccrued); - USDC.mint(address(ancestorsVault100), usdcAccrued); - - LINK.mint(address(ancestorsVault100), linkAccrued); - } - - function _setupMaxUniqueTree() internal { - // init royalty policy for roots - address[] memory nullTargetAncestors = new address[](0); - uint32[] memory nullTargetRoyaltyAmount = new uint32[](0); - uint32[] memory parentRoyalties = new uint32[](0); - address[] memory nullParentAncestors1 = new address[](0); - address[] memory nullParentAncestors2 = new address[](0); - uint32[] memory nullParentAncestorsRoyalties1 = new uint32[](0); - uint32[] memory nullParentAncestorsRoyalties2 = new uint32[](0); - InitParams memory nullInitParams = InitParams({ - targetAncestors: nullTargetAncestors, - targetRoyaltyAmount: nullTargetRoyaltyAmount, - parentAncestors1: nullParentAncestors1, - parentAncestors2: nullParentAncestors2, - parentAncestorsRoyalties1: nullParentAncestorsRoyalties1, - parentAncestorsRoyalties2: nullParentAncestorsRoyalties2 - }); - bytes memory nullBytes = abi.encode(nullInitParams); - royaltyPolicyLAP.onLicenseMinting(address(7), abi.encode(uint32(7)), nullBytes); - royaltyPolicyLAP.onLicenseMinting(address(8), abi.encode(uint32(8)), nullBytes); - royaltyPolicyLAP.onLicenseMinting(address(9), abi.encode(uint32(9)), nullBytes); - royaltyPolicyLAP.onLicenseMinting(address(10), abi.encode(uint32(10)), nullBytes); - royaltyPolicyLAP.onLicenseMinting(address(11), abi.encode(uint32(11)), nullBytes); - royaltyPolicyLAP.onLicenseMinting(address(12), abi.encode(uint32(12)), nullBytes); - royaltyPolicyLAP.onLicenseMinting(address(13), abi.encode(uint32(13)), nullBytes); - royaltyPolicyLAP.onLicenseMinting(address(14), abi.encode(uint32(14)), nullBytes); - - // init 2nd level with children - address[] memory parents = new address[](2); - address[] memory targetAncestors1 = new address[](2); - uint32[] memory targetRoyaltyAmount1 = new uint32[](2); - uint32[] memory parentRoyalties1 = new uint32[](2); - bytes[] memory encodedLicenseData = new bytes[](2); - - // 3 is child of 7 and 8 - parents[0] = address(7); - parents[1] = address(8); - parentRoyalties1[0] = 7; - parentRoyalties1[1] = 8; - targetAncestors1[0] = address(7); - targetAncestors1[1] = address(8); - targetRoyaltyAmount1[0] = 7; - targetRoyaltyAmount1[1] = 8; - InitParams memory initParams = InitParams({ - targetAncestors: targetAncestors1, - targetRoyaltyAmount: targetRoyaltyAmount1, - parentAncestors1: nullParentAncestors1, - parentAncestors2: nullParentAncestors2, - parentAncestorsRoyalties1: nullParentAncestorsRoyalties1, - parentAncestorsRoyalties2: nullParentAncestorsRoyalties2 - }); - for (uint32 i = 0; i < parentRoyalties1.length; i++) { - encodedLicenseData[i] = abi.encode(parentRoyalties1[i]); - } - bytes memory encodedBytes = abi.encode(initParams); - royaltyPolicyLAP.onLinkToParents(address(3), parents, encodedLicenseData, encodedBytes); - - // 4 is child of 9 and 10 - parents[0] = address(9); - parents[1] = address(10); - parentRoyalties1[0] = 9; - parentRoyalties1[1] = 10; - targetAncestors1[0] = address(9); - targetAncestors1[1] = address(10); - targetRoyaltyAmount1[0] = 9; - targetRoyaltyAmount1[1] = 10; - initParams = InitParams({ - targetAncestors: targetAncestors1, - targetRoyaltyAmount: targetRoyaltyAmount1, - parentAncestors1: nullParentAncestors1, - parentAncestors2: nullParentAncestors2, - parentAncestorsRoyalties1: nullParentAncestorsRoyalties1, - parentAncestorsRoyalties2: nullParentAncestorsRoyalties2 - }); - for (uint32 i = 0; i < parentRoyalties1.length; i++) { - encodedLicenseData[i] = abi.encode(parentRoyalties1[i]); - } - encodedBytes = abi.encode(initParams); - royaltyPolicyLAP.onLinkToParents(address(4), parents, encodedLicenseData, encodedBytes); - - // 5 is child of 11 and 12 - parents[0] = address(11); - parents[1] = address(12); - parentRoyalties1[0] = 11; - parentRoyalties1[1] = 12; - targetAncestors1[0] = address(11); - targetAncestors1[1] = address(12); - targetRoyaltyAmount1[0] = 11; - targetRoyaltyAmount1[1] = 12; - initParams = InitParams({ - targetAncestors: targetAncestors1, - targetRoyaltyAmount: targetRoyaltyAmount1, - parentAncestors1: nullParentAncestors1, - parentAncestors2: nullParentAncestors2, - parentAncestorsRoyalties1: nullParentAncestorsRoyalties1, - parentAncestorsRoyalties2: nullParentAncestorsRoyalties2 - }); - for (uint32 i = 0; i < parentRoyalties1.length; i++) { - encodedLicenseData[i] = abi.encode(parentRoyalties1[i]); - } - encodedBytes = abi.encode(initParams); - royaltyPolicyLAP.onLinkToParents(address(5), parents, encodedLicenseData, encodedBytes); - - // 6 is child of 13 and 14 - parents[0] = address(13); - parents[1] = address(14); - parentRoyalties1[0] = 13; - parentRoyalties1[1] = 14; - targetAncestors1[0] = address(13); - targetAncestors1[1] = address(14); - targetRoyaltyAmount1[0] = 13; - targetRoyaltyAmount1[1] = 14; - initParams = InitParams({ - targetAncestors: targetAncestors1, - targetRoyaltyAmount: targetRoyaltyAmount1, - parentAncestors1: nullParentAncestors1, - parentAncestors2: nullParentAncestors2, - parentAncestorsRoyalties1: nullParentAncestorsRoyalties1, - parentAncestorsRoyalties2: nullParentAncestorsRoyalties2 - }); - for (uint32 i = 0; i < parentRoyalties1.length; i++) { - encodedLicenseData[i] = abi.encode(parentRoyalties1[i]); - } - encodedBytes = abi.encode(initParams); - royaltyPolicyLAP.onLinkToParents(address(6), parents, encodedLicenseData, encodedBytes); - - // init 3rd level with children - address[] memory targetAncestors2 = new address[](6); - uint32[] memory targetRoyaltyAmount2 = new uint32[](6); - address[] memory parentAncestors1 = new address[](2); - address[] memory parentAncestors2 = new address[](2); - uint32[] memory parentAncestorsRoyalties1 = new uint32[](2); - uint32[] memory parentAncestorsRoyalties2 = new uint32[](2); - - // 1 is child of 3 and 4 - parents[0] = address(3); - parents[1] = address(4); - parentRoyalties1[0] = 3; - parentRoyalties1[1] = 4; - parentAncestors1[0] = address(7); - parentAncestors1[1] = address(8); - parentAncestors2[0] = address(9); - parentAncestors2[1] = address(10); - parentAncestorsRoyalties1[0] = 7; - parentAncestorsRoyalties1[1] = 8; - parentAncestorsRoyalties2[0] = 9; - parentAncestorsRoyalties2[1] = 10; - targetAncestors2[0] = address(3); - targetAncestors2[1] = address(7); - targetAncestors2[2] = address(8); - targetAncestors2[3] = address(4); - targetAncestors2[4] = address(9); - targetAncestors2[5] = address(10); - targetRoyaltyAmount2[0] = 3; - targetRoyaltyAmount2[1] = 7; - targetRoyaltyAmount2[2] = 8; - targetRoyaltyAmount2[3] = 4; - targetRoyaltyAmount2[4] = 9; - targetRoyaltyAmount2[5] = 10; - initParams = InitParams({ - targetAncestors: targetAncestors2, - targetRoyaltyAmount: targetRoyaltyAmount2, - parentAncestors1: parentAncestors1, - parentAncestors2: parentAncestors2, - parentAncestorsRoyalties1: parentAncestorsRoyalties1, - parentAncestorsRoyalties2: parentAncestorsRoyalties2 - }); - for (uint32 i = 0; i < parentRoyalties1.length; i++) { - encodedLicenseData[i] = abi.encode(parentRoyalties1[i]); - } - encodedBytes = abi.encode(initParams); - royaltyPolicyLAP.onLinkToParents(address(1), parents, encodedLicenseData, encodedBytes); - - // 2 is child of 5 and 6 - parents[0] = address(5); - parents[1] = address(6); - parentRoyalties1[0] = 5; - parentRoyalties1[1] = 6; - parentAncestors1[0] = address(11); - parentAncestors1[1] = address(12); - parentAncestors2[0] = address(13); - parentAncestors2[1] = address(14); - parentAncestorsRoyalties1[0] = 11; - parentAncestorsRoyalties1[1] = 12; - parentAncestorsRoyalties2[0] = 13; - parentAncestorsRoyalties2[1] = 14; - targetAncestors2[0] = address(5); - targetAncestors2[1] = address(11); - targetAncestors2[2] = address(12); - targetAncestors2[3] = address(6); - targetAncestors2[4] = address(13); - targetAncestors2[5] = address(14); - targetRoyaltyAmount2[0] = 5; - targetRoyaltyAmount2[1] = 11; - targetRoyaltyAmount2[2] = 12; - targetRoyaltyAmount2[3] = 6; - targetRoyaltyAmount2[4] = 13; - targetRoyaltyAmount2[5] = 14; - initParams = InitParams({ - targetAncestors: targetAncestors2, - targetRoyaltyAmount: targetRoyaltyAmount2, - parentAncestors1: parentAncestors1, - parentAncestors2: parentAncestors2, - parentAncestorsRoyalties1: parentAncestorsRoyalties1, - parentAncestorsRoyalties2: parentAncestorsRoyalties2 - }); - for (uint32 i = 0; i < parentRoyalties1.length; i++) { - encodedLicenseData[i] = abi.encode(parentRoyalties1[i]); - } - encodedBytes = abi.encode(initParams); - royaltyPolicyLAP.onLinkToParents(address(2), parents, encodedLicenseData, encodedBytes); - - address[] memory parentAncestors3 = new address[](6); - address[] memory parentAncestors4 = new address[](6); - uint32[] memory parentAncestorsRoyalties3 = new uint32[](6); - uint32[] memory parentAncestorsRoyalties4 = new uint32[](6); - uint32[] memory parentRoyalties3 = new uint32[](2); - - // 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; - - parentAncestors3[0] = address(3); - parentAncestors3[1] = address(7); - parentAncestors3[2] = address(8); - parentAncestors3[3] = address(4); - parentAncestors3[4] = address(9); - parentAncestors3[5] = address(10); - parentAncestorsRoyalties3[0] = 3; - parentAncestorsRoyalties3[1] = 7; - parentAncestorsRoyalties3[2] = 8; - parentAncestorsRoyalties3[3] = 4; - parentAncestorsRoyalties3[4] = 9; - parentAncestorsRoyalties3[5] = 10; - - parentAncestors4[0] = address(5); - parentAncestors4[1] = address(11); - parentAncestors4[2] = address(12); - parentAncestors4[3] = address(6); - parentAncestors4[4] = address(13); - parentAncestors4[5] = address(14); - parentAncestorsRoyalties4[0] = 5; - parentAncestorsRoyalties4[1] = 11; - parentAncestorsRoyalties4[2] = 12; - parentAncestorsRoyalties4[3] = 6; - parentAncestorsRoyalties4[4] = 13; - parentAncestorsRoyalties4[5] = 14; - - parentRoyalties3[0] = 1; - parentRoyalties3[1] = 2; - - initParamsMax = InitParams({ - targetAncestors: MAX_ANCESTORS_, - targetRoyaltyAmount: MAX_ANCESTORS_ROYALTY_, - parentAncestors1: parentAncestors3, - parentAncestors2: parentAncestors4, - parentAncestorsRoyalties1: parentAncestorsRoyalties3, - parentAncestorsRoyalties2: parentAncestorsRoyalties4 - }); - - MAX_ANCESTORS = abi.encode(initParamsMax); - } - - function test_AncestorsVaultLAP_claim_AlreadyClaimed() public { - ancestorsVault100.claim(ipIdToClaim, claimerIpId7, tokens); - - vm.expectRevert(Errors.AncestorsVaultLAP__AlreadyClaimed.selector); - ancestorsVault100.claim(ipIdToClaim, claimerIpId7, tokens); - } - - function test_AncestorsVaultLAP_claim_InvalidVault() public { - vm.expectRevert(Errors.AncestorsVaultLAP__InvalidVault.selector); - ancestorsVault1.claim(ipIdToClaim, claimerIpId7, tokens); - } - - function test_AncestorsVaultLAP_claim_ClaimerNotAnAncestor() public { - address notAnAncestor = address(50); - vm.expectRevert(Errors.AncestorsVaultLAP__ClaimerNotAnAncestor.selector); - ancestorsVault100.claim(ipIdToClaim, notAnAncestor, tokens); - } - - function test_AncestorsVaultLAP_claim_ERC20BalanceNotZero() public { - USDC.mint(address(splitClone100), 1000 * 10 ** 6); - - address[] memory ancestors = new address[](2); - ancestors[0] = address(100); - ancestors[1] = address(ancestorsVault100); - - ILiquidSplitClone(splitClone100).distributeFunds(address(USDC), ancestors, address(0)); - - ERC20 token_ = USDC; - // value of 0 is stored as 1 in 0xSplits (for cheaper, warm storage) - assertGt( - ILiquidSplitMain(royaltyPolicyLAP.LIQUID_SPLIT_MAIN()).getERC20Balance(address(ancestorsVault100), token_), - 1 - ); - - vm.expectRevert(Errors.AncestorsVaultLAP__ERC20BalanceNotZero.selector); - ancestorsVault100.claim(ipIdToClaim, claimerIpId7, tokens); - } - - function test_AncestorsVaultLAP_claim() public { - // address 7 as the root will claim from address 100 - uint256 rnftBalBefore = ILiquidSplitClone(splitClone100).balanceOf(address(7), 0); - uint256 ancestorsVaultUSDCBalBefore = USDC.balanceOf(address(ancestorsVault100)); - uint256 claimerVaultUSDCBalBefore = USDC.balanceOf(address(7)); - uint256 ancestorsVaultLinkBalBefore = LINK.balanceOf(address(ancestorsVault100)); - uint256 claimerVaultLinkBalBefore = LINK.balanceOf(address(7)); - - vm.expectEmit(true, true, true, true, address(ancestorsVault100)); - emit Claimed(ipIdToClaim, claimerIpId7, tokens); - - ancestorsVault100.claim(ipIdToClaim, claimerIpId7, tokens); - - uint256 rnftBalAfter = ILiquidSplitClone(splitClone100).balanceOf(address(7), 0); - uint256 ancestorsVaultUSDCBalAfter = USDC.balanceOf(address(ancestorsVault100)); - uint256 claimerVaultUSDCBalAfter = USDC.balanceOf(address(7)); - uint256 ancestorsVaultLinkBalAfter = LINK.balanceOf(address(ancestorsVault100)); - uint256 claimerVaultLinkBalAfter = LINK.balanceOf(address(7)); - - assertEq(ancestorsVault100.isClaimed(ipIdToClaim, claimerIpId7), true); - assertEq(rnftBalAfter - rnftBalBefore, expectedRnft7); - assertEq( - ancestorsVaultUSDCBalBefore - ancestorsVaultUSDCBalAfter, - (usdcAccrued * expectedRnft7) / (royaltyStack100) - ); - assertEq( - claimerVaultUSDCBalAfter - claimerVaultUSDCBalBefore, - (usdcAccrued * expectedRnft7) / (royaltyStack100) - ); - assertEq( - ancestorsVaultLinkBalBefore - ancestorsVaultLinkBalAfter, - (linkAccrued * expectedRnft7) / (royaltyStack100) - ); - assertEq( - claimerVaultLinkBalAfter - claimerVaultLinkBalBefore, - (linkAccrued * expectedRnft7) / (royaltyStack100) - ); - } - - function test_AncestorsVaultLAP_claim_MultipleClaims() public { - ancestorsVault100.claim(ipIdToClaim, claimerIpId10, tokens); - - // address 7 as the root will claim from address 100 - uint256 rnftBalBefore = ILiquidSplitClone(splitClone100).balanceOf(address(7), 0); - uint256 ancestorsVaultUSDCBalBefore = USDC.balanceOf(address(ancestorsVault100)); - uint256 claimerVaultUSDCBalBefore = USDC.balanceOf(address(7)); - uint256 ancestorsVaultLinkBalBefore = LINK.balanceOf(address(ancestorsVault100)); - uint256 claimerVaultLinkBalBefore = LINK.balanceOf(address(7)); - - vm.expectEmit(true, true, true, true, address(ancestorsVault100)); - emit Claimed(ipIdToClaim, claimerIpId7, tokens); - - ancestorsVault100.claim(ipIdToClaim, claimerIpId7, tokens); - - uint256 rnftBalAfter = ILiquidSplitClone(splitClone100).balanceOf(address(7), 0); - uint256 ancestorsVaultUSDCBalAfter = USDC.balanceOf(address(ancestorsVault100)); - uint256 claimerVaultUSDCBalAfter = USDC.balanceOf(address(7)); - uint256 ancestorsVaultLinkBalAfter = LINK.balanceOf(address(ancestorsVault100)); - uint256 claimerVaultLinkBalAfter = LINK.balanceOf(address(7)); - - assertEq(ancestorsVault100.isClaimed(ipIdToClaim, claimerIpId7), true); - assertEq(rnftBalAfter - rnftBalBefore, expectedRnft7); - assertEq( - ancestorsVaultUSDCBalBefore - ancestorsVaultUSDCBalAfter, - (usdcAccrued * expectedRnft7) / (royaltyStack100) - ); - assertEq( - claimerVaultUSDCBalAfter - claimerVaultUSDCBalBefore, - (usdcAccrued * expectedRnft7) / (royaltyStack100) - ); - assertEq( - ancestorsVaultLinkBalBefore - ancestorsVaultLinkBalAfter, - (linkAccrued * expectedRnft7) / (royaltyStack100) - ); - assertEq( - claimerVaultLinkBalAfter - claimerVaultLinkBalBefore, - (linkAccrued * expectedRnft7) / (royaltyStack100) - ); - } -} diff --git a/test/foundry/modules/royalty/IpRoyaltyVault.t.sol b/test/foundry/modules/royalty/IpRoyaltyVault.t.sol new file mode 100644 index 00000000..83814cfe --- /dev/null +++ b/test/foundry/modules/royalty/IpRoyaltyVault.t.sol @@ -0,0 +1,410 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.23; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import { IpRoyaltyVault } from "../../../../contracts/modules/royalty/policies/IpRoyaltyVault.sol"; +import { Errors } from "../../../../contracts/lib/Errors.sol"; + +import { BaseTest } from "../../utils/BaseTest.t.sol"; + +contract TestIpRoyaltyVault is BaseTest { + event RoyaltyTokensCollected(address ancestorIpId, uint256 royaltyTokensCollected); + event SnapshotCompleted(uint256 snapshotId, uint256 timestamp, uint32 unclaimedTokens); + + IpRoyaltyVault ipRoyaltyVault; + + function setUp() public override { + super.setUp(); + buildDeployModuleCondition( + DeployModuleCondition({ disputeModule: false, royaltyModule: true, licensingModule: false }) + ); + buildDeployPolicyCondition(DeployPolicyCondition({ arbitrationPolicySP: false, royaltyPolicyLAP: true })); + deployConditionally(); + postDeploymentSetup(); + + 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(); + + (, address IpRoyaltyVault2, , , ) = royaltyPolicyLAP.getRoyaltyData(address(2)); + ipRoyaltyVault = IpRoyaltyVault(IpRoyaltyVault2); + } + + 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); + + // 3 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(3), 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 3 and 4 + parents[0] = address(3); + 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(100), address(royaltyPolicyLAP), parents, encodedLicenseData, ""); + //royaltyPolicyLAP.onLinkToParents(address(100), parents, encodedLicenseData, ""); + } + + function test_IpRoyaltyVault_AddIpRoyaltyVaultTokens_NotRoyaltyPolicyLAP() public { + vm.expectRevert(Errors.IpRoyaltyVault__NotRoyaltyPolicyLAP.selector); + ipRoyaltyVault.addIpRoyaltyVaultTokens(address(0)); + } + + function test_IpRoyaltyVault_AddIpRoyaltyVaultTokens() public { + vm.startPrank(address(royaltyPolicyLAP)); + ipRoyaltyVault.addIpRoyaltyVaultTokens(address(1)); + + address[] memory tokens = ipRoyaltyVault.getVaultTokens(); + + assertEq(tokens.length, 1); + assertEq(tokens[0], address(1)); + } + + function test_IpRoyaltyVault_ClaimableRevenue() public { + // payment is made to vault + uint256 royaltyAmount = 100000 * 10 ** 6; + USDC.mint(address(100), 100000 * 10 ** 6); // 100k USDC + vm.startPrank(address(100)); + USDC.approve(address(royaltyPolicyLAP), royaltyAmount); + royaltyModule.payRoyaltyOnBehalf(address(2), address(100), address(USDC), royaltyAmount); + vm.stopPrank(); + + // take snapshot + vm.warp(block.timestamp + 7 days + 1); + ipRoyaltyVault.snapshot(); + + (, , uint32 royaltyStack2, , ) = royaltyPolicyLAP.getRoyaltyData(address(2)); + + uint256 claimableRevenue = ipRoyaltyVault.claimableRevenue(address(2), 1, address(USDC)); + assertEq( + claimableRevenue, + royaltyAmount - (royaltyAmount * royaltyStack2) / royaltyPolicyLAP.TOTAL_RT_SUPPLY() + ); + } + + function test_IpRoyaltyVault_ClaimRevenueByTokenBatch() public { + // payment is made to vault + uint256 royaltyAmount = 100000 * 10 ** 6; + USDC.mint(address(100), royaltyAmount); // 100k USDC + LINK.mint(address(100), royaltyAmount); // 100k LINK + vm.startPrank(address(100)); + USDC.approve(address(royaltyPolicyLAP), royaltyAmount); + royaltyModule.payRoyaltyOnBehalf(address(2), address(100), address(USDC), royaltyAmount); + LINK.approve(address(royaltyPolicyLAP), royaltyAmount); + royaltyModule.payRoyaltyOnBehalf(address(2), address(100), address(LINK), royaltyAmount); + vm.stopPrank(); + + // take snapshot + vm.warp(block.timestamp + 7 days + 1); + ipRoyaltyVault.snapshot(); + + (, , uint32 royaltyStack2, , ) = royaltyPolicyLAP.getRoyaltyData(address(2)); + + address[] memory tokens = new address[](2); + tokens[0] = address(USDC); + tokens[1] = address(LINK); + + uint256 userUsdcBalanceBefore = USDC.balanceOf(address(2)); + uint256 userLinkBalanceBefore = LINK.balanceOf(address(2)); + uint256 contractUsdcBalanceBefore = USDC.balanceOf(address(ipRoyaltyVault)); + uint256 contractLinkBalanceBefore = LINK.balanceOf(address(ipRoyaltyVault)); + uint256 usdcClaimVaultBefore = ipRoyaltyVault.claimVaultAmount(address(USDC)); + uint256 linkClaimVaultBefore = ipRoyaltyVault.claimVaultAmount(address(LINK)); + + vm.startPrank(address(2)); + ipRoyaltyVault.claimRevenueByTokenBatch(1, tokens); + + uint256 expectedAmount = royaltyAmount - (royaltyAmount * royaltyStack2) / royaltyPolicyLAP.TOTAL_RT_SUPPLY(); + + assertEq(USDC.balanceOf(address(2)) - userUsdcBalanceBefore, expectedAmount); + assertEq(LINK.balanceOf(address(2)) - userLinkBalanceBefore, expectedAmount); + assertEq(contractUsdcBalanceBefore - USDC.balanceOf(address(ipRoyaltyVault)), expectedAmount); + assertEq(contractLinkBalanceBefore - LINK.balanceOf(address(ipRoyaltyVault)), expectedAmount); + assertEq(usdcClaimVaultBefore - ipRoyaltyVault.claimVaultAmount(address(USDC)), expectedAmount); + assertEq(linkClaimVaultBefore - ipRoyaltyVault.claimVaultAmount(address(LINK)), expectedAmount); + assertEq(ipRoyaltyVault.isClaimedAtSnapshot(1, address(2), address(USDC)), true); + assertEq(ipRoyaltyVault.isClaimedAtSnapshot(1, address(2), address(LINK)), true); + } + + function test_IpRoyaltyVault_ClaimRevenueBySnapshotBatch() public { + uint256 royaltyAmount = 100000 * 10 ** 6; + USDC.mint(address(100), royaltyAmount); // 100k USDC + + // 1st payment is made to vault + vm.startPrank(address(100)); + USDC.approve(address(royaltyPolicyLAP), royaltyAmount); + royaltyModule.payRoyaltyOnBehalf(address(2), address(100), 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(100), 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; + + uint256 userUsdcBalanceBefore = USDC.balanceOf(address(2)); + uint256 contractUsdcBalanceBefore = USDC.balanceOf(address(ipRoyaltyVault)); + uint256 usdcClaimVaultBefore = ipRoyaltyVault.claimVaultAmount(address(USDC)); + + vm.startPrank(address(2)); + ipRoyaltyVault.claimRevenueBySnapshotBatch(snapshots, address(USDC)); + + uint256 expectedAmount = royaltyAmount - (royaltyAmount * royaltyStack2) / royaltyPolicyLAP.TOTAL_RT_SUPPLY(); + + assertEq(USDC.balanceOf(address(2)) - userUsdcBalanceBefore, expectedAmount); + assertEq(contractUsdcBalanceBefore - USDC.balanceOf(address(ipRoyaltyVault)), expectedAmount); + assertEq(usdcClaimVaultBefore - ipRoyaltyVault.claimVaultAmount(address(USDC)), expectedAmount); + assertEq(ipRoyaltyVault.isClaimedAtSnapshot(1, address(2), address(USDC)), true); + 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_Snapshot() public { + // payment is made to vault + uint256 royaltyAmount = 100000 * 10 ** 6; + USDC.mint(address(100), royaltyAmount); // 100k USDC + LINK.mint(address(100), royaltyAmount); // 100k LINK + vm.startPrank(address(100)); + USDC.approve(address(royaltyPolicyLAP), royaltyAmount); + royaltyModule.payRoyaltyOnBehalf(address(2), address(100), address(USDC), royaltyAmount); + LINK.approve(address(royaltyPolicyLAP), royaltyAmount); + royaltyModule.payRoyaltyOnBehalf(address(2), address(100), 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)); + uint256 usdcAncestorsVaultBefore = ipRoyaltyVault.ancestorsVaultAmount(address(USDC)); + uint256 linkAncestorsVaultBefore = ipRoyaltyVault.ancestorsVaultAmount(address(LINK)); + + (, , uint32 royaltyStack2, , ) = royaltyPolicyLAP.getRoyaltyData(address(2)); + + vm.expectEmit(true, true, true, true, address(ipRoyaltyVault)); + emit SnapshotCompleted(1, block.timestamp, royaltyStack2); + + 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); + + ipRoyaltyVault.collectRoyaltyTokens(address(5)); + ipRoyaltyVault.collectRoyaltyTokens(address(11)); + ipRoyaltyVault.collectRoyaltyTokens(address(12)); + ipRoyaltyVault.collectRoyaltyTokens(address(6)); + ipRoyaltyVault.collectRoyaltyTokens(address(13)); + ipRoyaltyVault.collectRoyaltyTokens(address(14)); + + // take snapshot + vm.warp(block.timestamp + 7 days + 1); + ipRoyaltyVault.snapshot(); + + // all USDC was claimed but LINK was not + assertEq(ipRoyaltyVault.getVaultTokens().length, 1); + } + + function test_IpRoyaltyVault_CollectRoyaltyTokens_AlreadyClaimed() public { + ipRoyaltyVault.collectRoyaltyTokens(address(5)); + + vm.expectRevert(Errors.IpRoyaltyVault__AlreadyClaimed.selector); + ipRoyaltyVault.collectRoyaltyTokens(address(5)); + } + + function test_IpRoyaltyVault_CollectRoyaltyTokens_ClaimerNotAnAncestor() public { + vm.expectRevert(Errors.IpRoyaltyVault__ClaimerNotAnAncestor.selector); + ipRoyaltyVault.collectRoyaltyTokens(address(0)); + } + + 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(); + + // payment is made to vault + USDC.mint(address(100), royaltyAmount); // 100k USDC + vm.startPrank(address(100)); + USDC.approve(address(royaltyPolicyLAP), royaltyAmount); + royaltyModule.payRoyaltyOnBehalf(address(2), address(100), 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 RoyaltyTokensCollected(address(5), parentRoyalty); + + ipRoyaltyVault.collectRoyaltyTokens(address(5)); + + assertEq(USDC.balanceOf(address(5)) - userUsdcBalanceBefore, accruedCollectableRevenue); + assertEq(contractUsdcBalanceBefore - USDC.balanceOf(address(ipRoyaltyVault)), accruedCollectableRevenue); + assertEq(ipRoyaltyVault.isClaimedByAncestor(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); + assertEq( + ancestorsVaultAmountBefore - ipRoyaltyVault.ancestorsVaultAmount(address(USDC)), + accruedCollectableRevenue + ); + } +} diff --git a/test/foundry/modules/royalty/RoyaltyModule.t.sol b/test/foundry/modules/royalty/RoyaltyModule.t.sol index 44616a8c..fc3b4553 100644 --- a/test/foundry/modules/royalty/RoyaltyModule.t.sol +++ b/test/foundry/modules/royalty/RoyaltyModule.t.sol @@ -47,9 +47,7 @@ contract TestRoyaltyModule is BaseTest { USDC.mint(ipAccount2, 1000 * 10 ** 6); // 1000 USDC - address impl = address( - new RoyaltyPolicyLAP(getRoyaltyModule(), getLicensingModule(), LIQUID_SPLIT_FACTORY, LIQUID_SPLIT_MAIN) - ); + address impl = address(new RoyaltyPolicyLAP(getRoyaltyModule(), getLicensingModule())); royaltyPolicyLAP2 = RoyaltyPolicyLAP( TestProxyHelper.deployUUPSProxy(impl, abi.encodeCall(RoyaltyPolicyLAP.initialize, (getGovernance()))) ); @@ -268,11 +266,11 @@ contract TestRoyaltyModule is BaseTest { vm.stopPrank(); vm.startPrank(u.admin); - royaltyModule.whitelistRoyaltyPolicy(address(royaltyPolicyLAP2), true); + royaltyModule.whitelistRoyaltyPolicy(address(royaltyPolicyLAP), true); vm.stopPrank(); vm.startPrank(address(licensingModule)); - royaltyModule.onLicenseMinting(licensor, address(royaltyPolicyLAP2), licenseData, nullBytes); + royaltyModule.onLicenseMinting(licensor, address(royaltyPolicyLAP), licenseData, nullBytes); } function test_RoyaltyModule_onLinkToParents_revert_NotWhitelistedRoyaltyPolicy() public { @@ -468,14 +466,14 @@ contract TestRoyaltyModule is BaseTest { address receiverIpId = address(7); address payerIpId = address(3); - (, address splitClone, , , , ) = royaltyPolicyLAP.getRoyaltyData(receiverIpId); + (, address ipRoyaltyVault, , , ) = royaltyPolicyLAP.getRoyaltyData(receiverIpId); vm.startPrank(payerIpId); USDC.mint(payerIpId, royaltyAmount); USDC.approve(address(royaltyPolicyLAP), royaltyAmount); uint256 payerIpIdUSDCBalBefore = USDC.balanceOf(payerIpId); - uint256 splitCloneUSDCBalBefore = USDC.balanceOf(splitClone); + uint256 ipRoyaltyVaultUSDCBalBefore = USDC.balanceOf(ipRoyaltyVault); vm.expectEmit(true, true, true, true, address(royaltyModule)); emit RoyaltyPaid(receiverIpId, payerIpId, payerIpId, address(USDC), royaltyAmount); @@ -483,10 +481,10 @@ contract TestRoyaltyModule is BaseTest { royaltyModule.payRoyaltyOnBehalf(receiverIpId, payerIpId, address(USDC), royaltyAmount); uint256 payerIpIdUSDCBalAfter = USDC.balanceOf(payerIpId); - uint256 splitCloneUSDCBalAfter = USDC.balanceOf(splitClone); + uint256 ipRoyaltyVaultUSDCBalAfter = USDC.balanceOf(ipRoyaltyVault); assertEq(payerIpIdUSDCBalBefore - payerIpIdUSDCBalAfter, royaltyAmount); - assertEq(splitCloneUSDCBalAfter - splitCloneUSDCBalBefore, royaltyAmount); + assertEq(ipRoyaltyVaultUSDCBalAfter - ipRoyaltyVaultUSDCBalBefore, royaltyAmount); } function test_RoyaltyModule_payLicenseMintingFee() public { @@ -496,7 +494,7 @@ contract TestRoyaltyModule is BaseTest { address licenseRoyaltyPolicy = address(royaltyPolicyLAP); address token = address(USDC); - (, address splitClone, , , , ) = royaltyPolicyLAP.getRoyaltyData(receiverIpId); + (, address ipRoyaltyVault, , , ) = royaltyPolicyLAP.getRoyaltyData(receiverIpId); vm.startPrank(payerAddress); USDC.mint(payerAddress, royaltyAmount); @@ -504,7 +502,7 @@ contract TestRoyaltyModule is BaseTest { vm.stopPrank; uint256 payerAddressUSDCBalBefore = USDC.balanceOf(payerAddress); - uint256 splitCloneUSDCBalBefore = USDC.balanceOf(splitClone); + uint256 ipRoyaltyVaultUSDCBalBefore = USDC.balanceOf(ipRoyaltyVault); vm.expectEmit(true, true, true, true, address(royaltyModule)); emit LicenseMintingFeePaid(receiverIpId, payerAddress, address(USDC), royaltyAmount); @@ -513,9 +511,9 @@ contract TestRoyaltyModule is BaseTest { royaltyModule.payLicenseMintingFee(receiverIpId, payerAddress, licenseRoyaltyPolicy, token, royaltyAmount); uint256 payerAddressUSDCBalAfter = USDC.balanceOf(payerAddress); - uint256 splitCloneUSDCBalAfter = USDC.balanceOf(splitClone); + uint256 ipRoyaltyVaultUSDCBalAfter = USDC.balanceOf(ipRoyaltyVault); assertEq(payerAddressUSDCBalBefore - payerAddressUSDCBalAfter, royaltyAmount); - assertEq(splitCloneUSDCBalAfter - splitCloneUSDCBalBefore, royaltyAmount); + assertEq(ipRoyaltyVaultUSDCBalAfter - ipRoyaltyVaultUSDCBalBefore, royaltyAmount); } } diff --git a/test/foundry/modules/royalty/RoyaltyPolicyLAP.t.sol b/test/foundry/modules/royalty/RoyaltyPolicyLAP.t.sol index 94f665ba..41a5cda6 100644 --- a/test/foundry/modules/royalty/RoyaltyPolicyLAP.t.sol +++ b/test/foundry/modules/royalty/RoyaltyPolicyLAP.t.sol @@ -1,26 +1,12 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.23; -import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import { ERC1155 } from "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; - import { RoyaltyPolicyLAP } from "../../../../contracts/modules/royalty/policies/RoyaltyPolicyLAP.sol"; -import { ILiquidSplitMain } from "../../../../contracts/interfaces/modules/royalty/policies/ILiquidSplitMain.sol"; import { Errors } from "../../../../contracts/lib/Errors.sol"; -import { TestProxyHelper } from "test/foundry/utils/TestProxyHelper.sol"; import { BaseTest } from "../../utils/BaseTest.t.sol"; contract TestRoyaltyPolicyLAP is BaseTest { - event PolicyInitialized( - address ipId, - address splitClone, - address claimer, - uint32 royaltyStack, - address[] targetAncestors, - uint32[] targetRoyaltyAmount - ); - RoyaltyPolicyLAP internal testRoyaltyPolicyLAP; address[] internal MAX_ANCESTORS_ = new address[](14); @@ -165,28 +151,32 @@ contract TestRoyaltyPolicyLAP is BaseTest { parentsIpIds100[1] = address(2); } - function test_RoyaltyPolicyLAP_setAncestorsVaultImplementation_ZeroAncestorsVaultImpl() public { - vm.startPrank(u.admin); - vm.expectRevert(Errors.RoyaltyPolicyLAP__ZeroAncestorsVaultImpl.selector); - royaltyPolicyLAP.setAncestorsVaultImplementation(address(0)); + function test_RoyaltyPolicyLAP_setSnapshotInterval_revert_NotOwner() public { + vm.expectRevert(Errors.Governance__OnlyProtocolAdmin.selector); + royaltyPolicyLAP.setSnapshotInterval(100); } - function test_RoyaltyPolicyLAP_setAncestorsVaultImplementation_ImplementationAlreadySet() public { + function test_RoyaltyPolicyLAP_setSnapshotInterval() public { vm.startPrank(u.admin); - vm.expectRevert(Errors.RoyaltyPolicyLAP__ImplementationAlreadySet.selector); - royaltyPolicyLAP.setAncestorsVaultImplementation(address(2)); + royaltyPolicyLAP.setSnapshotInterval(100); + assertEq(royaltyPolicyLAP.getSnapshotInterval(), 100); } - function test_RoyaltyPolicyLAP_setAncestorsVaultImplementation() public { - address impl = address(new RoyaltyPolicyLAP(address(1), address(2), address(3), address(4))); - RoyaltyPolicyLAP royaltyPolicyLAP2 = RoyaltyPolicyLAP( - TestProxyHelper.deployUUPSProxy(impl, abi.encodeCall(RoyaltyPolicyLAP.initialize, (getGovernance()))) - ); + function test_RoyaltyPolicyLAP_setIpRoyaltyVaultBeacon_revert_NotOwner() public { + vm.expectRevert(Errors.Governance__OnlyProtocolAdmin.selector); + royaltyPolicyLAP.setIpRoyaltyVaultBeacon(address(1)); + } + function testRoyaltyPolicyLAP_setIpRoyaltyVaultBeacon_revert_ZeroIpRoyaltyVaultBeacon() public { vm.startPrank(u.admin); - royaltyPolicyLAP2.setAncestorsVaultImplementation(address(2)); + vm.expectRevert(Errors.RoyaltyPolicyLAP__ZeroIpRoyaltyVaultBeacon.selector); + royaltyPolicyLAP.setIpRoyaltyVaultBeacon(address(0)); + } - assertEq(royaltyPolicyLAP2.ancestorsVaultImpl(), address(2)); + function test_RoyaltyPolicyLAP_setIpRoyaltyVaultBeacon() public { + vm.startPrank(u.admin); + royaltyPolicyLAP.setIpRoyaltyVaultBeacon(address(1)); + assertEq(royaltyPolicyLAP.getIpRoyaltyVaultBeacon(), address(1)); } function test_RoyaltyPolicyLAP_onLicenseMinting_revert_NotRoyaltyModule() public { @@ -196,8 +186,9 @@ contract TestRoyaltyPolicyLAP is BaseTest { } function test_RoyaltyPolicyLAP_onLicenseMinting_revert_AboveRoyaltyStackLimit() public { + uint256 excessPercent = royaltyPolicyLAP.TOTAL_RT_SUPPLY() + 1; vm.expectRevert(Errors.RoyaltyPolicyLAP__AboveRoyaltyStackLimit.selector); - royaltyPolicyLAP.onLicenseMinting(address(100), abi.encode(uint32(1001)), ""); + royaltyPolicyLAP.onLicenseMinting(address(100), abi.encode(excessPercent), ""); } function test_RoyaltyPolicyLAP_onLicenseMinting_revert_LastPositionNotAbleToMintLicense() public { @@ -217,8 +208,7 @@ contract TestRoyaltyPolicyLAP is BaseTest { ( , - address splitClone, - address ancestorsVault, + address ipRoyaltyVault, uint32 royaltyStack, address[] memory ancestors, uint32[] memory ancestorsRoyalties @@ -227,8 +217,7 @@ contract TestRoyaltyPolicyLAP is BaseTest { assertEq(royaltyStack, 0); assertEq(ancestors.length, 0); assertEq(ancestorsRoyalties.length, 0); - assertFalse(splitClone == address(0)); - assertEq(ancestorsVault, address(0)); + assertFalse(ipRoyaltyVault == address(0)); } function test_RoyaltyPolicyLAP_onLinkToParents_revert_NotRoyaltyModule() public { @@ -267,8 +256,7 @@ contract TestRoyaltyPolicyLAP is BaseTest { ( , - address splitClone, - address ancestorsVault, + address ipRoyaltyVault, uint32 royaltyStack, address[] memory ancestors, uint32[] memory ancestorsRoyalties @@ -279,8 +267,7 @@ contract TestRoyaltyPolicyLAP is BaseTest { assertEq(ancestorsRoyalties[i], MAX_ANCESTORS_ROYALTY_[i]); } assertEq(ancestors, MAX_ANCESTORS_); - assertFalse(splitClone == address(0)); - assertFalse(ancestorsVault == address(0)); + assertFalse(ipRoyaltyVault == address(0)); } function test_RoyaltyPolicyLAP_onRoyaltyPayment_NotRoyaltyModule() public { @@ -290,7 +277,7 @@ contract TestRoyaltyPolicyLAP is BaseTest { } function test_RoyaltyPolicyLAP_onRoyaltyPayment() public { - (, address splitClone2, , , , ) = royaltyPolicyLAP.getRoyaltyData(address(2)); + (, address ipRoyaltyVault2, , , ) = royaltyPolicyLAP.getRoyaltyData(address(2)); uint256 royaltyAmount = 1000 * 10 ** 6; USDC.mint(address(1), royaltyAmount); vm.stopPrank(); @@ -301,93 +288,15 @@ contract TestRoyaltyPolicyLAP is BaseTest { vm.startPrank(address(royaltyModule)); - uint256 splitClone2USDCBalBefore = USDC.balanceOf(splitClone2); + uint256 ipRoyaltyVault2USDCBalBefore = USDC.balanceOf(ipRoyaltyVault2); uint256 splitMainUSDCBalBefore = USDC.balanceOf(address(1)); royaltyPolicyLAP.onRoyaltyPayment(address(1), address(2), address(USDC), royaltyAmount); - uint256 splitClone2USDCBalAfter = USDC.balanceOf(splitClone2); + uint256 ipRoyaltyVault2USDCBalAfter = USDC.balanceOf(ipRoyaltyVault2); uint256 splitMainUSDCBalAfter = USDC.balanceOf(address(1)); - assertEq(splitClone2USDCBalAfter - splitClone2USDCBalBefore, royaltyAmount); + assertEq(ipRoyaltyVault2USDCBalAfter - ipRoyaltyVault2USDCBalBefore, royaltyAmount); assertEq(splitMainUSDCBalBefore - splitMainUSDCBalAfter, royaltyAmount); } - - function test_RoyaltyPolicyLAP_distributeIpPoolFunds() public { - (, address splitClone2, address ancestorsVault2, , , ) = royaltyPolicyLAP.getRoyaltyData(address(2)); - - // send USDC to 0xSplitClone - uint256 royaltyAmount = 1000 * 10 ** 6; - USDC.mint(splitClone2, royaltyAmount); - - address[] memory accounts = new address[](2); - accounts[0] = address(2); - accounts[1] = ancestorsVault2; - - uint256 splitClone2USDCBalBefore = USDC.balanceOf(splitClone2); - uint256 splitMainUSDCBalBefore = USDC.balanceOf(royaltyPolicyLAP.LIQUID_SPLIT_MAIN()); - - royaltyPolicyLAP.distributeIpPoolFunds(address(2), address(USDC), accounts, address(0)); - - uint256 splitClone2USDCBalAfter = USDC.balanceOf(splitClone2); - uint256 splitMainUSDCBalAfter = USDC.balanceOf(royaltyPolicyLAP.LIQUID_SPLIT_MAIN()); - - assertApproxEqRel(splitClone2USDCBalBefore - splitClone2USDCBalAfter, royaltyAmount, 0.0001e18); - assertApproxEqRel(splitMainUSDCBalAfter - splitMainUSDCBalBefore, royaltyAmount, 0.0001e18); - } - - function test_RoyaltyPolicyLAP_claimFromIpPool() public { - (, address splitClone2, address ancestorsVault2, , , ) = royaltyPolicyLAP.getRoyaltyData(address(2)); - - // send USDC to 0xSplitClone - uint256 royaltyAmount = 1000 * 10 ** 6; - USDC.mint(splitClone2, royaltyAmount); - - address[] memory accounts = new address[](2); - accounts[0] = address(2); - accounts[1] = ancestorsVault2; - - royaltyPolicyLAP.distributeIpPoolFunds(address(2), address(USDC), accounts, address(0)); - - uint256 expectedAmountToBeClaimed = ILiquidSplitMain(royaltyPolicyLAP.LIQUID_SPLIT_MAIN()).getERC20Balance( - address(2), - USDC - ); - - ERC20[] memory tokens = new ERC20[](1); - tokens[0] = USDC; - - uint256 splitMainUSDCBalBefore = USDC.balanceOf(royaltyPolicyLAP.LIQUID_SPLIT_MAIN()); - uint256 address2USDCBalBefore = USDC.balanceOf(address(2)); - - royaltyPolicyLAP.claimFromIpPool(address(2), tokens); - - uint256 splitMainUSDCBalAfter = USDC.balanceOf(royaltyPolicyLAP.LIQUID_SPLIT_MAIN()); - uint256 address2USDCBalAfter = USDC.balanceOf(address(2)); - - assertApproxEqRel(splitMainUSDCBalBefore - splitMainUSDCBalAfter, expectedAmountToBeClaimed, 0.0001e18); - assertApproxEqRel(address2USDCBalAfter - address2USDCBalBefore, expectedAmountToBeClaimed, 0.0001e18); - } - - function test_RoyaltyPolicyLAP_claimAsFullRnftOwner() public { - (, address splitClone7, , , , ) = royaltyPolicyLAP.getRoyaltyData(address(7)); - - uint256 royaltyAmountUSDC = 100 * 10 ** 6; - USDC.mint(address(splitClone7), royaltyAmountUSDC); - - vm.startPrank(address(7)); - ERC1155(address(splitClone7)).setApprovalForAll(address(royaltyPolicyLAP), true); - - uint256 usdcClaimerBalBefore = USDC.balanceOf(address(7)); - uint256 rnftClaimerBalBefore = ERC1155(address(splitClone7)).balanceOf(address(7), 0); - - royaltyPolicyLAP.claimFromIpPoolAsTotalRnftOwner(address(7), address(USDC)); - - uint256 usdcClaimerBalAfter = USDC.balanceOf(address(7)); - uint256 rnftClaimerBalAfter = ERC1155(address(splitClone7)).balanceOf(address(7), 0); - - assertApproxEqRel(usdcClaimerBalAfter - usdcClaimerBalBefore, royaltyAmountUSDC, 0.0001e18); - assertEq(rnftClaimerBalAfter, 1000); - assertEq(rnftClaimerBalBefore, 1000); - } } diff --git a/test/foundry/utils/BaseTest.t.sol b/test/foundry/utils/BaseTest.t.sol index fbd95f1e..d47d4fec 100644 --- a/test/foundry/utils/BaseTest.t.sol +++ b/test/foundry/utils/BaseTest.t.sol @@ -52,9 +52,6 @@ contract BaseTest is Test, DeployHelper, LicensingHelper { bob = u.bob; carl = u.carl; dan = u.dan; - - vm.label(LIQUID_SPLIT_FACTORY, "LIQUID_SPLIT_FACTORY"); - vm.label(LIQUID_SPLIT_MAIN, "LIQUID_SPLIT_MAIN"); } function postDeploymentSetup() public { @@ -197,10 +194,6 @@ contract BaseTest is Test, DeployHelper, LicensingHelper { function configureRoyaltyPolicyLAP() public { console2.log("BaseTest PostDeploymentSetup: Configure Royalty Policy LAP"); require(address(royaltyPolicyLAP) != address(0), "royaltyPolicyLAP not set"); - - vm.startPrank(u.admin); - royaltyPolicyLAP.setAncestorsVaultImplementation(address(ancestorsVaultImpl)); - vm.stopPrank(); } function _getIpId(MockERC721 mnft, uint256 tokenId) internal view returns (address ipId) { diff --git a/test/foundry/utils/DeployHelper.t.sol b/test/foundry/utils/DeployHelper.t.sol index 618e074b..d9ae105e 100644 --- a/test/foundry/utils/DeployHelper.t.sol +++ b/test/foundry/utils/DeployHelper.t.sol @@ -4,7 +4,9 @@ pragma solidity 0.8.23; // external import { console2 } from "forge-std/console2.sol"; // console to indicate mock deployment calls. +import { Test } from "forge-std/Test.sol"; import { ERC6551Registry } from "erc6551/ERC6551Registry.sol"; +import { UpgradeableBeacon } from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; // contracts import { AccessController } from "../../../contracts/access/AccessController.sol"; @@ -22,11 +24,11 @@ import { IPAssetRegistry } from "../../../contracts/registries/IPAssetRegistry.s import { ModuleRegistry } from "../../../contracts/registries/ModuleRegistry.sol"; import { LicenseRegistry } from "../../../contracts/registries/LicenseRegistry.sol"; import { RoyaltyModule } from "../../../contracts/modules/royalty/RoyaltyModule.sol"; -import { AncestorsVaultLAP } from "../../../contracts/modules/royalty/policies/AncestorsVaultLAP.sol"; import { RoyaltyPolicyLAP } from "../../../contracts/modules/royalty/policies/RoyaltyPolicyLAP.sol"; import { DisputeModule } from "../../../contracts/modules/dispute/DisputeModule.sol"; import { LicensingModule } from "../../../contracts/modules/licensing/LicensingModule.sol"; import { ArbitrationPolicySP } from "../../../contracts/modules/dispute/policies/ArbitrationPolicySP.sol"; +import { IpRoyaltyVault } from "../../../contracts/modules/royalty/policies/IpRoyaltyVault.sol"; // test import { MockAccessController } from "../mocks/access/MockAccessController.sol"; @@ -41,7 +43,7 @@ import { MockERC20 } from "../mocks/token/MockERC20.sol"; import { MockERC721 } from "../mocks/token/MockERC721.sol"; import { TestProxyHelper } from "./TestProxyHelper.sol"; -contract DeployHelper { +contract DeployHelper is Test { // TODO: three options, auto/mock/real in deploy condition, so that we don't need to manually // call getXXX to get mock contract (if there's no real contract deployed). @@ -111,13 +113,8 @@ contract DeployHelper { // Policy ArbitrationPolicySP internal arbitrationPolicySP; - AncestorsVaultLAP internal ancestorsVaultImpl; RoyaltyPolicyLAP internal royaltyPolicyLAP; - // Royalty Policy — 0xSplits Liquid Split (Sepolia) - address internal constant LIQUID_SPLIT_FACTORY = 0xF678Bae6091Ab6933425FE26Afc20Ee5F324c4aE; - address internal constant LIQUID_SPLIT_MAIN = 0x57CBFA83f000a38C5b5881743E298819c503A559; - // Arbitration Policy // TODO: custom arbitration price for testing uint256 internal constant ARBITRATION_PRICE = 1000; // not decimal exponentiated @@ -294,16 +291,19 @@ contract DeployHelper { console2.log("DeployHelper: Using Mock ArbitrationPolicySP"); } if (d.royaltyPolicyLAP) { - address impl = address( - new RoyaltyPolicyLAP(getRoyaltyModule(), getLicensingModule(), LIQUID_SPLIT_FACTORY, LIQUID_SPLIT_MAIN) - ); + address impl = address(new RoyaltyPolicyLAP(getRoyaltyModule(), getLicensingModule())); royaltyPolicyLAP = RoyaltyPolicyLAP( TestProxyHelper.deployUUPSProxy(impl, abi.encodeCall(RoyaltyPolicyLAP.initialize, (getGovernance()))) ); console2.log("DeployHelper: Using REAL RoyaltyPolicyLAP"); - ancestorsVaultImpl = new AncestorsVaultLAP(address(royaltyPolicyLAP)); - console2.log("DeployHelper: Using REAL AncestorsVaultLAP"); + // deploy ip royalty vault implementation and beacon + vm.startPrank(governanceAdmin); + address ipRoyaltyVaultImplementation = address(new IpRoyaltyVault(address(royaltyPolicyLAP))); + address ipRoyaltyVaultBeacon = address( + new UpgradeableBeacon(ipRoyaltyVaultImplementation, getGovernance()) + ); + royaltyPolicyLAP.setIpRoyaltyVaultBeacon(ipRoyaltyVaultBeacon); } else { // mockRoyaltyPolicyLAP = new MockRoyaltyPolicyLAP(getRoyaltyModule()); // console2.log("DeployHelper: Using Mock RoyaltyPolicyLAP"); diff --git a/yarn.lock b/yarn.lock index ef6af56f..2b0be6ef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -841,6 +841,11 @@ "@nomicfoundation/solidity-analyzer-win32-ia32-msvc" "0.1.1" "@nomicfoundation/solidity-analyzer-win32-x64-msvc" "0.1.1" +"@openzeppelin/contracts-upgradeable-v4@npm:@openzeppelin/contracts-upgradeable@4.9.6": + version "4.9.6" + resolved "https://registry.yarnpkg.com/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-4.9.6.tgz#38b21708a719da647de4bb0e4802ee235a0d24df" + integrity sha512-m4iHazOsOCv1DgM7eD7GupTJ+NFVujRZt1wzddDPSVGpWdKq1SKkla5htKG7+IS4d2XOCtzkUNwRZ7Vq5aEUMA== + "@openzeppelin/contracts-upgradeable@5.0.2": version "5.0.2" resolved "https://registry.yarnpkg.com/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-5.0.2.tgz#3e5321a2ecdd0b206064356798c21225b6ec7105"