Skip to content

Commit bffd534

Browse files
feat: restrict currency/currencyToken by chain (#701)
* feat: restrict currency/currencyToken by chain (WIP only on Mainnet, WIP or MERC20 on Aeneid) * fix: resolve ESLint errors in currencyValidation, pilFlavor, registerValidation
1 parent f4bd6a9 commit bffd534

File tree

10 files changed

+218
-60
lines changed

10 files changed

+218
-60
lines changed

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import {
5353
RemoveIpsFromGroupRequest,
5454
} from "../types/resources/group";
5555
import { AccessPermission } from "../types/resources/permission";
56+
import { validateCurrencyToken } from "../utils/currencyValidation";
5657
import { handleError } from "../utils/errors";
5758
import { getFunctionSignature } from "../utils/getFunctionSignature";
5859
import { getIpMetadataForWorkflow } from "../utils/getIpMetadataForWorkflow";
@@ -430,6 +431,9 @@ export class GroupClient {
430431
if (currencyTokens.some((token) => token === zeroAddress)) {
431432
throw new Error("Currency token cannot be the zero address.");
432433
}
434+
currencyTokens.forEach((token) =>
435+
validateCurrencyToken(validateAddress(token), this.chainId),
436+
);
433437
const collectAndClaimParams = {
434438
groupIpId: validateAddress(groupIpId),
435439
currencyTokens: validateAddresses(currencyTokens),
@@ -527,6 +531,7 @@ export class GroupClient {
527531
memberIpIds,
528532
}: GetClaimableRewardRequest): Promise<bigint[]> {
529533
try {
534+
validateCurrencyToken(validateAddress(currencyToken), this.chainId);
530535
const claimableReward = await this.groupingModuleClient.getClaimableReward({
531536
groupId: validateAddress(groupIpId),
532537
ipIds: validateAddresses(memberIpIds),
@@ -576,6 +581,7 @@ export class GroupClient {
576581
txOptions,
577582
}: ClaimRewardRequest): Promise<ClaimRewardResponse> {
578583
try {
584+
validateCurrencyToken(validateAddress(currencyToken), this.chainId);
579585
const claimRewardParam: GroupingModuleClaimRewardRequest = {
580586
groupId: validateAddress(groupIpId),
581587
ipIds: validateAddresses(memberIpIds),
@@ -608,6 +614,7 @@ export class GroupClient {
608614
txOptions,
609615
}: CollectRoyaltiesRequest): Promise<CollectRoyaltiesResponse> {
610616
try {
617+
validateCurrencyToken(validateAddress(currencyToken), this.chainId);
611618
const collectRoyaltiesParam: GroupingModuleCollectRoyaltiesRequest = {
612619
groupId: validateAddress(groupIpId),
613620
token: validateAddress(currencyToken),
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { Address, getAddress, zeroAddress } from "viem";
2+
3+
import { erc20Address, wrappedIpAddress } from "../abi/generated";
4+
import { ChainIds, SupportedChainIds } from "../types/config";
5+
6+
const MAINNET_CHAIN_ID = 1514;
7+
const AENEID_CHAIN_ID = 1315;
8+
9+
/** Per-chain whitelist: chainId -> set of allowed currency token addresses (checksummed). */
10+
const CURRENCY_TOKEN_WHITELIST: Record<ChainIds, ReadonlySet<string>> = {
11+
[MAINNET_CHAIN_ID]: new Set([
12+
getAddress(wrappedIpAddress[MAINNET_CHAIN_ID]),
13+
]),
14+
[AENEID_CHAIN_ID]: new Set([
15+
getAddress(wrappedIpAddress[AENEID_CHAIN_ID]),
16+
getAddress(erc20Address[AENEID_CHAIN_ID]),
17+
]),
18+
} as const;
19+
20+
/** Human-readable description and error hint per chain. */
21+
const CHAIN_CURRENCY_HINT: Record<ChainIds, string> = {
22+
[MAINNET_CHAIN_ID]: "On Mainnet, only WIP is allowed as currency token.",
23+
[AENEID_CHAIN_ID]: "On Aeneid Testnet, only WIP or MERC20 is allowed as currency token.",
24+
};
25+
26+
const toChainId = (chainId: SupportedChainIds): ChainIds => {
27+
if (chainId === "mainnet" || chainId === 1514) {
28+
return 1514;
29+
}
30+
31+
if (chainId === "aeneid" || chainId === 1315) {
32+
return 1315;
33+
}
34+
35+
return chainId as ChainIds;
36+
};
37+
38+
/**
39+
* Validates that the currency token is allowed for the given chain.
40+
* - Aeneid Testnet (1315): allows WIP or MERC20
41+
* - Mainnet (1514): allows WIP only
42+
*
43+
* @param token - The currency token address to validate
44+
* @param chainId - The chain ID (1315 or "aeneid" for Aeneid, 1514 or "mainnet" for Mainnet)
45+
* @throws Error if token is zero address or not allowed for the chain
46+
*/
47+
export const validateCurrencyToken = (
48+
token: Address,
49+
chainId: SupportedChainIds | ChainIds,
50+
): void => {
51+
if (token === null || token === undefined || token === zeroAddress) {
52+
throw new Error("Currency token cannot be zero address.");
53+
}
54+
55+
const resolvedChainId = toChainId(chainId as SupportedChainIds);
56+
const allowedTokens = CURRENCY_TOKEN_WHITELIST[resolvedChainId];
57+
58+
if (allowedTokens === null || allowedTokens === undefined) {
59+
throw new Error(`Unsupported chain ID: ${chainId}.`);
60+
}
61+
62+
const normalizedToken = getAddress(token);
63+
if (!allowedTokens.has(normalizedToken)) {
64+
throw new Error(
65+
`${CHAIN_CURRENCY_HINT[resolvedChainId]} The provided token is not allowed.`,
66+
);
67+
}
68+
};

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { zeroAddress } from "viem";
22

3+
import { validateCurrencyToken } from "./currencyValidation";
34
import { PILFlavorError } from "./errors";
45
import { royaltyPolicyInputToAddress } from "./royalty";
5-
import { SupportedChainIds } from "../types/config";
6+
import { ChainIds, SupportedChainIds } from "../types/config";
67
import { LicenseTerms, LicenseTermsInput } from "../types/resources/license";
78
import {
89
CommercialRemixRequest,
@@ -204,6 +205,16 @@ export class PILFlavor {
204205
throw new PILFlavorError("Royalty policy requires currency token.");
205206
}
206207

208+
// Validate currency token is allowed for the chain when currency is set (Aeneid: WIP or MERC20; Mainnet: WIP only). Zero address is allowed (e.g. no royalty).
209+
if (
210+
currency !== null &&
211+
currency !== undefined &&
212+
currency !== zeroAddress &&
213+
chainId !== undefined
214+
) {
215+
validateCurrencyToken(currency, chainId as ChainIds);
216+
}
217+
207218
// Validate defaultMintingFee
208219
if (normalized.defaultMintingFee < 0n) {
209220
throw new PILFlavorError("DefaultMintingFee should be greater than or equal to 0.");

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
ValidateDerivativeDataConfig,
2525
} from "../../types/utils/registerHelper";
2626
import { TokenSpender } from "../../types/utils/token";
27+
import { validateCurrencyToken } from "../currencyValidation";
2728
import { PILFlavor } from "../pilFlavor";
2829
import { getRevenueShare } from "../royalty";
2930
import { getDeadline } from "../sign";
@@ -68,6 +69,7 @@ export const validateLicenseTermsData = async (
6869
}
6970

7071
if (validateAddress(licenseTerm.currency) !== zeroAddress) {
72+
validateCurrencyToken(licenseTerm.currency, chainId);
7173
const isWhitelistedRoyaltyToken =
7274
await royaltyModuleReadOnlyClient.isWhitelistedRoyaltyToken({
7375
token: licenseTerm.currency,

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

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,16 @@ 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 { IpAccountImplClient, wrappedIpAddress } 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";
1010
import { createMockPublicClient, createMockWalletClient } from "../testUtils";
1111

1212
use(chaiAsPromised);
13+
14+
// Aeneid (1315) allows WIP or MERC20; group tests use chainId 1315
15+
const validCurrencyToken = wrappedIpAddress[aeneid];
16+
1317
const mockLicenseData: LicenseDataInput = {
1418
licenseTermsId: 100,
1519
licensingConfig: {
@@ -529,7 +533,7 @@ describe("Test IpAssetClient", () => {
529533

530534
const result = groupClient.collectAndDistributeGroupRoyalties({
531535
groupIpId: mockAddress,
532-
currencyTokens: [mockAddress],
536+
currencyTokens: [validCurrencyToken],
533537
memberIpIds: [mockAddress],
534538
});
535539
await expect(result).to.be.rejectedWith(
@@ -545,7 +549,7 @@ describe("Test IpAssetClient", () => {
545549
.resolves(false);
546550
const result = groupClient.collectAndDistributeGroupRoyalties({
547551
groupIpId: mockAddress,
548-
currencyTokens: [mockAddress],
552+
currencyTokens: [validCurrencyToken],
549553
memberIpIds: [mockAddress],
550554
});
551555
await expect(result).to.be.rejectedWith(
@@ -557,7 +561,7 @@ describe("Test IpAssetClient", () => {
557561
stub(groupClient.ipAssetRegistryClient, "isRegistered").resolves(true);
558562
const result = groupClient.collectAndDistributeGroupRoyalties({
559563
groupIpId: mockAddress,
560-
currencyTokens: [mockAddress],
564+
currencyTokens: [validCurrencyToken],
561565
memberIpIds: [],
562566
});
563567
await expect(result).to.be.rejectedWith(
@@ -616,7 +620,7 @@ describe("Test IpAssetClient", () => {
616620

617621
const result = await groupClient.collectAndDistributeGroupRoyalties({
618622
groupIpId: mockAddress,
619-
currencyTokens: [mockAddress],
623+
currencyTokens: [validCurrencyToken],
620624
memberIpIds: [mockAddress],
621625
});
622626
expect(result.txHash).equal(txHash);
@@ -648,7 +652,7 @@ describe("Test IpAssetClient", () => {
648652
stub(groupClient.groupingWorkflowsClient, "collectRoyaltiesAndClaimReward").resolves(txHash);
649653
const result = await groupClient.collectAndDistributeGroupRoyalties({
650654
groupIpId: mockAddress,
651-
currencyTokens: [mockAddress],
655+
currencyTokens: [validCurrencyToken],
652656
memberIpIds: [mockAddress],
653657
});
654658
expect(result.txHash).equal(txHash);
@@ -725,7 +729,7 @@ describe("Test IpAssetClient", () => {
725729
stub(groupClient.groupingModuleClient, "getClaimableReward").rejects(new Error("rpc error"));
726730
const result = groupClient.getClaimableReward({
727731
groupIpId: mockAddress,
728-
currencyToken: mockAddress,
732+
currencyToken: validCurrencyToken,
729733
memberIpIds: [mockAddress],
730734
});
731735
await expect(result).to.be.rejectedWith("Failed to get claimable reward: rpc error");
@@ -735,7 +739,7 @@ describe("Test IpAssetClient", () => {
735739
stub(groupClient.groupingModuleClient, "getClaimableReward").resolves([10n]);
736740
const result = await groupClient.getClaimableReward({
737741
groupIpId: mockAddress,
738-
currencyToken: mockAddress,
742+
currencyToken: validCurrencyToken,
739743
memberIpIds: [mockAddress],
740744
});
741745
expect(result).to.deep.equal([10n]);
@@ -767,7 +771,7 @@ describe("Test IpAssetClient", () => {
767771
stub(groupClient.groupingModuleClient, "claimReward").rejects(new Error("rpc error"));
768772
const result = groupClient.claimReward({
769773
groupIpId: mockAddress,
770-
currencyToken: mockAddress,
774+
currencyToken: validCurrencyToken,
771775
memberIpIds: [mockAddress],
772776
});
773777
await expect(result).to.be.rejectedWith("Failed to claim reward: rpc error");
@@ -785,7 +789,7 @@ describe("Test IpAssetClient", () => {
785789
]);
786790
const result = await groupClient.claimReward({
787791
groupIpId: mockAddress,
788-
currencyToken: mockAddress,
792+
currencyToken: validCurrencyToken,
789793
memberIpIds: [mockAddress],
790794
});
791795
expect(result.txHash).equal(txHash);
@@ -803,7 +807,7 @@ describe("Test IpAssetClient", () => {
803807
]);
804808
const result = await groupClient.claimReward({
805809
groupIpId: mockAddress,
806-
currencyToken: mockAddress,
810+
currencyToken: validCurrencyToken,
807811
memberIpIds: [mockAddress],
808812
});
809813
expect(result.txHash).equal(txHash);
@@ -824,7 +828,7 @@ describe("Test IpAssetClient", () => {
824828

825829
const result = groupClient.collectRoyalties({
826830
groupIpId: mockAddress,
827-
currencyToken: mockAddress,
831+
currencyToken: validCurrencyToken,
828832
});
829833
await expect(result).to.be.rejectedWith("Failed to collect royalties: rpc error");
830834
});
@@ -844,7 +848,7 @@ describe("Test IpAssetClient", () => {
844848
]);
845849
const result = await groupClient.collectRoyalties({
846850
groupIpId: mockAddress,
847-
currencyToken: mockAddress,
851+
currencyToken: validCurrencyToken,
848852
});
849853
expect(result.txHash).equal(txHash);
850854
expect(result.collectedRoyalties).to.equal(100n);
@@ -866,7 +870,7 @@ describe("Test IpAssetClient", () => {
866870

867871
const result = await groupClient.collectRoyalties({
868872
groupIpId: mockAddress,
869-
currencyToken: mockAddress,
873+
currencyToken: validCurrencyToken,
870874
});
871875
expect(result.txHash).equal(txHash);
872876
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: WIP_TOKEN_ADDRESS,
7777
uri: "",
7878
};
7979

@@ -1059,7 +1059,7 @@ describe("Test IpAssetClient", () => {
10591059
{
10601060
terms: PILFlavor.commercialRemix({
10611061
defaultMintingFee: 0n,
1062-
currency: mockAddress,
1062+
currency: WIP_TOKEN_ADDRESS,
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: WIP_TOKEN_ADDRESS,
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: WIP_TOKEN_ADDRESS,
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: WIP_TOKEN_ADDRESS,
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: WIP_TOKEN_ADDRESS,
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: WIP_TOKEN_ADDRESS,
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: WIP_TOKEN_ADDRESS,
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: WIP_TOKEN_ADDRESS,
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)