Skip to content

Commit c132b73

Browse files
feat: add currency whitelist validation (assertCurrencyAllowed)
1 parent a78af6b commit c132b73

File tree

11 files changed

+186
-94
lines changed

11 files changed

+186
-94
lines changed

packages/core-sdk/src/resources/dispute.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,12 @@ export class DisputeClient {
6464
throw new Error(`Liveness must be between ${minLiveness} and ${maxLiveness}.`);
6565
}
6666
const [minimumBond, maximumBond] = await Promise.all([
67-
getMinimumBond(this.rpcClient, this.arbitrationPolicyUmaClient, WIP_TOKEN_ADDRESS),
67+
getMinimumBond(
68+
this.rpcClient,
69+
this.arbitrationPolicyUmaClient,
70+
WIP_TOKEN_ADDRESS,
71+
this.chainId,
72+
),
6873
this.arbitrationPolicyUmaClient.maxBonds({
6974
token: WIP_TOKEN_ADDRESS,
7075
}),

packages/core-sdk/src/resources/group.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,12 @@ import { getIpMetadataForWorkflow } from "../utils/getIpMetadataForWorkflow";
5959
import { getRevenueShare } from "../utils/royalty";
6060
import { getDeadline, getPermissionSignature } from "../utils/sign";
6161
import { waitForTxReceipt } from "../utils/txOptions";
62-
import { validateAddress, validateAddresses } from "../utils/utils";
62+
import {
63+
assertCurrenciesAllowed,
64+
assertCurrencyAllowed,
65+
validateAddress,
66+
validateAddresses,
67+
} from "../utils/utils";
6368
import { validateLicenseConfig } from "../utils/validateLicenseConfig";
6469

6570
export class GroupClient {
@@ -430,6 +435,7 @@ export class GroupClient {
430435
if (currencyTokens.some((token) => token === zeroAddress)) {
431436
throw new Error("Currency token cannot be the zero address.");
432437
}
438+
assertCurrenciesAllowed(currencyTokens, this.chainId);
433439
const collectAndClaimParams = {
434440
groupIpId: validateAddress(groupIpId),
435441
currencyTokens: validateAddresses(currencyTokens),
@@ -527,6 +533,7 @@ export class GroupClient {
527533
memberIpIds,
528534
}: GetClaimableRewardRequest): Promise<bigint[]> {
529535
try {
536+
assertCurrencyAllowed(currencyToken, this.chainId);
530537
const claimableReward = await this.groupingModuleClient.getClaimableReward({
531538
groupId: validateAddress(groupIpId),
532539
ipIds: validateAddresses(memberIpIds),
@@ -576,6 +583,7 @@ export class GroupClient {
576583
txOptions,
577584
}: ClaimRewardRequest): Promise<ClaimRewardResponse> {
578585
try {
586+
assertCurrencyAllowed(currencyToken, this.chainId);
579587
const claimRewardParam: GroupingModuleClaimRewardRequest = {
580588
groupId: validateAddress(groupIpId),
581589
ipIds: validateAddresses(memberIpIds),
@@ -608,6 +616,7 @@ export class GroupClient {
608616
txOptions,
609617
}: CollectRoyaltiesRequest): Promise<CollectRoyaltiesResponse> {
610618
try {
619+
assertCurrencyAllowed(currencyToken, this.chainId);
611620
const collectRoyaltiesParam: GroupingModuleCollectRoyaltiesRequest = {
612621
groupId: validateAddress(groupIpId),
613622
token: validateAddress(currencyToken),

packages/core-sdk/src/utils/oov3.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ import { privateKeyToAccount } from "viem/accounts";
33

44
import { aeneid } from "./chain";
55
import { handleError } from "./errors";
6-
import { chainStringToViemChain } from "./utils";
6+
import { assertCurrencyAllowed, chainStringToViemChain } from "./utils";
77
import { ArbitrationPolicyUmaClient } from "../abi/generated";
88
import { ASSERTION_ABI } from "../abi/oov3Abi";
9+
import { SupportedChainIds } from "../types/config";
910
import { DisputeId } from "../types/resources/dispute";
1011

1112
export const getOov3Contract = async (
@@ -34,7 +35,9 @@ export const getMinimumBond = async (
3435
rpcClient: PublicClient,
3536
arbitrationPolicyUmaClient: ArbitrationPolicyUmaClient,
3637
currency: Address,
38+
chainId: SupportedChainIds,
3739
): Promise<bigint> => {
40+
assertCurrencyAllowed(currency, chainId);
3841
const oov3Contract = await getOov3Contract(arbitrationPolicyUmaClient);
3942
return await rpcClient.readContract({
4043
address: oov3Contract,

packages/core-sdk/src/utils/pilFlavor.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { zeroAddress } from "viem";
22

33
import { PILFlavorError } from "./errors";
44
import { royaltyPolicyInputToAddress } from "./royalty";
5+
import { assertCurrencyAllowed } from "./utils";
56
import { SupportedChainIds } from "../types/config";
67
import { LicenseTerms, LicenseTermsInput } from "../types/resources/license";
78
import {
@@ -189,16 +190,20 @@ export class PILFlavor {
189190
params: LicenseTermsInput,
190191
chainId?: SupportedChainIds,
191192
): LicenseTerms => {
193+
const resolvedChainId: SupportedChainIds = chainId ?? "aeneid";
192194
const normalized: LicenseTerms = {
193195
...params,
194196
defaultMintingFee: BigInt(params.defaultMintingFee),
195197
expiration: BigInt(params.expiration),
196198
commercialRevCeiling: BigInt(params.commercialRevCeiling),
197199
derivativeRevCeiling: BigInt(params.derivativeRevCeiling),
198-
royaltyPolicy: royaltyPolicyInputToAddress(params.royaltyPolicy, chainId),
200+
royaltyPolicy: royaltyPolicyInputToAddress(params.royaltyPolicy, resolvedChainId),
199201
};
200202
const { royaltyPolicy, currency } = normalized;
201203

204+
// Validate currency whitelist for the resolved chain.
205+
assertCurrencyAllowed(currency, resolvedChainId);
206+
202207
// Validate royalty policy and currency relationship
203208
if (royaltyPolicy !== zeroAddress && currency === zeroAddress) {
204209
throw new PILFlavorError("Royalty policy requires currency token.");

packages/core-sdk/src/utils/utils.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,61 @@ import {
1010
Hex,
1111
isAddress,
1212
PublicClient,
13+
zeroAddress,
1314
} from "viem";
1415

16+
import { erc20Address, wrappedIpAddress } from "../abi/generated";
1517
import { aeneid, mainnet } from "./chain";
1618
import { ChainIds, SupportedChainIds } from "../types/config";
1719

20+
/** Allowed currency token addresses per chain (whitelist for licensing, group, OOV3, dispute, etc.). */
21+
const allowedCurrenciesByChain: Record<SupportedChainIds, readonly Address[]> = {
22+
aeneid: [erc20Address[1315] as Address, wrappedIpAddress[1315] as Address],
23+
1315: [erc20Address[1315] as Address, wrappedIpAddress[1315] as Address],
24+
mainnet: [wrappedIpAddress[1514] as Address],
25+
1514: [wrappedIpAddress[1514] as Address],
26+
};
27+
28+
export const getAllowedCurrencies = (chainId: SupportedChainIds): readonly Address[] => {
29+
return allowedCurrenciesByChain[chainId];
30+
};
31+
32+
/**
33+
* Validate that a currency token is allowed on a given chain.
34+
*
35+
* - If `currency` is the zero address, it's treated as "no currency" and allowed.
36+
* - Otherwise, it must exist in the allowed currency whitelist for the chain.
37+
*
38+
* Throws an Error with a consistent message used across the SDK.
39+
*/
40+
export const assertCurrencyAllowed = (currency: Address, chainId: SupportedChainIds): void => {
41+
if (currency === zeroAddress) {
42+
return;
43+
}
44+
45+
if (!getAllowedCurrencies(chainId).includes(currency)) {
46+
throw new Error(`Currency token ${currency} is not allowed on chain ${String(chainId)}.`);
47+
}
48+
};
49+
50+
/**
51+
* Validate that all currency tokens are allowed on a given chain.
52+
* Throws an Error with the same message shape previously used by callers that
53+
* validated arrays (e.g. group royalties distribution).
54+
*/
55+
export const assertCurrenciesAllowed = (
56+
currencies: readonly Address[],
57+
chainId: SupportedChainIds,
58+
): void => {
59+
if (currencies.length === 0) {
60+
return;
61+
}
62+
63+
if (currencies.some((c) => c !== zeroAddress && !getAllowedCurrencies(chainId).includes(c))) {
64+
throw new Error(`Currency token ${currencies.toString()} is not allowed on chain ${String(chainId)}.`);
65+
}
66+
};
67+
1868
export const waitTxAndFilterLog = async <
1969
const TAbi extends Abi | readonly unknown[],
2070
TEventName extends ContractEventName<TAbi> | undefined = ContractEventName<TAbi>,

packages/core-sdk/test/integration/dispute.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ describe("Dispute Functions", () => {
5050
publicClient,
5151
new ArbitrationPolicyUmaClient(publicClient, walletClient),
5252
WIP_TOKEN_ADDRESS,
53+
aeneid,
5354
);
5455

5556
const txData = await clientA.nftClient.createNFTCollection({

packages/core-sdk/test/unit/resources/group.test.ts

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ import { stub } from "sinon";
44
import { Address, PublicClient, WalletClient, zeroAddress, zeroHash } from "viem";
55

66
import { GroupClient } from "../../../src";
7-
import { IpAccountImplClient } from "../../../src/abi/generated";
7+
import { erc20Address, IpAccountImplClient } from "../../../src/abi/generated";
88
import { LicenseDataInput } from "../../../src/types/resources/group";
9-
import { mockAddress, txHash, walletAddress } from "../mockData";
9+
import { aeneid, mockAddress, txHash, walletAddress } from "../mockData";
10+
11+
const allowedCurrency = erc20Address[aeneid];
1012
import { createMockPublicClient, createMockWalletClient } from "../testUtils";
1113

1214
use(chaiAsPromised);
@@ -529,7 +531,7 @@ describe("Test IpAssetClient", () => {
529531

530532
const result = groupClient.collectAndDistributeGroupRoyalties({
531533
groupIpId: mockAddress,
532-
currencyTokens: [mockAddress],
534+
currencyTokens: [allowedCurrency],
533535
memberIpIds: [mockAddress],
534536
});
535537
await expect(result).to.be.rejectedWith(
@@ -545,7 +547,7 @@ describe("Test IpAssetClient", () => {
545547
.resolves(false);
546548
const result = groupClient.collectAndDistributeGroupRoyalties({
547549
groupIpId: mockAddress,
548-
currencyTokens: [mockAddress],
550+
currencyTokens: [allowedCurrency],
549551
memberIpIds: [mockAddress],
550552
});
551553
await expect(result).to.be.rejectedWith(
@@ -557,7 +559,7 @@ describe("Test IpAssetClient", () => {
557559
stub(groupClient.ipAssetRegistryClient, "isRegistered").resolves(true);
558560
const result = groupClient.collectAndDistributeGroupRoyalties({
559561
groupIpId: mockAddress,
560-
currencyTokens: [mockAddress],
562+
currencyTokens: [allowedCurrency],
561563
memberIpIds: [],
562564
});
563565
await expect(result).to.be.rejectedWith(
@@ -616,7 +618,7 @@ describe("Test IpAssetClient", () => {
616618

617619
const result = await groupClient.collectAndDistributeGroupRoyalties({
618620
groupIpId: mockAddress,
619-
currencyTokens: [mockAddress],
621+
currencyTokens: [allowedCurrency],
620622
memberIpIds: [mockAddress],
621623
});
622624
expect(result.txHash).equal(txHash);
@@ -648,7 +650,7 @@ describe("Test IpAssetClient", () => {
648650
stub(groupClient.groupingWorkflowsClient, "collectRoyaltiesAndClaimReward").resolves(txHash);
649651
const result = await groupClient.collectAndDistributeGroupRoyalties({
650652
groupIpId: mockAddress,
651-
currencyTokens: [mockAddress],
653+
currencyTokens: [allowedCurrency],
652654
memberIpIds: [mockAddress],
653655
});
654656
expect(result.txHash).equal(txHash);
@@ -725,7 +727,7 @@ describe("Test IpAssetClient", () => {
725727
stub(groupClient.groupingModuleClient, "getClaimableReward").rejects(new Error("rpc error"));
726728
const result = groupClient.getClaimableReward({
727729
groupIpId: mockAddress,
728-
currencyToken: mockAddress,
730+
currencyToken: allowedCurrency,
729731
memberIpIds: [mockAddress],
730732
});
731733
await expect(result).to.be.rejectedWith("Failed to get claimable reward: rpc error");
@@ -735,7 +737,7 @@ describe("Test IpAssetClient", () => {
735737
stub(groupClient.groupingModuleClient, "getClaimableReward").resolves([10n]);
736738
const result = await groupClient.getClaimableReward({
737739
groupIpId: mockAddress,
738-
currencyToken: mockAddress,
740+
currencyToken: allowedCurrency,
739741
memberIpIds: [mockAddress],
740742
});
741743
expect(result).to.deep.equal([10n]);
@@ -767,7 +769,7 @@ describe("Test IpAssetClient", () => {
767769
stub(groupClient.groupingModuleClient, "claimReward").rejects(new Error("rpc error"));
768770
const result = groupClient.claimReward({
769771
groupIpId: mockAddress,
770-
currencyToken: mockAddress,
772+
currencyToken: allowedCurrency,
771773
memberIpIds: [mockAddress],
772774
});
773775
await expect(result).to.be.rejectedWith("Failed to claim reward: rpc error");
@@ -785,7 +787,7 @@ describe("Test IpAssetClient", () => {
785787
]);
786788
const result = await groupClient.claimReward({
787789
groupIpId: mockAddress,
788-
currencyToken: mockAddress,
790+
currencyToken: allowedCurrency,
789791
memberIpIds: [mockAddress],
790792
});
791793
expect(result.txHash).equal(txHash);
@@ -803,7 +805,7 @@ describe("Test IpAssetClient", () => {
803805
]);
804806
const result = await groupClient.claimReward({
805807
groupIpId: mockAddress,
806-
currencyToken: mockAddress,
808+
currencyToken: allowedCurrency,
807809
memberIpIds: [mockAddress],
808810
});
809811
expect(result.txHash).equal(txHash);
@@ -824,7 +826,7 @@ describe("Test IpAssetClient", () => {
824826

825827
const result = groupClient.collectRoyalties({
826828
groupIpId: mockAddress,
827-
currencyToken: mockAddress,
829+
currencyToken: allowedCurrency,
828830
});
829831
await expect(result).to.be.rejectedWith("Failed to collect royalties: rpc error");
830832
});
@@ -844,7 +846,7 @@ describe("Test IpAssetClient", () => {
844846
]);
845847
const result = await groupClient.collectRoyalties({
846848
groupIpId: mockAddress,
847-
currencyToken: mockAddress,
849+
currencyToken: allowedCurrency,
848850
});
849851
expect(result.txHash).equal(txHash);
850852
expect(result.collectedRoyalties).to.equal(100n);
@@ -866,7 +868,7 @@ describe("Test IpAssetClient", () => {
866868

867869
const result = await groupClient.collectRoyalties({
868870
groupIpId: mockAddress,
869-
currencyToken: mockAddress,
871+
currencyToken: allowedCurrency,
870872
});
871873
expect(result.txHash).equal(txHash);
872874
expect(result.collectedRoyalties).to.equal(100n);

packages/core-sdk/test/unit/resources/ipAsset.test.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ const licenseTerms: LicenseTerms = {
7373
derivativesApproval: false,
7474
derivativesReciprocal: true,
7575
derivativeRevCeiling: BigInt(0),
76-
currency: "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c",
76+
currency: erc20Address[aeneid],
7777
uri: "",
7878
};
7979

@@ -1059,7 +1059,7 @@ describe("Test IpAssetClient", () => {
10591059
{
10601060
terms: PILFlavor.commercialRemix({
10611061
defaultMintingFee: 0n,
1062-
currency: mockAddress,
1062+
currency: erc20Address[aeneid],
10631063
commercialRevShare: 90,
10641064
royaltyPolicy: NativeRoyaltyPolicy.LAP,
10651065
override: {
@@ -1078,7 +1078,7 @@ describe("Test IpAssetClient", () => {
10781078
commercialUse: true,
10791079
commercializerChecker: zeroAddress,
10801080
commercializerCheckerData: zeroAddress,
1081-
currency: mockAddress,
1081+
currency: erc20Address[aeneid],
10821082
defaultMintingFee: 0n,
10831083
derivativeRevCeiling: 0n,
10841084
derivativesAllowed: true,
@@ -1414,7 +1414,7 @@ describe("Test IpAssetClient", () => {
14141414
{
14151415
terms: PILFlavor.commercialUse({
14161416
defaultMintingFee: 100n,
1417-
currency: mockAddress,
1417+
currency: erc20Address[aeneid],
14181418
royaltyPolicy: NativeRoyaltyPolicy.LRP,
14191419
}),
14201420
},
@@ -1429,7 +1429,7 @@ describe("Test IpAssetClient", () => {
14291429
commercialUse: true,
14301430
commercializerChecker: zeroAddress,
14311431
commercializerCheckerData: zeroAddress,
1432-
currency: mockAddress,
1432+
currency: erc20Address[aeneid],
14331433
defaultMintingFee: 100n,
14341434
derivativeRevCeiling: 0n,
14351435
derivativesAllowed: false,
@@ -3011,7 +3011,7 @@ describe("Test IpAssetClient", () => {
30113011
licenseTermsData: [
30123012
{
30133013
terms: PILFlavor.creativeCommonsAttribution({
3014-
currency: mockAddress,
3014+
currency: erc20Address[aeneid],
30153015
royaltyPolicy: mockAddress,
30163016
}),
30173017
},
@@ -3040,7 +3040,7 @@ describe("Test IpAssetClient", () => {
30403040
derivativesApproval: false,
30413041
derivativesReciprocal: true,
30423042
derivativeRevCeiling: 0n,
3043-
currency: mockAddress,
3043+
currency: erc20Address[aeneid],
30443044
uri: "https://github.com/piplabs/pil-document/blob/998c13e6ee1d04eb817aefd1fe16dfe8be3cd7a2/off-chain-terms/CC-BY.json",
30453045
defaultMintingFee: 0n,
30463046
expiration: 0n,
@@ -3703,7 +3703,7 @@ describe("Test IpAssetClient", () => {
37033703
licenseTermsData: [
37043704
{
37053705
terms: PILFlavor.creativeCommonsAttribution({
3706-
currency: mockAddress,
3706+
currency: erc20Address[aeneid],
37073707
royaltyPolicy: mockAddress,
37083708
}),
37093709
},
@@ -3728,7 +3728,7 @@ describe("Test IpAssetClient", () => {
37283728
derivativesApproval: false,
37293729
derivativesReciprocal: true,
37303730
derivativeRevCeiling: 0n,
3731-
currency: mockAddress,
3731+
currency: erc20Address[aeneid],
37323732
uri: "https://github.com/piplabs/pil-document/blob/998c13e6ee1d04eb817aefd1fe16dfe8be3cd7a2/off-chain-terms/CC-BY.json",
37333733
defaultMintingFee: 0n,
37343734
expiration: 0n,

0 commit comments

Comments
 (0)