Skip to content

Commit 4225263

Browse files
authored
Add RoyaltyWorkflows to Release Branch (#76)
* chore: update protocol-core to v1.2.2 * feat(spg): add `RoyaltyWorkflows` * test: add tests for `RoyaltyWorkflows` * chore: update foundry_ci.yml to include release branch * feat(royalty): add enhancements to `RoyaltyWorkflows`
1 parent 08499bb commit 4225263

File tree

10 files changed

+1101
-13
lines changed

10 files changed

+1101
-13
lines changed

.github/workflows/foundry_ci.yml

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ on:
44
pull_request:
55
branches:
66
- main
7+
- release-v1.x.x
78

89
jobs:
910

contracts/RoyaltyWorkflows.sol

+257
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity 0.8.26;
3+
4+
// solhint-disable-next-line max-line-length
5+
import { AccessManagedUpgradeable } from "@openzeppelin/contracts-upgradeable/access/manager/AccessManagedUpgradeable.sol";
6+
import { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol";
7+
import { MulticallUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/MulticallUpgradeable.sol";
8+
import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
9+
10+
import { Errors as CoreErrors } from "@storyprotocol/core/lib/Errors.sol";
11+
// solhint-disable-next-line max-line-length
12+
import { IGraphAwareRoyaltyPolicy } from "@storyprotocol/core/interfaces/modules/royalty/policies/IGraphAwareRoyaltyPolicy.sol";
13+
import { IIpRoyaltyVault } from "@storyprotocol/core/interfaces/modules/royalty/policies/IIpRoyaltyVault.sol";
14+
import { IRoyaltyModule } from "@storyprotocol/core/interfaces/modules/royalty/IRoyaltyModule.sol";
15+
16+
import { Errors } from "./lib/Errors.sol";
17+
import { IRoyaltyWorkflows } from "./interfaces/IRoyaltyWorkflows.sol";
18+
19+
/// @title Royalty Workflows
20+
/// @notice Each workflow bundles multiple core protocol operations into a single function to enable one-click
21+
/// IP revenue claiming in the Story Proof-of-Creativity Protocol.
22+
contract RoyaltyWorkflows is IRoyaltyWorkflows, MulticallUpgradeable, AccessManagedUpgradeable, UUPSUpgradeable {
23+
using ERC165Checker for address;
24+
25+
/// @notice The address of the Royalty Module.
26+
IRoyaltyModule public immutable ROYALTY_MODULE;
27+
28+
/// @custom:oz-upgrades-unsafe-allow constructor
29+
constructor(address royaltyModule) {
30+
if (royaltyModule == address(0)) revert Errors.RoyaltyWorkflows__ZeroAddressParam();
31+
32+
ROYALTY_MODULE = IRoyaltyModule(royaltyModule);
33+
34+
_disableInitializers();
35+
}
36+
37+
/// @dev Initializes the contract.
38+
/// @param accessManager The address of the protocol access manager.
39+
function initialize(address accessManager) external initializer {
40+
if (accessManager == address(0)) revert Errors.RoyaltyWorkflows__ZeroAddressParam();
41+
__AccessManaged_init(accessManager);
42+
__UUPSUpgradeable_init();
43+
}
44+
45+
/// @notice Transfers royalties from royalty policy to the ancestor IP's royalty vault, takes a snapshot,
46+
/// and claims revenue on that snapshot for each specified currency token.
47+
/// @param ancestorIpId The address of the ancestor IP.
48+
/// @param claimer The address of the claimer of the revenue tokens (must be a royalty token holder).
49+
/// @param royaltyClaimDetails The details of the royalty claim from child IPs,
50+
/// see {IRoyaltyWorkflows-RoyaltyClaimDetails}.
51+
/// @return snapshotId The ID of the snapshot taken.
52+
/// @return amountsClaimed The amount of revenue claimed for each currency token.
53+
function transferToVaultAndSnapshotAndClaimByTokenBatch(
54+
address ancestorIpId,
55+
address claimer,
56+
RoyaltyClaimDetails[] calldata royaltyClaimDetails
57+
) external returns (uint256 snapshotId, uint256[] memory amountsClaimed) {
58+
// Transfers to ancestor's vault an amount of revenue tokens claimable via the given royalty policy
59+
for (uint256 i = 0; i < royaltyClaimDetails.length; i++) {
60+
IGraphAwareRoyaltyPolicy(royaltyClaimDetails[i].royaltyPolicy).transferToVault({
61+
ipId: royaltyClaimDetails[i].childIpId,
62+
ancestorIpId: ancestorIpId,
63+
token: royaltyClaimDetails[i].currencyToken,
64+
amount: royaltyClaimDetails[i].amount
65+
});
66+
}
67+
68+
// Gets the ancestor IP's royalty vault
69+
IIpRoyaltyVault ancestorIpRoyaltyVault = IIpRoyaltyVault(ROYALTY_MODULE.ipRoyaltyVaults(ancestorIpId));
70+
71+
// Takes a snapshot of the ancestor IP's royalty vault
72+
snapshotId = ancestorIpRoyaltyVault.snapshot();
73+
74+
// Claims revenue for each specified currency token from the latest snapshot
75+
amountsClaimed = ancestorIpRoyaltyVault.claimRevenueOnBehalfByTokenBatch({
76+
snapshotId: snapshotId,
77+
tokenList: _getCurrencyTokenList(royaltyClaimDetails),
78+
claimer: claimer
79+
});
80+
}
81+
82+
/// @notice Transfers royalties to the ancestor IP's royalty vault, takes a snapshot, claims revenue for each
83+
/// specified currency token both on the new snapshot and on each specified unclaimed snapshots.
84+
/// @param ancestorIpId The address of the ancestor IP.
85+
/// @param claimer The address of the claimer of the revenue tokens (must be a royalty token holder).
86+
/// @param unclaimedSnapshotIds The IDs of unclaimed snapshots to include in the claim.
87+
/// @param royaltyClaimDetails The details of the royalty claim from child IPs,
88+
/// see {IRoyaltyWorkflows-RoyaltyClaimDetails}.
89+
/// @return snapshotId The ID of the snapshot taken.
90+
/// @return amountsClaimed The amounts of revenue claimed for each currency token.
91+
function transferToVaultAndSnapshotAndClaimBySnapshotBatch(
92+
address ancestorIpId,
93+
address claimer,
94+
uint256[] calldata unclaimedSnapshotIds,
95+
RoyaltyClaimDetails[] calldata royaltyClaimDetails
96+
) external returns (uint256 snapshotId, uint256[] memory amountsClaimed) {
97+
// Transfers to ancestor's vault an amount of revenue tokens claimable via the given royalty policy
98+
for (uint256 i = 0; i < royaltyClaimDetails.length; i++) {
99+
IGraphAwareRoyaltyPolicy(royaltyClaimDetails[i].royaltyPolicy).transferToVault({
100+
ipId: royaltyClaimDetails[i].childIpId,
101+
ancestorIpId: ancestorIpId,
102+
token: royaltyClaimDetails[i].currencyToken,
103+
amount: royaltyClaimDetails[i].amount
104+
});
105+
}
106+
107+
// Gets the ancestor IP's royalty vault
108+
IIpRoyaltyVault ancestorIpRoyaltyVault = IIpRoyaltyVault(ROYALTY_MODULE.ipRoyaltyVaults(ancestorIpId));
109+
110+
// Takes a snapshot of the ancestor IP's royalty vault
111+
snapshotId = ancestorIpRoyaltyVault.snapshot();
112+
113+
address[] memory currencyTokens = _getCurrencyTokenList(royaltyClaimDetails);
114+
115+
// Claims revenue for each specified currency token from the latest snapshot
116+
amountsClaimed = ancestorIpRoyaltyVault.claimRevenueOnBehalfByTokenBatch({
117+
snapshotId: snapshotId,
118+
tokenList: currencyTokens,
119+
claimer: claimer
120+
});
121+
122+
// Claims revenue for each specified currency token from the unclaimed snapshots
123+
for (uint256 i = 0; i < currencyTokens.length; i++) {
124+
try
125+
ancestorIpRoyaltyVault.claimRevenueOnBehalfBySnapshotBatch({
126+
snapshotIds: unclaimedSnapshotIds,
127+
token: currencyTokens[i],
128+
claimer: claimer
129+
})
130+
returns (uint256 claimedAmount) {
131+
amountsClaimed[i] += claimedAmount;
132+
} catch (bytes memory reason) {
133+
// If the error is not IpRoyaltyVault__NoClaimableTokens, revert with the original error
134+
if (CoreErrors.IpRoyaltyVault__NoClaimableTokens.selector != bytes4(reason)) {
135+
assembly {
136+
revert(add(reason, 32), mload(reason))
137+
}
138+
}
139+
}
140+
}
141+
}
142+
143+
/// @notice Takes a snapshot of the IP's royalty vault and claims revenue on that snapshot for each
144+
/// specified currency token.
145+
/// @param ipId The address of the IP.
146+
/// @param claimer The address of the claimer of the revenue tokens (must be a royalty token holder).
147+
/// @param currencyTokens The addresses of the currency (revenue) tokens to claim.
148+
/// @return snapshotId The ID of the snapshot taken.
149+
/// @return amountsClaimed The amounts of revenue claimed for each currency token.
150+
function snapshotAndClaimByTokenBatch(
151+
address ipId,
152+
address claimer,
153+
address[] calldata currencyTokens
154+
) external returns (uint256 snapshotId, uint256[] memory amountsClaimed) {
155+
// Gets the IP's royalty vault
156+
IIpRoyaltyVault ipRoyaltyVault = IIpRoyaltyVault(ROYALTY_MODULE.ipRoyaltyVaults(ipId));
157+
158+
// Claims revenue for each specified currency token from the latest snapshot
159+
snapshotId = ipRoyaltyVault.snapshot();
160+
amountsClaimed = ipRoyaltyVault.claimRevenueOnBehalfByTokenBatch({
161+
snapshotId: snapshotId,
162+
tokenList: currencyTokens,
163+
claimer: claimer
164+
});
165+
}
166+
167+
/// @notice Takes a snapshot of the IP's royalty vault and claims revenue for each specified currency token
168+
/// both on the new snapshot and on each specified unclaimed snapshot.
169+
/// @param ipId The address of the IP.
170+
/// @param claimer The address of the claimer of the revenue tokens (must be a royalty token holder).
171+
/// @param unclaimedSnapshotIds The IDs of unclaimed snapshots to include in the claim.
172+
/// @param currencyTokens The addresses of the currency (revenue) tokens to claim.
173+
/// @return snapshotId The ID of the snapshot taken.
174+
/// @return amountsClaimed The amounts of revenue claimed for each currency token.
175+
function snapshotAndClaimBySnapshotBatch(
176+
address ipId,
177+
address claimer,
178+
uint256[] calldata unclaimedSnapshotIds,
179+
address[] calldata currencyTokens
180+
) external returns (uint256 snapshotId, uint256[] memory amountsClaimed) {
181+
// Gets the IP's royalty vault
182+
IIpRoyaltyVault ipRoyaltyVault = IIpRoyaltyVault(ROYALTY_MODULE.ipRoyaltyVaults(ipId));
183+
184+
// Claims revenue for each specified currency token from the latest snapshot
185+
snapshotId = ipRoyaltyVault.snapshot();
186+
amountsClaimed = ipRoyaltyVault.claimRevenueOnBehalfByTokenBatch({
187+
snapshotId: snapshotId,
188+
tokenList: currencyTokens,
189+
claimer: claimer
190+
});
191+
192+
// Claims revenue for each specified currency token from the unclaimed snapshots
193+
for (uint256 i = 0; i < currencyTokens.length; i++) {
194+
try
195+
ipRoyaltyVault.claimRevenueOnBehalfBySnapshotBatch({
196+
snapshotIds: unclaimedSnapshotIds,
197+
token: currencyTokens[i],
198+
claimer: claimer
199+
})
200+
returns (uint256 claimedAmount) {
201+
amountsClaimed[i] += claimedAmount;
202+
} catch (bytes memory reason) {
203+
// If the error is not IpRoyaltyVault__NoClaimableTokens, revert with the original error
204+
if (CoreErrors.IpRoyaltyVault__NoClaimableTokens.selector != bytes4(reason)) {
205+
assembly {
206+
revert(add(reason, 32), mload(reason))
207+
}
208+
}
209+
}
210+
}
211+
}
212+
213+
/// @dev Extracts all unique currency token addresses from an array of RoyaltyClaimDetails.
214+
/// @param royaltyClaimDetails The details of the royalty claim from child IPs,
215+
/// see {IRoyaltyWorkflows-RoyaltyClaimDetails}.
216+
/// @return currencyTokenList An array of unique currency token addresses extracted from `royaltyClaimDetails`.
217+
function _getCurrencyTokenList(
218+
RoyaltyClaimDetails[] calldata royaltyClaimDetails
219+
) private pure returns (address[] memory currencyTokenList) {
220+
uint256 length = royaltyClaimDetails.length;
221+
address[] memory tempUniqueTokenList = new address[](length);
222+
uint256 uniqueCount = 0;
223+
224+
for (uint256 i = 0; i < length; i++) {
225+
address currencyToken = royaltyClaimDetails[i].currencyToken;
226+
bool isDuplicate = false;
227+
228+
// Check if `currencyToken` already in `tempUniqueTokenList`
229+
for (uint256 j = 0; j < uniqueCount; j++) {
230+
if (tempUniqueTokenList[j] == currencyToken) {
231+
// set the `isDuplicate` flag if `currencyToken` already in `tempUniqueTokenList`
232+
isDuplicate = true;
233+
break;
234+
}
235+
}
236+
237+
// Add `currencyToken` to `tempUniqueTokenList` if it's not already in `tempUniqueTokenList`
238+
if (!isDuplicate) {
239+
tempUniqueTokenList[uniqueCount] = currencyToken;
240+
uniqueCount++;
241+
}
242+
}
243+
244+
currencyTokenList = new address[](uniqueCount);
245+
for (uint256 i = 0; i < uniqueCount; i++) {
246+
currencyTokenList[i] = tempUniqueTokenList[i];
247+
}
248+
}
249+
250+
//
251+
// Upgrade
252+
//
253+
254+
/// @dev Hook to authorize the upgrade according to UUPSUpgradeable
255+
/// @param newImplementation The address of the new implementation
256+
function _authorizeUpgrade(address newImplementation) internal override restricted {}
257+
}
+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity 0.8.26;
3+
4+
/// @title Royalty Workflows Interface
5+
/// @notice Interface for IP royalty workflows.
6+
interface IRoyaltyWorkflows {
7+
/// @notice Details for claiming royalties from a child IP.
8+
/// @param childIpId The address of the child IP.
9+
/// @param royaltyPolicy The address of the royalty policy.
10+
/// @param currencyToken The address of the currency (revenue) token to claim.
11+
/// @param amount The amount of currency (revenue) token to claim.
12+
struct RoyaltyClaimDetails {
13+
address childIpId;
14+
address royaltyPolicy;
15+
address currencyToken;
16+
uint256 amount;
17+
}
18+
19+
/// @notice Transfers royalties from royalty policy to the ancestor IP's royalty vault, takes a snapshot,
20+
/// and claims revenue on that snapshot for each specified currency token.
21+
/// @param ancestorIpId The address of the ancestor IP.
22+
/// @param claimer The address of the claimer of the revenue tokens (must be a royalty token holder).
23+
/// @param royaltyClaimDetails The details of the royalty claim from child IPs,
24+
/// see {IRoyaltyWorkflows-RoyaltyClaimDetails}.
25+
/// @return snapshotId The ID of the snapshot taken.
26+
/// @return amountsClaimed The amount of revenue claimed for each currency token.
27+
function transferToVaultAndSnapshotAndClaimByTokenBatch(
28+
address ancestorIpId,
29+
address claimer,
30+
RoyaltyClaimDetails[] calldata royaltyClaimDetails
31+
) external returns (uint256 snapshotId, uint256[] memory amountsClaimed);
32+
33+
/// @notice Transfers royalties to the ancestor IP's royalty vault, takes a snapshot, claims revenue for each
34+
/// specified currency token both on the new snapshot and on each specified unclaimed snapshots.
35+
/// @param ancestorIpId The address of the ancestor IP.
36+
/// @param claimer The address of the claimer of the revenue tokens (must be a royalty token holder).
37+
/// @param unclaimedSnapshotIds The IDs of unclaimed snapshots to include in the claim.
38+
/// @param royaltyClaimDetails The details of the royalty claim from child IPs,
39+
/// see {IRoyaltyWorkflows-RoyaltyClaimDetails}.
40+
/// @return snapshotId The ID of the snapshot taken.
41+
/// @return amountsClaimed The amounts of revenue claimed for each currency token.
42+
function transferToVaultAndSnapshotAndClaimBySnapshotBatch(
43+
address ancestorIpId,
44+
address claimer,
45+
uint256[] calldata unclaimedSnapshotIds,
46+
RoyaltyClaimDetails[] calldata royaltyClaimDetails
47+
) external returns (uint256 snapshotId, uint256[] memory amountsClaimed);
48+
49+
/// @notice Takes a snapshot of the IP's royalty vault and claims revenue on that snapshot for each
50+
/// specified currency token.
51+
/// @param ipId The address of the IP.
52+
/// @param claimer The address of the claimer of the revenue tokens (must be a royalty token holder).
53+
/// @param currencyTokens The addresses of the currency (revenue) tokens to claim.
54+
/// @return snapshotId The ID of the snapshot taken.
55+
/// @return amountsClaimed The amounts of revenue claimed for each currency token.
56+
function snapshotAndClaimByTokenBatch(
57+
address ipId,
58+
address claimer,
59+
address[] calldata currencyTokens
60+
) external returns (uint256 snapshotId, uint256[] memory amountsClaimed);
61+
62+
/// @notice Takes a snapshot of the IP's royalty vault and claims revenue for each specified currency token
63+
/// both on the new snapshot and on each specified unclaimed snapshot.
64+
/// @param ipId The address of the IP.
65+
/// @param claimer The address of the claimer of the revenue tokens (must be a royalty token holder).
66+
/// @param unclaimedSnapshotIds The IDs of unclaimed snapshots to include in the claim.
67+
/// @param currencyTokens The addresses of the currency (revenue) tokens to claim.
68+
/// @return snapshotId The ID of the snapshot taken.
69+
/// @return amountsClaimed The amounts of revenue claimed for each currency token.
70+
function snapshotAndClaimBySnapshotBatch(
71+
address ipId,
72+
address claimer,
73+
uint256[] calldata unclaimedSnapshotIds,
74+
address[] calldata currencyTokens
75+
) external returns (uint256 snapshotId, uint256[] memory amountsClaimed);
76+
}

contracts/lib/Errors.sol

+3
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,7 @@ library Errors {
3939

4040
/// @notice Zero address provided as a param to the GroupingWorkflows.
4141
error GroupingWorkflows__ZeroAddressParam();
42+
43+
/// @notice Zero address provided as a param to the RoyaltyWorkflows.
44+
error RoyaltyWorkflows__ZeroAddressParam();
4245
}

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@story-protocol/protocol-periphery",
3-
"version": "v1.2.1",
3+
"version": "v1.2.2",
44
"description": "Story Proof-of-Creativity protocol periphery smart contracts",
55
"main": "",
66
"directories": {
@@ -40,7 +40,7 @@
4040
"@openzeppelin/contracts": "5.0.1",
4141
"@openzeppelin/contracts-upgradeable": "5.0.1",
4242
"@story-protocol/create3-deployer": "github:storyprotocol/create3-deployer#main",
43-
"@story-protocol/protocol-core": "github:storyprotocol/protocol-core-v1#v1.2.1",
43+
"@story-protocol/protocol-core": "github:storyprotocol/protocol-core-v1#v1.2.2",
4444
"erc6551": "^0.3.1",
4545
"solady": "^0.0.192"
4646
}

0 commit comments

Comments
 (0)