From dda4afdce9de7aa8bf972da73d2f8e1d5b6b8a80 Mon Sep 17 00:00:00 2001 From: Sebastian Liu Date: Mon, 23 Sep 2024 11:19:40 -0700 Subject: [PATCH 1/3] feat(royalty): rm `currencyTokens` from para list --- .../workflows/IRoyaltyWorkflows.sol | 4 -- contracts/workflows/RoyaltyWorkflows.sol | 48 ++++++++++++++++--- 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/contracts/interfaces/workflows/IRoyaltyWorkflows.sol b/contracts/interfaces/workflows/IRoyaltyWorkflows.sol index 526db56..0aa5e0f 100644 --- a/contracts/interfaces/workflows/IRoyaltyWorkflows.sol +++ b/contracts/interfaces/workflows/IRoyaltyWorkflows.sol @@ -20,7 +20,6 @@ interface IRoyaltyWorkflows { /// and claims revenue on that snapshot for each specified currency token. /// @param ancestorIpId The address of the ancestor IP. /// @param claimer The address of the claimer of the revenue tokens (must be a royalty token holder). - /// @param currencyTokens The addresses of the currency (revenue) tokens to claim (each address must be unique). /// @param royaltyClaimDetails The details of the royalty claim from child IPs, /// see {IRoyaltyWorkflows-RoyaltyClaimDetails}. /// @return snapshotId The ID of the snapshot taken. @@ -28,7 +27,6 @@ interface IRoyaltyWorkflows { function transferToVaultAndSnapshotAndClaimByTokenBatch( address ancestorIpId, address claimer, - address[] calldata currencyTokens, RoyaltyClaimDetails[] calldata royaltyClaimDetails ) external returns (uint256 snapshotId, uint256[] memory amountsClaimed); @@ -36,7 +34,6 @@ interface IRoyaltyWorkflows { /// specified currency token both on the new snapshot and on each specified unclaimed snapshots. /// @param ancestorIpId The address of the ancestor IP. /// @param claimer The address of the claimer of the revenue tokens (must be a royalty token holder). - /// @param currencyTokens The addresses of the currency (revenue) tokens to claim (each address must be unique). /// @param unclaimedSnapshotIds The IDs of unclaimed snapshots to include in the claim. /// @param royaltyClaimDetails The details of the royalty claim from child IPs, /// see {IRoyaltyWorkflows-RoyaltyClaimDetails}. @@ -45,7 +42,6 @@ interface IRoyaltyWorkflows { function transferToVaultAndSnapshotAndClaimBySnapshotBatch( address ancestorIpId, address claimer, - address[] calldata currencyTokens, uint256[] calldata unclaimedSnapshotIds, RoyaltyClaimDetails[] calldata royaltyClaimDetails ) external returns (uint256 snapshotId, uint256[] memory amountsClaimed); diff --git a/contracts/workflows/RoyaltyWorkflows.sol b/contracts/workflows/RoyaltyWorkflows.sol index b3147a2..5b5afe0 100644 --- a/contracts/workflows/RoyaltyWorkflows.sol +++ b/contracts/workflows/RoyaltyWorkflows.sol @@ -45,7 +45,6 @@ contract RoyaltyWorkflows is IRoyaltyWorkflows, MulticallUpgradeable, AccessMana /// and claims revenue on that snapshot for each specified currency token. /// @param ancestorIpId The address of the ancestor IP. /// @param claimer The address of the claimer of the revenue tokens (must be a royalty token holder). - /// @param currencyTokens The addresses of the currency (revenue) tokens to claim (each address must be unique). /// @param royaltyClaimDetails The details of the royalty claim from child IPs, /// see {IRoyaltyWorkflows-RoyaltyClaimDetails}. /// @return snapshotId The ID of the snapshot taken. @@ -53,7 +52,6 @@ contract RoyaltyWorkflows is IRoyaltyWorkflows, MulticallUpgradeable, AccessMana function transferToVaultAndSnapshotAndClaimByTokenBatch( address ancestorIpId, address claimer, - address[] calldata currencyTokens, RoyaltyClaimDetails[] calldata royaltyClaimDetails ) external returns (uint256 snapshotId, uint256[] memory amountsClaimed) { // Transfers to ancestor's vault an amount of revenue tokens claimable via the given royalty policy @@ -75,7 +73,7 @@ contract RoyaltyWorkflows is IRoyaltyWorkflows, MulticallUpgradeable, AccessMana // Claims revenue for each specified currency token from the latest snapshot amountsClaimed = ancestorIpRoyaltyVault.claimRevenueOnBehalfByTokenBatch({ snapshotId: snapshotId, - tokenList: currencyTokens, + tokenList: _getCurrencyTokenList(royaltyClaimDetails), claimer: claimer }); } @@ -84,7 +82,6 @@ contract RoyaltyWorkflows is IRoyaltyWorkflows, MulticallUpgradeable, AccessMana /// specified currency token both on the new snapshot and on each specified unclaimed snapshots. /// @param ancestorIpId The address of the ancestor IP. /// @param claimer The address of the claimer of the revenue tokens (must be a royalty token holder). - /// @param currencyTokens The addresses of the currency (revenue) tokens to claim (each address must be unique). /// @param unclaimedSnapshotIds The IDs of unclaimed snapshots to include in the claim. /// @param royaltyClaimDetails The details of the royalty claim from child IPs, /// see {IRoyaltyWorkflows-RoyaltyClaimDetails}. @@ -93,7 +90,6 @@ contract RoyaltyWorkflows is IRoyaltyWorkflows, MulticallUpgradeable, AccessMana function transferToVaultAndSnapshotAndClaimBySnapshotBatch( address ancestorIpId, address claimer, - address[] calldata currencyTokens, uint256[] calldata unclaimedSnapshotIds, RoyaltyClaimDetails[] calldata royaltyClaimDetails ) external returns (uint256 snapshotId, uint256[] memory amountsClaimed) { @@ -113,6 +109,8 @@ contract RoyaltyWorkflows is IRoyaltyWorkflows, MulticallUpgradeable, AccessMana // Takes a snapshot of the ancestor IP's royalty vault snapshotId = ancestorIpRoyaltyVault.snapshot(); + address[] memory currencyTokens = _getCurrencyTokenList(royaltyClaimDetails); + // Claims revenue for each specified currency token from the latest snapshot amountsClaimed = ancestorIpRoyaltyVault.claimRevenueOnBehalfByTokenBatch({ snapshotId: snapshotId, @@ -131,7 +129,7 @@ contract RoyaltyWorkflows is IRoyaltyWorkflows, MulticallUpgradeable, AccessMana returns (uint256 claimedAmount) { amountsClaimed[i] += claimedAmount; } catch { - amountsClaimed[i] += 0; + // Continue to the next currency token } } } @@ -197,11 +195,47 @@ contract RoyaltyWorkflows is IRoyaltyWorkflows, MulticallUpgradeable, AccessMana amountsClaimed[i] += claimedAmount; } catch { // Continue to the next currency token - amountsClaimed[i] += 0; } } } + /// @dev Extracts all unique currency token addresses from an array of RoyaltyClaimDetails. + /// @param royaltyClaimDetails The details of the royalty claim from child IPs, + /// see {IRoyaltyWorkflows-RoyaltyClaimDetails}. + /// @return currencyTokenList An array of unique currency token addresses extracted from `royaltyClaimDetails`. + function _getCurrencyTokenList( + RoyaltyClaimDetails[] calldata royaltyClaimDetails + ) private pure returns (address[] memory currencyTokenList) { + uint256 length = royaltyClaimDetails.length; + address[] memory tempUniqueTokenList = new address[](length); + uint256 uniqueCount = 0; + + for (uint256 i = 0; i < length; i++) { + address currencyToken = royaltyClaimDetails[i].currencyToken; + bool isDuplicate = false; + + // Check if `currencyToken` already in `tempUniqueTokenList` + for (uint256 j = 0; j < uniqueCount; j++) { + if (tempUniqueTokenList[j] == currencyToken) { + // set the `isDuplicate` flag if `currencyToken` already in `tempUniqueTokenList` + isDuplicate = true; + break; + } + } + + // Add `currencyToken` to `tempUniqueTokenList` if it's not already in `tempUniqueTokenList` + if (!isDuplicate) { + tempUniqueTokenList[uniqueCount] = currencyToken; + uniqueCount++; + } + } + + currencyTokenList = new address[](uniqueCount); + for (uint256 i = 0; i < uniqueCount; i++) { + currencyTokenList[i] = tempUniqueTokenList[i]; + } + } + // // Upgrade // From 322a5603304589ccea7eb20c169b6c501a32ab88 Mon Sep 17 00:00:00 2001 From: Sebastian Liu Date: Mon, 23 Sep 2024 11:22:59 -0700 Subject: [PATCH 2/3] test(royalty): update tests & add more asserts --- test/workflows/RoyaltyWorkflows.t.sol | 44 +++++++++++++++------------ 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/test/workflows/RoyaltyWorkflows.t.sol b/test/workflows/RoyaltyWorkflows.t.sol index fced407..34389c7 100644 --- a/test/workflows/RoyaltyWorkflows.t.sol +++ b/test/workflows/RoyaltyWorkflows.t.sol @@ -46,7 +46,8 @@ contract RoyaltyWorkflowsTest is BaseTest { function test_RoyaltyWorkflows_transferToVaultAndSnapshotAndClaimByTokenBatch() public { // setup IP graph with no snapshot - _setupIpGraph(0); + uint256 numSnapshots = 0; + _setupIpGraph(numSnapshots); IRoyaltyWorkflows.RoyaltyClaimDetails[] memory claimDetails = new IRoyaltyWorkflows.RoyaltyClaimDetails[](4); claimDetails[0] = IRoyaltyWorkflows.RoyaltyClaimDetails({ @@ -79,10 +80,6 @@ contract RoyaltyWorkflowsTest is BaseTest { amount: (defaultMintingFeeC * defaultCommRevShareC) / royaltyModule.maxPercent() // 500 * 20% = 100 }); - address[] memory currencyTokens = new address[](2); - currencyTokens[0] = address(mockTokenA); - currencyTokens[1] = address(mockTokenC); - uint256 claimerBalanceABefore = mockTokenA.balanceOf(u.admin); uint256 claimerBalanceCBefore = mockTokenC.balanceOf(u.admin); @@ -90,13 +87,16 @@ contract RoyaltyWorkflowsTest is BaseTest { .transferToVaultAndSnapshotAndClaimByTokenBatch({ ancestorIpId: ancestorIpId, claimer: u.admin, - currencyTokens: currencyTokens, royaltyClaimDetails: claimDetails }); uint256 claimerBalanceAAfter = mockTokenA.balanceOf(u.admin); uint256 claimerBalanceCAfter = mockTokenC.balanceOf(u.admin); + assertEq(snapshotId, numSnapshots + 1); + assertEq(amountsClaimed.length, 2); // there are 2 currency tokens + assertEq(claimerBalanceAAfter - claimerBalanceABefore, amountsClaimed[0]); + assertEq(claimerBalanceCAfter - claimerBalanceCBefore, amountsClaimed[1]); assertEq( claimerBalanceAAfter - claimerBalanceABefore, defaultMintingFeeA + @@ -109,7 +109,6 @@ contract RoyaltyWorkflowsTest is BaseTest { royaltyModule.maxPercent() // 1000 * 10% * 10% = 10 royalty from grandChildIp // TODO: should be 20 but MockIPGraph currently only supports single-path calculation ); - assertEq( claimerBalanceCAfter - claimerBalanceCBefore, defaultMintingFeeC + (defaultMintingFeeC * defaultCommRevShareC) / royaltyModule.maxPercent() // 500 from from minting fee of childIpC // 500 * 20% = 100 royalty from childIpC @@ -118,7 +117,8 @@ contract RoyaltyWorkflowsTest is BaseTest { function test_RoyaltyWorkflows_transferToVaultAndSnapshotAndClaimBySnapshotBatch() public { // setup IP graph and takes 3 snapshots of ancestor IP's royalty vault - _setupIpGraph(3); + uint256 numSnapshots = 3; + _setupIpGraph(numSnapshots); IRoyaltyWorkflows.RoyaltyClaimDetails[] memory claimDetails = new IRoyaltyWorkflows.RoyaltyClaimDetails[](4); claimDetails[0] = IRoyaltyWorkflows.RoyaltyClaimDetails({ @@ -150,10 +150,6 @@ contract RoyaltyWorkflowsTest is BaseTest { amount: (defaultMintingFeeC * defaultCommRevShareC) / royaltyModule.maxPercent() // 500 * 20% = 100 }); - address[] memory currencyTokens = new address[](2); - currencyTokens[0] = address(mockTokenA); - currencyTokens[1] = address(mockTokenC); - uint256 claimerBalanceABefore = mockTokenA.balanceOf(u.admin); uint256 claimerBalanceCBefore = mockTokenC.balanceOf(u.admin); @@ -161,7 +157,6 @@ contract RoyaltyWorkflowsTest is BaseTest { .transferToVaultAndSnapshotAndClaimBySnapshotBatch({ ancestorIpId: ancestorIpId, claimer: u.admin, - currencyTokens: currencyTokens, unclaimedSnapshotIds: unclaimedSnapshotIds, royaltyClaimDetails: claimDetails }); @@ -169,6 +164,10 @@ contract RoyaltyWorkflowsTest is BaseTest { uint256 claimerBalanceAAfter = mockTokenA.balanceOf(u.admin); uint256 claimerBalanceCAfter = mockTokenC.balanceOf(u.admin); + assertEq(snapshotId, numSnapshots + 1); + assertEq(amountsClaimed.length, 2); // there are 2 currency tokens + assertEq(claimerBalanceAAfter - claimerBalanceABefore, amountsClaimed[0]); + assertEq(claimerBalanceCAfter - claimerBalanceCBefore, amountsClaimed[1]); assertEq( claimerBalanceAAfter - claimerBalanceABefore, defaultMintingFeeA + @@ -180,7 +179,6 @@ contract RoyaltyWorkflowsTest is BaseTest { (((defaultMintingFeeA * defaultCommRevShareA) / royaltyModule.maxPercent()) * defaultCommRevShareA) / royaltyModule.maxPercent() // 1000 * 10% * 10% = 10 royalty from grandChildIp ); - assertEq( claimerBalanceCAfter - claimerBalanceCBefore, defaultMintingFeeC + (defaultMintingFeeC * defaultCommRevShareC) / royaltyModule.maxPercent() // 500 from minting fee of childIpC // 500 * 20% = 100 royalty from childIpC @@ -189,7 +187,8 @@ contract RoyaltyWorkflowsTest is BaseTest { function test_RoyaltyWorkflows_snapshotAndClaimByTokenBatch() public { // setup IP graph with no snapshot - _setupIpGraph(0); + uint256 numSnapshots = 0; + _setupIpGraph(numSnapshots); address[] memory currencyTokens = new address[](2); currencyTokens[0] = address(mockTokenA); @@ -207,11 +206,14 @@ contract RoyaltyWorkflowsTest is BaseTest { uint256 claimerBalanceAAfter = mockTokenA.balanceOf(u.admin); uint256 claimerBalanceCAfter = mockTokenC.balanceOf(u.admin); + assertEq(snapshotId, numSnapshots + 1); + assertEq(amountsClaimed.length, 2); // there are 2 currency tokens + assertEq(claimerBalanceAAfter - claimerBalanceABefore, amountsClaimed[0]); + assertEq(claimerBalanceCAfter - claimerBalanceCBefore, amountsClaimed[1]); assertEq( claimerBalanceAAfter - claimerBalanceABefore, defaultMintingFeeA + defaultMintingFeeA // 1000 + 1000 from minting fee of childIpA and childIpB ); - assertEq( claimerBalanceCAfter - claimerBalanceCBefore, defaultMintingFeeC // 500 from from minting fee of childIpC @@ -220,7 +222,8 @@ contract RoyaltyWorkflowsTest is BaseTest { function test_RoyaltyWorkflows_snapshotAndClaimBySnapshotBatch() public { // setup IP graph and takes 1 snapshot of ancestor IP's royalty vault - _setupIpGraph(1); + uint256 numSnapshots = 1; + _setupIpGraph(numSnapshots); address[] memory currencyTokens = new address[](2); currencyTokens[0] = address(mockTokenA); @@ -239,11 +242,14 @@ contract RoyaltyWorkflowsTest is BaseTest { uint256 claimerBalanceAAfter = mockTokenA.balanceOf(u.admin); uint256 claimerBalanceCAfter = mockTokenC.balanceOf(u.admin); + assertEq(snapshotId, numSnapshots + 1); + assertEq(amountsClaimed.length, 2); // there are 2 currency tokens + assertEq(claimerBalanceAAfter - claimerBalanceABefore, amountsClaimed[0]); + assertEq(claimerBalanceCAfter - claimerBalanceCBefore, amountsClaimed[1]); assertEq( claimerBalanceAAfter - claimerBalanceABefore, defaultMintingFeeA + defaultMintingFeeA // 1000 + 1000 from minting fee of childIpA and childIpB ); - assertEq( claimerBalanceCAfter - claimerBalanceCBefore, defaultMintingFeeC // 500 from from minting fee of childIpC @@ -291,7 +297,7 @@ contract RoyaltyWorkflowsTest is BaseTest { /// - `childIpC`: It has licenseTermsC attached, has 1 parent `ancestorIp`, and has 1 grandchild `grandChildIp`. /// - `grandChildIp`: It has all 3 license terms attached. It has 3 parents and 1 grandparent IPs. /// @param numSnapshots The number of snapshots to take of the ancestor IP's royalty vault. - function _setupIpGraph(uint32 numSnapshots) private { + function _setupIpGraph(uint256 numSnapshots) private { uint256 ancestorTokenId = mockNft.mint(u.admin); uint256 childTokenIdA = mockNft.mint(u.alice); uint256 childTokenIdB = mockNft.mint(u.bob); From 45ec7727237791ad0e01e8f2d2e83d735fe3be80 Mon Sep 17 00:00:00 2001 From: Sebastian Liu Date: Mon, 23 Sep 2024 17:23:36 -0700 Subject: [PATCH 3/3] improve error handling in RoyaltyWorkflows - Catch and handle `IpRoyaltyVault__NoClaimableTokens` error. - Revert with original error for all other cases using assembly. - Existing tests already covers the non-revert case - Added tests to cover the revert case --- contracts/workflows/RoyaltyWorkflows.sol | 19 +++++-- test/workflows/RoyaltyWorkflows.t.sol | 67 ++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 4 deletions(-) diff --git a/contracts/workflows/RoyaltyWorkflows.sol b/contracts/workflows/RoyaltyWorkflows.sol index 5b5afe0..c4c7dec 100644 --- a/contracts/workflows/RoyaltyWorkflows.sol +++ b/contracts/workflows/RoyaltyWorkflows.sol @@ -7,6 +7,7 @@ import { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC16 import { MulticallUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/MulticallUpgradeable.sol"; import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import { Errors as CoreErrors } from "@storyprotocol/core/lib/Errors.sol"; // solhint-disable-next-line max-line-length import { IGraphAwareRoyaltyPolicy } from "@storyprotocol/core/interfaces/modules/royalty/policies/IGraphAwareRoyaltyPolicy.sol"; import { IIpRoyaltyVault } from "@storyprotocol/core/interfaces/modules/royalty/policies/IIpRoyaltyVault.sol"; @@ -128,8 +129,13 @@ contract RoyaltyWorkflows is IRoyaltyWorkflows, MulticallUpgradeable, AccessMana }) returns (uint256 claimedAmount) { amountsClaimed[i] += claimedAmount; - } catch { - // Continue to the next currency token + } catch (bytes memory reason) { + // If the error is not IpRoyaltyVault__NoClaimableTokens, revert with the original error + if (CoreErrors.IpRoyaltyVault__NoClaimableTokens.selector != bytes4(reason)) { + assembly { + revert(add(reason, 32), mload(reason)) + } + } } } } @@ -193,8 +199,13 @@ contract RoyaltyWorkflows is IRoyaltyWorkflows, MulticallUpgradeable, AccessMana }) returns (uint256 claimedAmount) { amountsClaimed[i] += claimedAmount; - } catch { - // Continue to the next currency token + } catch (bytes memory reason) { + // If the error is not IpRoyaltyVault__NoClaimableTokens, revert with the original error + if (CoreErrors.IpRoyaltyVault__NoClaimableTokens.selector != bytes4(reason)) { + assembly { + revert(add(reason, 32), mload(reason)) + } + } } } } diff --git a/test/workflows/RoyaltyWorkflows.t.sol b/test/workflows/RoyaltyWorkflows.t.sol index 34389c7..5e16d30 100644 --- a/test/workflows/RoyaltyWorkflows.t.sol +++ b/test/workflows/RoyaltyWorkflows.t.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.26; // external import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; +import { Errors as CoreErrors } from "@storyprotocol/core/lib/Errors.sol"; import { IIPAccount } from "@storyprotocol/core/interfaces/IIPAccount.sol"; import { IpRoyaltyVault } from "@storyprotocol/core/modules/royalty/policies/IpRoyaltyVault.sol"; import { PILFlavors } from "@storyprotocol/core/lib/PILFlavors.sol"; @@ -185,6 +186,52 @@ contract RoyaltyWorkflowsTest is BaseTest { ); } + function test_RoyaltyWorkflows_revert_transferToVaultAndSnapshotAndClaimBySnapshotBatch() public { + // setup IP graph and takes 3 snapshots of ancestor IP's royalty vault + uint256 numSnapshots = 3; + _setupIpGraph(numSnapshots); + + IRoyaltyWorkflows.RoyaltyClaimDetails[] memory claimDetails = new IRoyaltyWorkflows.RoyaltyClaimDetails[](4); + claimDetails[0] = IRoyaltyWorkflows.RoyaltyClaimDetails({ + childIpId: childIpIdA, + royaltyPolicy: address(royaltyPolicyLRP), + currencyToken: address(mockTokenA), + amount: (defaultMintingFeeA * defaultCommRevShareA) / royaltyModule.maxPercent() // 1000 * 10% = 100 + }); + + claimDetails[1] = IRoyaltyWorkflows.RoyaltyClaimDetails({ + childIpId: childIpIdB, + royaltyPolicy: address(royaltyPolicyLRP), + currencyToken: address(mockTokenA), + amount: (defaultMintingFeeA * defaultCommRevShareA) / royaltyModule.maxPercent() // 1000 * 10% = 100 + }); + + claimDetails[2] = IRoyaltyWorkflows.RoyaltyClaimDetails({ + childIpId: grandChildIpId, + royaltyPolicy: address(royaltyPolicyLRP), + currencyToken: address(mockTokenA), + amount: (((defaultMintingFeeA * defaultCommRevShareA) / royaltyModule.maxPercent()) * + defaultCommRevShareA) / royaltyModule.maxPercent() // 1000 * 10% * 10% = 10 + }); + + claimDetails[3] = IRoyaltyWorkflows.RoyaltyClaimDetails({ + childIpId: childIpIdC, + royaltyPolicy: address(royaltyPolicyLAP), + currencyToken: address(mockTokenC), + amount: (defaultMintingFeeC * defaultCommRevShareC) / royaltyModule.maxPercent() // 500 * 20% = 100 + }); + + address ancestorVault = royaltyModule.ipRoyaltyVaults(ancestorIpId); + + vm.expectRevert(CoreErrors.IpRoyaltyVault__VaultsMustClaimAsSelf.selector); + royaltyWorkflows.transferToVaultAndSnapshotAndClaimBySnapshotBatch({ + ancestorIpId: ancestorIpId, + claimer: ancestorVault, + unclaimedSnapshotIds: unclaimedSnapshotIds, + royaltyClaimDetails: claimDetails + }); + } + function test_RoyaltyWorkflows_snapshotAndClaimByTokenBatch() public { // setup IP graph with no snapshot uint256 numSnapshots = 0; @@ -256,6 +303,26 @@ contract RoyaltyWorkflowsTest is BaseTest { ); } + function test_RoyaltyWorkflows_revert_snapshotAndClaimBySnapshotBatch() public { + // setup IP graph and takes 1 snapshot of ancestor IP's royalty vault + uint256 numSnapshots = 1; + _setupIpGraph(numSnapshots); + + address[] memory currencyTokens = new address[](2); + currencyTokens[0] = address(mockTokenA); + currencyTokens[1] = address(mockTokenC); + + address ancestorVault = royaltyModule.ipRoyaltyVaults(ancestorIpId); + + vm.expectRevert(CoreErrors.IpRoyaltyVault__VaultsMustClaimAsSelf.selector); + royaltyWorkflows.snapshotAndClaimBySnapshotBatch({ + ipId: ancestorIpId, + claimer: ancestorVault, + unclaimedSnapshotIds: unclaimedSnapshotIds, + currencyTokens: currencyTokens + }); + } + function _setupCurrencyTokens() private { mockTokenA = new MockERC20("MockTokenA", "MTA"); mockTokenC = new MockERC20("MockTokenC", "MTC");