From 85a652f3de70e365445376c37b30b86f4f9ef2af Mon Sep 17 00:00:00 2001 From: Bhargava Sai Macha Date: Mon, 20 May 2024 12:06:39 -0400 Subject: [PATCH 1/8] minor security fixes and test updates --- clients/rwa-token-sdk/package.json | 4 +- .../src/asset-controller/instructions.ts | 6 + .../rwa-token-sdk/src/classes/PolicyEngine.ts | 15 + .../rwa-token-sdk/src/policy-engine/data.ts | 14 +- .../src/policy-engine/instructions.ts | 54 +- .../src/programs/idls/AssetController.json | 668 ++++++++++-------- .../src/programs/idls/PolicyEngine.json | 8 +- .../programs/types/AssetControllerTypes.ts | 668 ++++++++++-------- .../src/programs/types/PolicyEngineTypes.ts | 8 +- clients/rwa-token-sdk/tests/e2e.test.ts | 29 +- programs/asset_controller/src/error.rs | 4 + .../src/instructions/execute.rs | 25 +- programs/asset_controller/src/state/track.rs | 1 + programs/asset_controller/src/utils.rs | 23 +- .../src/instructions/account/update.rs | 4 +- .../policy_engine/src/instructions/create.rs | 1 + .../policy_engine/src/instructions/detach.rs | 2 +- .../src/instructions/engine/delegate.rs | 30 - .../src/instructions/engine/mod.rs | 2 - programs/policy_engine/src/state/account.rs | 8 +- programs/policy_engine/src/utils.rs | 1 + 21 files changed, 869 insertions(+), 706 deletions(-) delete mode 100644 programs/policy_engine/src/instructions/engine/delegate.rs diff --git a/clients/rwa-token-sdk/package.json b/clients/rwa-token-sdk/package.json index 2f68b22..72d61f7 100644 --- a/clients/rwa-token-sdk/package.json +++ b/clients/rwa-token-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@bridgesplit/rwa-token-sdk", - "version": "0.0.0", + "version": "0.0.1", "description": "RWA Token SDK for the development of permissioned tokens on SVM blockchains.", "homepage": "https://github.com/bridgesplit/rwa-token#readme", "scripts": { @@ -35,7 +35,7 @@ "author": "Standard Labs, Inc.", "contributors": [ "Luke Truitt ", - "Bhargava Macha ", + "Bhargava Sai Macha ", "Chris Hagedorn " ], "license": "MIT" diff --git a/clients/rwa-token-sdk/src/asset-controller/instructions.ts b/clients/rwa-token-sdk/src/asset-controller/instructions.ts index 2d90538..1e4d94d 100644 --- a/clients/rwa-token-sdk/src/asset-controller/instructions.ts +++ b/clients/rwa-token-sdk/src/asset-controller/instructions.ts @@ -4,6 +4,7 @@ import { Keypair, PublicKey, SystemProgram, + SYSVAR_INSTRUCTIONS_PUBKEY, type TransactionInstruction, } from "@solana/web3.js"; import { @@ -199,6 +200,11 @@ export async function getTransferTokensIx( isWritable: false, isSigner: false, }, + { + pubkey: SYSVAR_INSTRUCTIONS_PUBKEY, + isWritable: false, + isSigner: false, + }, { pubkey: getExtraMetasListPda(args.assetMint), isWritable: false, diff --git a/clients/rwa-token-sdk/src/classes/PolicyEngine.ts b/clients/rwa-token-sdk/src/classes/PolicyEngine.ts index 9b881c2..c5bc997 100644 --- a/clients/rwa-token-sdk/src/classes/PolicyEngine.ts +++ b/clients/rwa-token-sdk/src/classes/PolicyEngine.ts @@ -1,8 +1,10 @@ import { type IxReturn } from "../utils"; import { type AttachPolicyArgs, + DetachPolicyArgs, getAttachToPolicyAccountIx, getCreatePolicyAccountIx, + getDetachFromPolicyAccountIx, getPolicyEnginePda, } from "../policy-engine"; import { type RwaClient } from "./Client"; @@ -44,6 +46,19 @@ export class PolicyEngine { return attachPolicyIx; } + /** + * Asynchronously detaches a policy to the policy account. + * @param - {@link DetachPolicyArgs} + * @returns A Promise that resolves to the instructions to detach a policy. + * */ + async detachPolicy(policyArgs: DetachPolicyArgs): Promise { + const attachPolicyIx = await getDetachFromPolicyAccountIx( + policyArgs, + this.rwaClient.provider + ); + return attachPolicyIx; + } + /** * Retrieves the policy registry pda account for a specific asset mint. * @param assetMint - The string representation of the asset's mint address. diff --git a/clients/rwa-token-sdk/src/policy-engine/data.ts b/clients/rwa-token-sdk/src/policy-engine/data.ts index d9b194f..21c5c03 100644 --- a/clients/rwa-token-sdk/src/policy-engine/data.ts +++ b/clients/rwa-token-sdk/src/policy-engine/data.ts @@ -1,6 +1,6 @@ import { type AnchorProvider } from "@coral-xyz/anchor"; import { type PolicyEngineAccount, type PolicyAccount } from "./types"; -import { getPolicyEnginePda, getPolicyEngineProgram } from "./utils"; +import { getPolicyAccountPda, getPolicyEnginePda, getPolicyEngineProgram } from "./utils"; /** * Retrieves policy engine account associated with a specific asset mint. @@ -16,16 +16,12 @@ export async function getPolicyEngineAccount(assetMint: string, provider: Anchor } /** - * Retrieves all policy engine accounts for a specific asset mint. + * Retrieves the policy engine account for a specific asset mint. * @param assetMint - The string representation of the asset mint. * @returns A promise resolving to an array of {@link PolicyAccount}, or `undefined` if undefined doesn't exist. */ -export async function getPolicyAccounts(assetMint: string, provider: AnchorProvider): Promise { +export async function getPolicyAccount(assetMint: string, provider: AnchorProvider): Promise { const policyEngineProgram = getPolicyEngineProgram(provider); - const policyEnginePda = getPolicyEnginePda(assetMint); - const policyAccounts = await provider.connection.getProgramAccounts(policyEngineProgram.programId, { - filters: - [{ memcmp: { offset: 9, bytes: policyEnginePda.toBase58() } }], - }); - return policyAccounts.map(account => policyEngineProgram.coder.accounts.decode("PolicyAccount", account.account.data)); + const policyAccountPda = getPolicyAccountPda(assetMint); + return policyEngineProgram.account.policyAccount.fetch(policyAccountPda); } diff --git a/clients/rwa-token-sdk/src/policy-engine/instructions.ts b/clients/rwa-token-sdk/src/policy-engine/instructions.ts index a04b24f..2a26aae 100644 --- a/clients/rwa-token-sdk/src/policy-engine/instructions.ts +++ b/clients/rwa-token-sdk/src/policy-engine/instructions.ts @@ -42,7 +42,7 @@ export async function getCreatePolicyEngineIx( return ix; } -/** Represents the arguments required to attach a policy to an assets. */ +/** Represents the arguments required to attach a policy to an asset. */ export type AttachPolicyArgs = { authority: string; owner: string; @@ -52,14 +52,50 @@ export type AttachPolicyArgs = { policyType: PolicyType; }; -/** TODO: Cleanup unused helper function */ -export function padIdentityLevels(levels: number[]): number[] { - const maxLevels = 10; - return levels.concat(new Array(maxLevels - levels.length).fill(0)); +/** Represents the arguments required to detach a policy from an asset. */ +export type DetachPolicyArgs = { + authority: string; + owner: string; + assetMint: string; + payer: string; + hash: string; + }; + +/** + * Generate instructions to connect am policy to an asset. + * + * This function constructs an instruction to attach a policy account to an asset + * using the provided arguments. It calls the policy engine program to attach the policy account, + * and returns the generated instruction along with the required signers. + * + * @param args {@link AttachPolicyArgs} + * @returns - {@link IxReturn}, a list of transaction instructions and a new key pair responsible to sign it. + */ +export async function getAttachToPolicyAccountIx( + args: AttachPolicyArgs, + provider: AnchorProvider +): Promise { + const policyProgram = getPolicyEngineProgram(provider); + const policyAccount = getPolicyAccountPda(args.assetMint); + const ix = await policyProgram.methods + .attachToPolicyAccount(args.identityFilter, args.policyType) + .accountsStrict({ + policyAccount, + signer: new PublicKey(args.authority), + payer: args.payer, + policyEngine: getPolicyEnginePda(args.assetMint), + systemProgram: SystemProgram.programId, + }) + .instruction(); + return { + ixs: [ix], + signers: [], + }; } + /** - * Generate instructions to connect an identity policy account to an asset. + * Generate instructions to detac an identity policy account to an asset. * * This function constructs an instruction to attach a policy account to an asset * using the provided arguments. It creates a new policy account, calls the policy @@ -69,14 +105,14 @@ export function padIdentityLevels(levels: number[]): number[] { * @param args {@link AttachPolicyArgs} * @returns - {@link IxReturn}, a list of transaction instructions and a new key pair responsible to sign it. */ -export async function getAttachToPolicyAccountIx( - args: AttachPolicyArgs, +export async function getDetachFromPolicyAccountIx( + args: DetachPolicyArgs, provider: AnchorProvider ): Promise { const policyProgram = getPolicyEngineProgram(provider); const policyAccount = getPolicyAccountPda(args.assetMint); const ix = await policyProgram.methods - .attachToPolicyAccount(args.identityFilter, args.policyType) + .detachFromPolicyAccount(args.hash) .accountsStrict({ policyAccount, signer: new PublicKey(args.authority), diff --git a/clients/rwa-token-sdk/src/programs/idls/AssetController.json b/clients/rwa-token-sdk/src/programs/idls/AssetController.json index 17aef50..5d289f3 100644 --- a/clients/rwa-token-sdk/src/programs/idls/AssetController.json +++ b/clients/rwa-token-sdk/src/programs/idls/AssetController.json @@ -37,7 +37,93 @@ }, { "name": "token_account", - "writable": true + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "owner" + }, + { + "kind": "const", + "value": [ + 6, + 221, + 246, + 225, + 215, + 101, + 161, + 147, + 217, + 203, + 225, + 70, + 206, + 235, + 121, + 172, + 28, + 180, + 133, + 237, + 95, + 91, + 55, + 145, + 58, + 140, + 245, + 133, + 126, + 255, + 0, + 169 + ] + }, + { + "kind": "account", + "path": "asset_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } }, { "name": "token_program", @@ -176,7 +262,60 @@ }, { "name": "token_account", - "writable": true + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "owner" + }, + { + "kind": "account", + "path": "token_program" + }, + { + "kind": "account", + "path": "asset_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } }, { "name": "tracker_account", @@ -226,7 +365,93 @@ ], "accounts": [ { - "name": "source_account" + "name": "source_account", + "pda": { + "seeds": [ + { + "kind": "account", + "path": "owner_delegate" + }, + { + "kind": "const", + "value": [ + 6, + 221, + 246, + 225, + 238, + 117, + 143, + 222, + 24, + 66, + 93, + 188, + 228, + 108, + 205, + 218, + 182, + 26, + 252, + 77, + 131, + 185, + 13, + 39, + 254, + 189, + 249, + 40, + 216, + 161, + 139, + 252 + ] + }, + { + "kind": "account", + "path": "asset_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } }, { "name": "asset_mint" @@ -296,6 +521,9 @@ }, { "name": "policy_account" + }, + { + "name": "instructions_program" } ], "args": [ @@ -331,7 +559,60 @@ }, { "name": "token_account", - "writable": true + "writable": true, + "pda": { + "seeds": [ + { + "kind": "arg", + "path": "args.to" + }, + { + "kind": "account", + "path": "token_program" + }, + { + "kind": "account", + "path": "asset_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } }, { "name": "token_program", @@ -375,7 +656,60 @@ }, { "name": "token_account", - "writable": true + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "owner" + }, + { + "kind": "account", + "path": "token_program" + }, + { + "kind": "account", + "path": "asset_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } }, { "name": "token_program", @@ -404,58 +738,6 @@ 105 ] }, - { - "name": "IdentityAccount", - "discriminator": [ - 194, - 90, - 181, - 160, - 182, - 206, - 116, - 158 - ] - }, - { - "name": "IdentityRegistryAccount", - "discriminator": [ - 154, - 254, - 118, - 4, - 115, - 36, - 125, - 78 - ] - }, - { - "name": "PolicyAccount", - "discriminator": [ - 218, - 201, - 183, - 164, - 156, - 127, - 81, - 175 - ] - }, - { - "name": "PolicyEngineAccount", - "discriminator": [ - 124, - 85, - 205, - 80, - 2, - 18, - 26, - 45 - ] - }, { "name": "TrackerAccount", "discriminator": [ @@ -510,6 +792,21 @@ "code": 6007, "name": "Unauthorized", "msg": "Unauthorized" + }, + { + "code": 6008, + "name": "InvalidPdaPassedIn", + "msg": "Pda passed in for transfer is wrong" + }, + { + "code": 6009, + "name": "InvalidCpiTransferProgram", + "msg": "Invalid cpi program in transfer" + }, + { + "code": 6010, + "name": "InvalidCpiTransferAmount", + "msg": "Invalid cpi amount in transfer" } ], "types": [ @@ -548,23 +845,6 @@ ] } }, - { - "name": "ComparisionType", - "repr": { - "kind": "rust" - }, - "type": { - "kind": "enum", - "variants": [ - { - "name": "Or" - }, - { - "name": "And" - } - ] - } - }, { "name": "CreateAssetControllerArgs", "type": { @@ -595,97 +875,6 @@ ] } }, - { - "name": "IdentityAccount", - "type": { - "kind": "struct", - "fields": [ - { - "name": "version", - "docs": [ - "version of the account" - ], - "type": "u8" - }, - { - "name": "identity_registry", - "docs": [ - "identity registry to which the account belongs" - ], - "type": "pubkey" - }, - { - "name": "owner", - "docs": [ - "owner of the identity account" - ], - "type": "pubkey" - }, - { - "name": "levels", - "type": "bytes" - } - ] - } - }, - { - "name": "IdentityFilter", - "type": { - "kind": "struct", - "fields": [ - { - "name": "identity_levels", - "type": { - "array": [ - "u8", - 10 - ] - } - }, - { - "name": "comparision_type", - "type": { - "defined": { - "name": "ComparisionType" - } - } - } - ] - } - }, - { - "name": "IdentityRegistryAccount", - "type": { - "kind": "struct", - "fields": [ - { - "name": "version", - "type": "u8" - }, - { - "name": "asset_mint", - "docs": [ - "corresponding asset mint" - ], - "type": "pubkey" - }, - { - "name": "authority", - "docs": [ - "authority to manage the registry" - ], - "type": "pubkey" - }, - { - "name": "delegate", - "docs": [ - "registry delegate" - ], - "type": "pubkey" - } - ] - } - }, { "name": "IssueTokensArgs", "type": { @@ -702,155 +891,6 @@ ] } }, - { - "name": "Policy", - "type": { - "kind": "struct", - "fields": [ - { - "name": "hash", - "type": "string" - }, - { - "name": "policy_type", - "type": { - "defined": { - "name": "PolicyType" - } - } - }, - { - "name": "identity_filter", - "type": { - "defined": { - "name": "IdentityFilter" - } - } - } - ] - } - }, - { - "name": "PolicyAccount", - "type": { - "kind": "struct", - "fields": [ - { - "name": "version", - "type": "u8" - }, - { - "name": "policy_engine", - "docs": [ - "Engine account that the policy belongs to" - ], - "type": "pubkey" - }, - { - "name": "policies", - "docs": [ - "Different policies that can be applied to the policy account" - ], - "type": { - "vec": { - "defined": { - "name": "Policy" - } - } - } - } - ] - } - }, - { - "name": "PolicyEngineAccount", - "type": { - "kind": "struct", - "fields": [ - { - "name": "version", - "docs": [ - "version" - ], - "type": "u8" - }, - { - "name": "asset_mint", - "docs": [ - "asset mint" - ], - "type": "pubkey" - }, - { - "name": "authority", - "docs": [ - "authority of the registry" - ], - "type": "pubkey" - }, - { - "name": "delegate", - "docs": [ - "policy delegate" - ], - "type": "pubkey" - }, - { - "name": "max_timeframe", - "docs": [ - "max timeframe of all the policies" - ], - "type": "i64" - } - ] - } - }, - { - "name": "PolicyType", - "type": { - "kind": "enum", - "variants": [ - { - "name": "IdentityApproval" - }, - { - "name": "TransactionAmountLimit", - "fields": [ - { - "name": "limit", - "type": "u64" - } - ] - }, - { - "name": "TransactionAmountVelocity", - "fields": [ - { - "name": "limit", - "type": "u64" - }, - { - "name": "timeframe", - "type": "i64" - } - ] - }, - { - "name": "TransactionCountVelocity", - "fields": [ - { - "name": "limit", - "type": "u64" - }, - { - "name": "timeframe", - "type": "i64" - } - ] - } - ] - } - }, { "name": "TrackerAccount", "type": { diff --git a/clients/rwa-token-sdk/src/programs/idls/PolicyEngine.json b/clients/rwa-token-sdk/src/programs/idls/PolicyEngine.json index d9f9eb1..800019c 100644 --- a/clients/rwa-token-sdk/src/programs/idls/PolicyEngine.json +++ b/clients/rwa-token-sdk/src/programs/idls/PolicyEngine.json @@ -374,18 +374,18 @@ "type": "string" }, { - "name": "policy_type", + "name": "identity_filter", "type": { "defined": { - "name": "PolicyType" + "name": "IdentityFilter" } } }, { - "name": "identity_filter", + "name": "policy_type", "type": { "defined": { - "name": "IdentityFilter" + "name": "PolicyType" } } } diff --git a/clients/rwa-token-sdk/src/programs/types/AssetControllerTypes.ts b/clients/rwa-token-sdk/src/programs/types/AssetControllerTypes.ts index a09987c..0bb58c7 100644 --- a/clients/rwa-token-sdk/src/programs/types/AssetControllerTypes.ts +++ b/clients/rwa-token-sdk/src/programs/types/AssetControllerTypes.ts @@ -43,7 +43,93 @@ export type AssetController = { }, { "name": "tokenAccount", - "writable": true + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "owner" + }, + { + "kind": "const", + "value": [ + 6, + 221, + 246, + 225, + 215, + 101, + 161, + 147, + 217, + 203, + 225, + 70, + 206, + 235, + 121, + 172, + 28, + 180, + 133, + 237, + 95, + 91, + 55, + 145, + 58, + 140, + 245, + 133, + 126, + 255, + 0, + 169 + ] + }, + { + "kind": "account", + "path": "assetMint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } }, { "name": "tokenProgram", @@ -182,7 +268,60 @@ export type AssetController = { }, { "name": "tokenAccount", - "writable": true + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "owner" + }, + { + "kind": "account", + "path": "tokenProgram" + }, + { + "kind": "account", + "path": "assetMint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } }, { "name": "trackerAccount", @@ -232,7 +371,93 @@ export type AssetController = { ], "accounts": [ { - "name": "sourceAccount" + "name": "sourceAccount", + "pda": { + "seeds": [ + { + "kind": "account", + "path": "ownerDelegate" + }, + { + "kind": "const", + "value": [ + 6, + 221, + 246, + 225, + 238, + 117, + 143, + 222, + 24, + 66, + 93, + 188, + 228, + 108, + 205, + 218, + 182, + 26, + 252, + 77, + 131, + 185, + 13, + 39, + 254, + 189, + 249, + 40, + 216, + 161, + 139, + 252 + ] + }, + { + "kind": "account", + "path": "assetMint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } }, { "name": "assetMint" @@ -302,6 +527,9 @@ export type AssetController = { }, { "name": "policyAccount" + }, + { + "name": "instructionsProgram" } ], "args": [ @@ -337,7 +565,60 @@ export type AssetController = { }, { "name": "tokenAccount", - "writable": true + "writable": true, + "pda": { + "seeds": [ + { + "kind": "arg", + "path": "args.to" + }, + { + "kind": "account", + "path": "tokenProgram" + }, + { + "kind": "account", + "path": "assetMint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } }, { "name": "tokenProgram", @@ -381,7 +662,60 @@ export type AssetController = { }, { "name": "tokenAccount", - "writable": true + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "owner" + }, + { + "kind": "account", + "path": "tokenProgram" + }, + { + "kind": "account", + "path": "assetMint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } }, { "name": "tokenProgram", @@ -410,58 +744,6 @@ export type AssetController = { 105 ] }, - { - "name": "identityAccount", - "discriminator": [ - 194, - 90, - 181, - 160, - 182, - 206, - 116, - 158 - ] - }, - { - "name": "identityRegistryAccount", - "discriminator": [ - 154, - 254, - 118, - 4, - 115, - 36, - 125, - 78 - ] - }, - { - "name": "policyAccount", - "discriminator": [ - 218, - 201, - 183, - 164, - 156, - 127, - 81, - 175 - ] - }, - { - "name": "policyEngineAccount", - "discriminator": [ - 124, - 85, - 205, - 80, - 2, - 18, - 26, - 45 - ] - }, { "name": "trackerAccount", "discriminator": [ @@ -516,6 +798,21 @@ export type AssetController = { "code": 6007, "name": "unauthorized", "msg": "unauthorized" + }, + { + "code": 6008, + "name": "invalidPdaPassedIn", + "msg": "Pda passed in for transfer is wrong" + }, + { + "code": 6009, + "name": "invalidCpiTransferProgram", + "msg": "Invalid cpi program in transfer" + }, + { + "code": 6010, + "name": "invalidCpiTransferAmount", + "msg": "Invalid cpi amount in transfer" } ], "types": [ @@ -554,23 +851,6 @@ export type AssetController = { ] } }, - { - "name": "comparisionType", - "repr": { - "kind": "rust" - }, - "type": { - "kind": "enum", - "variants": [ - { - "name": "or" - }, - { - "name": "and" - } - ] - } - }, { "name": "createAssetControllerArgs", "type": { @@ -601,97 +881,6 @@ export type AssetController = { ] } }, - { - "name": "identityAccount", - "type": { - "kind": "struct", - "fields": [ - { - "name": "version", - "docs": [ - "version of the account" - ], - "type": "u8" - }, - { - "name": "identityRegistry", - "docs": [ - "identity registry to which the account belongs" - ], - "type": "pubkey" - }, - { - "name": "owner", - "docs": [ - "owner of the identity account" - ], - "type": "pubkey" - }, - { - "name": "levels", - "type": "bytes" - } - ] - } - }, - { - "name": "identityFilter", - "type": { - "kind": "struct", - "fields": [ - { - "name": "identityLevels", - "type": { - "array": [ - "u8", - 10 - ] - } - }, - { - "name": "comparisionType", - "type": { - "defined": { - "name": "comparisionType" - } - } - } - ] - } - }, - { - "name": "identityRegistryAccount", - "type": { - "kind": "struct", - "fields": [ - { - "name": "version", - "type": "u8" - }, - { - "name": "assetMint", - "docs": [ - "corresponding asset mint" - ], - "type": "pubkey" - }, - { - "name": "authority", - "docs": [ - "authority to manage the registry" - ], - "type": "pubkey" - }, - { - "name": "delegate", - "docs": [ - "registry delegate" - ], - "type": "pubkey" - } - ] - } - }, { "name": "issueTokensArgs", "type": { @@ -708,155 +897,6 @@ export type AssetController = { ] } }, - { - "name": "policy", - "type": { - "kind": "struct", - "fields": [ - { - "name": "hash", - "type": "string" - }, - { - "name": "policyType", - "type": { - "defined": { - "name": "policyType" - } - } - }, - { - "name": "identityFilter", - "type": { - "defined": { - "name": "identityFilter" - } - } - } - ] - } - }, - { - "name": "policyAccount", - "type": { - "kind": "struct", - "fields": [ - { - "name": "version", - "type": "u8" - }, - { - "name": "policyEngine", - "docs": [ - "Engine account that the policy belongs to" - ], - "type": "pubkey" - }, - { - "name": "policies", - "docs": [ - "Different policies that can be applied to the policy account" - ], - "type": { - "vec": { - "defined": { - "name": "policy" - } - } - } - } - ] - } - }, - { - "name": "policyEngineAccount", - "type": { - "kind": "struct", - "fields": [ - { - "name": "version", - "docs": [ - "version" - ], - "type": "u8" - }, - { - "name": "assetMint", - "docs": [ - "asset mint" - ], - "type": "pubkey" - }, - { - "name": "authority", - "docs": [ - "authority of the registry" - ], - "type": "pubkey" - }, - { - "name": "delegate", - "docs": [ - "policy delegate" - ], - "type": "pubkey" - }, - { - "name": "maxTimeframe", - "docs": [ - "max timeframe of all the policies" - ], - "type": "i64" - } - ] - } - }, - { - "name": "policyType", - "type": { - "kind": "enum", - "variants": [ - { - "name": "identityApproval" - }, - { - "name": "transactionAmountLimit", - "fields": [ - { - "name": "limit", - "type": "u64" - } - ] - }, - { - "name": "transactionAmountVelocity", - "fields": [ - { - "name": "limit", - "type": "u64" - }, - { - "name": "timeframe", - "type": "i64" - } - ] - }, - { - "name": "transactionCountVelocity", - "fields": [ - { - "name": "limit", - "type": "u64" - }, - { - "name": "timeframe", - "type": "i64" - } - ] - } - ] - } - }, { "name": "trackerAccount", "type": { diff --git a/clients/rwa-token-sdk/src/programs/types/PolicyEngineTypes.ts b/clients/rwa-token-sdk/src/programs/types/PolicyEngineTypes.ts index cdc510a..7c0231b 100644 --- a/clients/rwa-token-sdk/src/programs/types/PolicyEngineTypes.ts +++ b/clients/rwa-token-sdk/src/programs/types/PolicyEngineTypes.ts @@ -380,18 +380,18 @@ export type PolicyEngine = { "type": "string" }, { - "name": "policyType", + "name": "identityFilter", "type": { "defined": { - "name": "policyType" + "name": "identityFilter" } } }, { - "name": "identityFilter", + "name": "policyType", "type": { "defined": { - "name": "identityFilter" + "name": "policyType" } } } diff --git a/clients/rwa-token-sdk/tests/e2e.test.ts b/clients/rwa-token-sdk/tests/e2e.test.ts index c37038c..2a4044f 100644 --- a/clients/rwa-token-sdk/tests/e2e.test.ts +++ b/clients/rwa-token-sdk/tests/e2e.test.ts @@ -1,7 +1,8 @@ import { BN, Wallet } from "@coral-xyz/anchor"; import { type AttachPolicyArgs, - type CreateDataAccountArgs, + CreateDataAccountArgs, + getPolicyAccount, getPolicyAccountPda, getTrackerAccount, getTrackerAccountPda, @@ -321,8 +322,7 @@ describe("e2e tests", () => { authority: setup.authority.toString(), signer: setup.authority.toString(), }; - const updateDataIx = - await rwaClient.dataRegistry.updateAssetsDataAccountInfoIxns(updateDataAccountArgs); + const updateDataIx = await rwaClient.dataRegistry.updateAssetsDataAccountInfoIxns(updateDataAccountArgs); const txnId = await sendAndConfirmTransaction( rwaClient.provider.connection, new Transaction().add(updateDataIx), @@ -352,4 +352,27 @@ describe("e2e tests", () => { ); expect(txnId).toBeTruthy(); }); + + test("detach all policies", async () => { + let policyAccount = await getPolicyAccount(mint, rwaClient.provider); + + for (const policy of policyAccount?.policies ?? []) { + const policyIx = await rwaClient.policyEngine.detachPolicy({ + authority: setup.authority.toString(), + owner: setup.authority.toString(), + assetMint: mint, + payer: setup.payer.toString(), + hash: policy.hash, + }); + const txnId = await sendAndConfirmTransaction( + rwaClient.provider.connection, + new Transaction().add(...policyIx.ixs), + [setup.payerKp, setup.authorityKp] + ); + expect(txnId).toBeTruthy(); + } + policyAccount = await getPolicyAccount(mint, rwaClient.provider); + + expect(policyAccount?.policies.length).toBe(0); + }); }); diff --git a/programs/asset_controller/src/error.rs b/programs/asset_controller/src/error.rs index e34ba54..105f59f 100644 --- a/programs/asset_controller/src/error.rs +++ b/programs/asset_controller/src/error.rs @@ -20,4 +20,8 @@ pub enum AssetControllerErrors { Unauthorized, #[msg("Pda passed in for transfer is wrong")] InvalidPdaPassedIn, + #[msg("Invalid cpi program in transfer")] + InvalidCpiTransferProgram, + #[msg("Invalid cpi amount in transfer")] + InvalidCpiTransferAmount, } diff --git a/programs/asset_controller/src/instructions/execute.rs b/programs/asset_controller/src/instructions/execute.rs index 976abf9..5ed77d0 100644 --- a/programs/asset_controller/src/instructions/execute.rs +++ b/programs/asset_controller/src/instructions/execute.rs @@ -1,9 +1,12 @@ -use anchor_lang::prelude::*; +use anchor_lang::{ + prelude::*, + solana_program::sysvar::{self}, +}; use anchor_spl::token_interface::{Mint, TokenAccount}; use identity_registry::{program::IdentityRegistry, IdentityAccount, SKIP_POLICY_LEVEL}; use policy_engine::{enforce_policy, program::PolicyEngine, PolicyAccount, PolicyEngineAccount}; -use crate::{state::*, verify_pda}; +use crate::{state::*, verify_cpi_program_is_token22, verify_pda}; #[derive(Accounts)] #[instruction(amount: u64)] @@ -54,9 +57,14 @@ pub struct ExecuteTransferHook<'info> { #[account()] /// CHECK: internal ix checks pub policy_account: UncheckedAccount<'info>, + #[account(constraint = instructions_program.key() == sysvar::instructions::id())] + /// CHECK: constraint check + pub instructions_program: UncheckedAccount<'info>, } pub fn handler(ctx: Context, amount: u64) -> Result<()> { + verify_cpi_program_is_token22(&ctx.accounts.instructions_program.to_account_info(), amount)?; + let asset_mint = ctx.accounts.asset_mint.key(); verify_pda( @@ -76,12 +84,13 @@ pub fn handler(ctx: Context, amount: u64) -> Result<()> { return Ok(()); } - let policy_engine_account = PolicyEngineAccount::deserialize( + let policy_engine_account = Box::new(PolicyEngineAccount::deserialize( &mut &ctx.accounts.policy_engine_account.data.borrow_mut()[8..], - )?; + )?); - let policy_account = - PolicyAccount::deserialize(&mut &ctx.accounts.policy_account.data.borrow_mut()[8..])?; + let policy_account = Box::new(PolicyAccount::deserialize( + &mut &ctx.accounts.policy_account.data.borrow_mut()[8..], + )?); // go through with transfer if there aren't any policies attached if policy_account.policies.is_empty() { @@ -103,9 +112,9 @@ pub fn handler(ctx: Context, amount: u64) -> Result<()> { &identity_registry::id(), )?; - let identity_account = IdentityAccount::deserialize( + let identity_account = Box::new(IdentityAccount::deserialize( &mut &ctx.accounts.identity_registry_account.data.borrow_mut()[8..], - )?; + )?); // if user has identity skip level, skip enforcing policy if identity_account.levels.contains(&SKIP_POLICY_LEVEL) { diff --git a/programs/asset_controller/src/state/track.rs b/programs/asset_controller/src/state/track.rs index 6d9d0bd..3911dba 100644 --- a/programs/asset_controller/src/state/track.rs +++ b/programs/asset_controller/src/state/track.rs @@ -27,6 +27,7 @@ impl TrackerAccount { self.owner = owner; } /// for all timestamps, if timestamp is older than timestamp - max_timeframe. remove it, + #[inline(never)] pub fn update_transfer_history( &mut self, amount: u64, diff --git a/programs/asset_controller/src/utils.rs b/programs/asset_controller/src/utils.rs index 6bec789..a66da77 100644 --- a/programs/asset_controller/src/utils.rs +++ b/programs/asset_controller/src/utils.rs @@ -1,8 +1,12 @@ use anchor_lang::{ prelude::Result, - solana_program::{program::invoke, pubkey::Pubkey, system_instruction::transfer}, + solana_program::{ + program::invoke, pubkey::Pubkey, system_instruction::transfer, + sysvar::instructions::get_instruction_relative, + }, Lamports, }; +use anchor_spl::token_2022; use spl_tlv_account_resolution::{ account::ExtraAccountMeta, seeds::Seed, state::ExtraAccountMetaList, }; @@ -75,6 +79,7 @@ pub fn update_account_lamports_to_minimum_balance<'info>( Ok(()) } +#[inline(never)] pub fn verify_pda(address: Pubkey, seeds: &[&[u8]], program_id: &Pubkey) -> Result<()> { let (pda, _) = Pubkey::find_program_address(seeds, program_id); if pda != address { @@ -82,3 +87,19 @@ pub fn verify_pda(address: Pubkey, seeds: &[&[u8]], program_id: &Pubkey) -> Resu } Ok(()) } + +pub fn verify_cpi_program_is_token22( + instructions_program: &AccountInfo, + amount: u64, +) -> Result<()> { + let ix_relative = get_instruction_relative(0, instructions_program)?; + if ix_relative.program_id != token_2022::ID { + return Err(AssetControllerErrors::InvalidCpiTransferProgram.into()); + } + + if ix_relative.data != amount.to_le_bytes() { + return Err(AssetControllerErrors::InvalidCpiTransferAmount.into()); + } + + Ok(()) +} diff --git a/programs/data_registry/src/instructions/account/update.rs b/programs/data_registry/src/instructions/account/update.rs index 3283d0f..3d45e39 100644 --- a/programs/data_registry/src/instructions/account/update.rs +++ b/programs/data_registry/src/instructions/account/update.rs @@ -15,7 +15,9 @@ pub struct UpdateDataAccount<'info> { constraint = data_registry.authority == signer.key() || data_registry.delegate == signer.key() )] pub signer: Signer<'info>, - #[account()] + #[account( + constraint = data_registry.key() == data_account.data_registry + )] pub data_registry: Box>, #[account(mut)] pub data_account: Box>, diff --git a/programs/policy_engine/src/instructions/create.rs b/programs/policy_engine/src/instructions/create.rs index 29f1fdf..90c41cf 100644 --- a/programs/policy_engine/src/instructions/create.rs +++ b/programs/policy_engine/src/instructions/create.rs @@ -36,5 +36,6 @@ pub fn handler( identity_filter, policy_type, ); + ctx.accounts.policy_engine.update_max_timeframe(policy_type); Ok(()) } diff --git a/programs/policy_engine/src/instructions/detach.rs b/programs/policy_engine/src/instructions/detach.rs index 595dc72..30061cf 100644 --- a/programs/policy_engine/src/instructions/detach.rs +++ b/programs/policy_engine/src/instructions/detach.rs @@ -63,7 +63,7 @@ pub fn handler(ctx: Context, hash: String) -> Result<() } } } + ctx.accounts.policy_engine.max_timeframe = max_timeframe; } - ctx.accounts.policy_engine.max_timeframe = max_timeframe; Ok(()) } diff --git a/programs/policy_engine/src/instructions/engine/delegate.rs b/programs/policy_engine/src/instructions/engine/delegate.rs deleted file mode 100644 index ce24920..0000000 --- a/programs/policy_engine/src/instructions/engine/delegate.rs +++ /dev/null @@ -1,30 +0,0 @@ -use anchor_lang::prelude::*; -use anchor_spl::token_interface::Mint; - -use crate::state::*; - -#[derive(Accounts)] -#[instruction()] -pub struct UpdatePolicyEngineDelegate<'info> { - #[account(mut)] - pub payer: Signer<'info>, - #[account( - mint::token_program = TOKEN22 - )] - pub asset_mint: Box>, - #[account( - init, - space = 8 + PolicyEngineAccount::INIT_SPACE, - seeds = [asset_mint.key().as_ref()], - bump, - payer = payer, - )] - pub policy_engine: Box>, - pub system_program: Program<'info, System>, -} - -pub fn handler(ctx: Context, delegate: Pubkey) -> Result<()> { - ctx.accounts.policy_engine.update_delegate(delegate); - - Ok(()) -} diff --git a/programs/policy_engine/src/instructions/engine/mod.rs b/programs/policy_engine/src/instructions/engine/mod.rs index f62905e..cb2f184 100644 --- a/programs/policy_engine/src/instructions/engine/mod.rs +++ b/programs/policy_engine/src/instructions/engine/mod.rs @@ -1,5 +1,3 @@ pub mod create; -pub mod delegate; pub use create::*; -pub use delegate::*; diff --git a/programs/policy_engine/src/state/account.rs b/programs/policy_engine/src/state/account.rs index 46bd2e4..7492007 100644 --- a/programs/policy_engine/src/state/account.rs +++ b/programs/policy_engine/src/state/account.rs @@ -5,8 +5,8 @@ use crate::PolicyEngineErrors; #[derive(AnchorDeserialize, AnchorSerialize, Clone, InitSpace, Copy, Debug)] pub struct IdentityFilter { - pub identity_levels: [u8; 10], - pub comparision_type: ComparisionType, + pub identity_levels: [u8; 10], // 10 + pub comparision_type: ComparisionType, // 2 } #[repr(u8)] @@ -23,7 +23,7 @@ pub struct PolicyAccount { /// Engine account that the policy belongs to pub policy_engine: Pubkey, /// Different policies that can be applied to the policy account - #[max_len(3)] // initial max_len + #[max_len(3)] // initial max_len. There is an issue with policy account deserialization for any initial length < 3. TODO: investigate and fix pub policies: Vec, } @@ -31,8 +31,8 @@ pub struct PolicyAccount { pub struct Policy { #[max_len(32)] pub hash: String, - pub policy_type: PolicyType, pub identity_filter: IdentityFilter, + pub policy_type: PolicyType, } #[derive(AnchorSerialize, AnchorDeserialize, Clone, InitSpace, PartialEq, Copy, Debug)] diff --git a/programs/policy_engine/src/utils.rs b/programs/policy_engine/src/utils.rs index 555bf41..ac23d94 100644 --- a/programs/policy_engine/src/utils.rs +++ b/programs/policy_engine/src/utils.rs @@ -60,6 +60,7 @@ pub struct Transfer { } /// enforces different types of policies +#[inline(never)] pub fn enforce_policy( policies: Vec, amount: u64, From cc2d71651740dfc77efa21b065b211cea05ad575 Mon Sep 17 00:00:00 2001 From: Bhargava Sai Macha Date: Mon, 20 May 2024 12:12:53 -0400 Subject: [PATCH 2/8] add more tests and update current ones (#56) * allow transfers for non policy attached assets and update tests * use self hosted * add more tests --- clients/rwa-token-sdk/package.json | 4 +- .../src/asset-controller/instructions.ts | 5 - .../src/policy-engine/instructions.ts | 1 - clients/rwa-token-sdk/src/utils/index.ts | 19 +- clients/rwa-token-sdk/tests/e2e.test.ts | 8 +- clients/rwa-token-sdk/tests/policies.test.ts | 350 ++++++++++++++++++ .../tests/policies/identity_approval.test.ts | 17 + .../policies/transaction_amount_limit.test.ts | 5 + .../transaction_amount_velocity.test.ts | 0 .../transaction_count_velocity.test.ts | 0 clients/rwa-token-sdk/tests/setup.ts | 23 +- clients/rwa-token-sdk/tests/tracker.test.ts | 232 ++++++++++++ clients/rwa-token-sdk/yarn.lock | 76 ++-- .../src/instructions/execute.rs | 49 +++ 14 files changed, 715 insertions(+), 74 deletions(-) create mode 100644 clients/rwa-token-sdk/tests/policies.test.ts create mode 100644 clients/rwa-token-sdk/tests/policies/identity_approval.test.ts create mode 100644 clients/rwa-token-sdk/tests/policies/transaction_amount_limit.test.ts create mode 100644 clients/rwa-token-sdk/tests/policies/transaction_amount_velocity.test.ts create mode 100644 clients/rwa-token-sdk/tests/policies/transaction_count_velocity.test.ts create mode 100644 clients/rwa-token-sdk/tests/tracker.test.ts diff --git a/clients/rwa-token-sdk/package.json b/clients/rwa-token-sdk/package.json index 72d61f7..23ff3d4 100644 --- a/clients/rwa-token-sdk/package.json +++ b/clients/rwa-token-sdk/package.json @@ -4,7 +4,7 @@ "description": "RWA Token SDK for the development of permissioned tokens on SVM blockchains.", "homepage": "https://github.com/bridgesplit/rwa-token#readme", "scripts": { - "test": "vitest run --testTimeout=120000", + "test": "vitest run --testTimeout=120000 ./tests/tracker.test.ts", "lint": "eslint . --ext .ts" }, "repository": { @@ -30,7 +30,7 @@ "eslint-config-xo-typescript": "^3.0.0", "typedoc": "^0.25.13", "typescript": ">=5.0.0", - "vitest": "^1.5.0" + "vitest": "^1.5.2" }, "author": "Standard Labs, Inc.", "contributors": [ diff --git a/clients/rwa-token-sdk/src/asset-controller/instructions.ts b/clients/rwa-token-sdk/src/asset-controller/instructions.ts index 1e4d94d..e6c4a1b 100644 --- a/clients/rwa-token-sdk/src/asset-controller/instructions.ts +++ b/clients/rwa-token-sdk/src/asset-controller/instructions.ts @@ -24,7 +24,6 @@ import { import { type CommonArgs, type IxReturn, - parseRemainingAccounts, } from "../utils"; import { ASSOCIATED_TOKEN_PROGRAM_ID, @@ -150,10 +149,7 @@ export type TransferTokensArgs = { from: string; to: string; amount: number; - authority: string; decimals: number; - /** Optional parameter for transfer controls (policies) and privacy (identity). */ - remainingAccounts?: string[]; } & CommonArgs; /** @@ -216,7 +212,6 @@ export async function getTransferTokensIx( isSigner: false, }, ]; - remainingAccounts.push(...parseRemainingAccounts(args.remainingAccounts)); const ix = createTransferCheckedInstruction( getAssociatedTokenAddressSync( new PublicKey(args.assetMint), diff --git a/clients/rwa-token-sdk/src/policy-engine/instructions.ts b/clients/rwa-token-sdk/src/policy-engine/instructions.ts index 2a26aae..12e6507 100644 --- a/clients/rwa-token-sdk/src/policy-engine/instructions.ts +++ b/clients/rwa-token-sdk/src/policy-engine/instructions.ts @@ -45,7 +45,6 @@ export async function getCreatePolicyEngineIx( /** Represents the arguments required to attach a policy to an asset. */ export type AttachPolicyArgs = { authority: string; - owner: string; assetMint: string; payer: string; identityFilter: IdentityFilter; diff --git a/clients/rwa-token-sdk/src/utils/index.ts b/clients/rwa-token-sdk/src/utils/index.ts index 37cce6c..1f95739 100644 --- a/clients/rwa-token-sdk/src/utils/index.ts +++ b/clients/rwa-token-sdk/src/utils/index.ts @@ -1,6 +1,6 @@ import { AnchorProvider } from "@coral-xyz/anchor"; import { - Connection, type Keypair, PublicKey, type TransactionInstruction, + Connection, type Keypair, type TransactionInstruction, } from "@solana/web3.js"; /** Retrieves the provider used for interacting with the Solana blockchain. @@ -21,23 +21,6 @@ export type IxReturn = { signers: Keypair[]; }; -/** - * Parses remaining accounts received from a transaction instruction. - * @param remainingAccounts - An optional array of strings representing account public keys. - * @returns An array of parsed account objects. - */ -export function parseRemainingAccounts(remainingAccounts?: string[]) { - if (!remainingAccounts) { - return []; - } - - return remainingAccounts.map(account => ({ - pubkey: new PublicKey(account), - isWritable: false, - isSigner: false, - })); -} - /** Common args for all RWA instructions */ export type CommonArgs = { assetMint: string; diff --git a/clients/rwa-token-sdk/tests/e2e.test.ts b/clients/rwa-token-sdk/tests/e2e.test.ts index 2a4044f..197587a 100644 --- a/clients/rwa-token-sdk/tests/e2e.test.ts +++ b/clients/rwa-token-sdk/tests/e2e.test.ts @@ -23,10 +23,10 @@ import { expect, test, describe } from "vitest"; import { type Config } from "../src/classes/types"; import { RwaClient } from "../src/classes"; -describe("e2e tests", () => { +describe("e2e tests", async () => { let rwaClient: RwaClient; let mint: string; - const setup = setupTests(); + const setup = await setupTests(); const decimals = 2; const remainingAccounts: string[] = []; @@ -138,7 +138,6 @@ describe("e2e tests", () => { test("create identity approval policy", async () => { const policyArgs: AttachPolicyArgs = { authority: setup.authority.toString(), - owner: setup.authority.toString(), assetMint: mint, payer: setup.payer.toString(), identityFilter: { @@ -163,7 +162,6 @@ describe("e2e tests", () => { test("attach transaction amount limit policy", async () => { const policyArgs: AttachPolicyArgs = { payer: setup.payer.toString(), - owner: setup.authority.toString(), assetMint: mint, authority: setup.authority.toString(), identityFilter: { @@ -189,7 +187,6 @@ describe("e2e tests", () => { test("attach transaction amount velocity policy", async () => { const policyArgs: AttachPolicyArgs = { payer: setup.payer.toString(), - owner: setup.authority.toString(), assetMint: mint, authority: setup.authority.toString(), identityFilter: { @@ -215,7 +212,6 @@ describe("e2e tests", () => { test("attach transaction count velocity policy", async () => { const policyArgs: AttachPolicyArgs = { payer: setup.payer.toString(), - owner: setup.authority.toString(), assetMint: mint, authority: setup.authority.toString(), identityFilter: { diff --git a/clients/rwa-token-sdk/tests/policies.test.ts b/clients/rwa-token-sdk/tests/policies.test.ts new file mode 100644 index 0000000..8f31beb --- /dev/null +++ b/clients/rwa-token-sdk/tests/policies.test.ts @@ -0,0 +1,350 @@ + +import { BN, Wallet } from "@coral-xyz/anchor"; +import { + getPolicyAccountPda, getPolicyEngineProgram, getTransferTokensIx, + RwaClient, +} from "../src"; +import { setupTests } from "./setup"; +import { ConfirmOptions, Connection, Transaction, sendAndConfirmTransaction } from "@solana/web3.js"; +import { expect, test, describe } from "vitest"; +import { Config } from "../src/classes/types"; + +describe("test policy setup", async () => { + let rwaClient: RwaClient; + let mint: string; + const setup = await setupTests(); + + const decimals = 2; + + test("setup provider", async () => { + const connectionUrl = process.env.RPC_URL ?? "http://localhost:8899"; + const connection = new Connection(connectionUrl); + + const confirmationOptions: ConfirmOptions = { + skipPreflight: false, + maxRetries: 3, + commitment: "processed", + }; + + const config: Config = { + connection, + rpcUrl: connectionUrl, + confirmationOptions, + }; + + rwaClient = new RwaClient(config, new Wallet(setup.payerKp)); + + await rwaClient.provider.connection.confirmTransaction( + await rwaClient.provider.connection.requestAirdrop( + setup.payerKp.publicKey, + 1000000000 + ) + ); + await rwaClient.provider.connection.confirmTransaction( + await rwaClient.provider.connection.requestAirdrop( + setup.authorityKp.publicKey, + 1000000000 + ) + ); + await rwaClient.provider.connection.confirmTransaction( + await rwaClient.provider.connection.requestAirdrop( + setup.delegateKp.publicKey, + 1000000000 + ) + ); + }); + + test("setup registries", async () => { + const createAssetControllerArgs = { + decimals, + payer: setup.payer.toString(), + authority: setup.authority.toString(), + name: "Test Asset", + uri: "https://test.com", + symbol: "TST", + }; + const setupAssetController = await rwaClient.assetController.setupNewRegistry( + createAssetControllerArgs + ); + const txnId = await sendAndConfirmTransaction(setup.provider.connection, new Transaction().add(...setupAssetController.ixs), [setup.payerKp, ...setupAssetController.signers]); + mint = setupAssetController.signers[0].publicKey.toString(); + expect(txnId).toBeTruthy(); + }); + + test("create policy account and attach identity approval policy", async () => { + const attachPolicy = await rwaClient.policyEngine.createPolicy({ + payer: setup.payer.toString(), + assetMint: mint, + authority: setup.authority.toString(), + identityFilter: { + identityLevels: [1, 2, 255], + comparisionType: { or: {} }, + }, + policyType: { + identityApproval: {}, + }, + }); + const txnId = await sendAndConfirmTransaction(setup.provider.connection, new Transaction().add(...attachPolicy.ixs), [setup.payerKp, ...attachPolicy.signers]); + expect(txnId).toBeTruthy(); + }); + + test("attach transaction amount limit policy to identity level 1", async () => { + const attachPolicy = await rwaClient.policyEngine.attachPolicy({ + payer: setup.payer.toString(), + assetMint: mint, + authority: setup.authority.toString(), + identityFilter: { + identityLevels: [1], // Going to skip other identity levels + comparisionType: { or: {} }, + }, + policyType: { + transactionAmountLimit: { + limit: new BN(100), + }, + }, + }); + const txnId = await sendAndConfirmTransaction(setup.provider.connection, new Transaction().add(...attachPolicy.ixs), [setup.payerKp, ...attachPolicy.signers]); + expect(txnId).toBeTruthy(); + }); + + test("attach transaction amount limit policy to identity level 2", async () => { + const attachPolicy = await rwaClient.policyEngine.attachPolicy({ + payer: setup.payer.toString(), + assetMint: mint, + authority: setup.authority.toString(), + identityFilter: { + identityLevels: [1], // Going to skip other identity levels + comparisionType: { or: {} }, + }, + policyType: { + transactionAmountLimit: { + limit: new BN(200000), + }, + }, + }); + const txnId = await sendAndConfirmTransaction(setup.provider.connection, new Transaction().add(...attachPolicy.ixs), [setup.payerKp, ...attachPolicy.signers]); + expect(txnId).toBeTruthy(); + }); + + test("attach transaction amount velocity policy to identity level 1", async () => { + const attachPolicy = await rwaClient.policyEngine.attachPolicy({ + payer: setup.payer.toString(), + assetMint: mint, + authority: setup.authority.toString(), + identityFilter: { + identityLevels: [1], // Going to skip other identity levels + comparisionType: { or: {} }, + }, + policyType: { + transactionAmountVelocity: { + limit: new BN(199), + timeframe: new BN(60), + }, + }, + }); + const txnId = await sendAndConfirmTransaction(setup.provider.connection, new Transaction().add(...attachPolicy.ixs), [setup.payerKp, ...attachPolicy.signers]); + expect(txnId).toBeTruthy(); + }); + + test("attach transaction count velocity policy to identity level 1", async () => { + const attachPolicy = await rwaClient.policyEngine.attachPolicy({ + payer: setup.payer.toString(), + assetMint: mint, + authority: setup.authority.toString(), + identityFilter: { + identityLevels: [1], // Going to skip other identity levels + comparisionType: { or: {} }, + }, + policyType: { + transactionCountVelocity: { + limit: new BN(2), + timeframe: new BN(300), + }, + }, + }); + const txnId = await sendAndConfirmTransaction(setup.provider.connection, new Transaction().add(...attachPolicy.ixs), [setup.payerKp, ...attachPolicy.signers]); + expect(txnId).toBeTruthy(); + }); + + test("attach transaction count velocity policy to identity level 2", async () => { + const attachPolicy = await rwaClient.policyEngine.attachPolicy({ + payer: setup.payer.toString(), + assetMint: mint, + authority: setup.authority.toString(), + identityFilter: { + identityLevels: [2], // Going to skip other identity levels + comparisionType: { or: {} }, + }, + policyType: { + transactionCountVelocity: { + limit: new BN(3), + timeframe: new BN(60), + }, + }, + }); + const txnId = await sendAndConfirmTransaction(setup.provider.connection, new Transaction().add(...attachPolicy.ixs), [setup.payerKp, ...attachPolicy.signers]); + expect(txnId).toBeTruthy(); + const policyAccount = await getPolicyEngineProgram(setup.provider).account.policyAccount.fetch(getPolicyAccountPda(mint)); + expect(policyAccount.policies.length).toBe(6); + }); + + test("setup user1", async () => { + const setupUser = await rwaClient.identityRegistry.setupUserIxns({ + payer: setup.payer.toString(), + owner: setup.user1.toString(), + assetMint: mint, + level: 1, + signer: setup.authorityKp.publicKey.toString() + }); + const txnId = await sendAndConfirmTransaction(setup.provider.connection, new Transaction().add(...setupUser.ixs), [setup.payerKp, ...setupUser.signers]); + expect(txnId).toBeTruthy(); + }); + + test("setup user2", async () => { + const setupUser = await rwaClient.identityRegistry.setupUserIxns({ + payer: setup.payer.toString(), + owner: setup.user2.toString(), + assetMint: mint, + level: 2, + signer: setup.authorityKp.publicKey.toString() + }); + const txnId = await sendAndConfirmTransaction(setup.provider.connection, new Transaction().add(...setupUser.ixs), [setup.payerKp, ...setupUser.signers]); + expect(txnId).toBeTruthy(); + }); + + test("setup user3", async () => { + const setupUser = await rwaClient.identityRegistry.setupUserIxns({ + payer: setup.payer.toString(), + owner: setup.user3.toString(), + assetMint: mint, + level: 255, // Skips all policies + signer: setup.authorityKp.publicKey.toString() + }); + const txnId = await sendAndConfirmTransaction(setup.provider.connection, new Transaction().add(...setupUser.ixs), [setup.payerKp, ...setupUser.signers]); + expect(txnId).toBeTruthy(); + }); + + test("issue tokens", async () => { + let issueTokens = await rwaClient.assetController.issueTokenIxns({ + authority: setup.authority.toString(), + payer: setup.payer.toString(), + owner: setup.user1.toString(), + assetMint: mint, + amount: 1000000, + }); + let txnId = await sendAndConfirmTransaction(setup.provider.connection, new Transaction().add(issueTokens), [setup.payerKp]); + expect(txnId).toBeTruthy(); + issueTokens = await rwaClient.assetController.issueTokenIxns({ + authority: setup.authority.toString(), + payer: setup.payer.toString(), + owner: setup.user2.toString(), + assetMint: mint, + amount: 1000000, + }); + txnId = await sendAndConfirmTransaction(setup.provider.connection, new Transaction().add(issueTokens), [setup.payerKp]); + expect(txnId).toBeTruthy(); + issueTokens = await rwaClient.assetController.issueTokenIxns({ + authority: setup.authority.toString(), + payer: setup.payer.toString(), + owner: setup.user3.toString(), + assetMint: mint, + amount: 1000000, + }); + txnId = await sendAndConfirmTransaction(setup.provider.connection, new Transaction().add(issueTokens), [setup.payerKp]); + expect(txnId).toBeTruthy(); + }); + + test("transfer 1000 tokens from user1, user2 and user3. fail for user1, success for others", async () => { + let transferTokensIx = await getTransferTokensIx({ + authority: setup.authority.toString(), + payer: setup.payer.toString(), + from: setup.user1.toString(), + to: setup.user2.toString(), + assetMint: mint, + amount: 1000, + decimals, + }); + void expect(sendAndConfirmTransaction( + setup.provider.connection, + new Transaction().add(transferTokensIx), + [setup.payerKp, setup.user1Kp], + )).rejects.toThrowError(); + transferTokensIx = await getTransferTokensIx({ + authority: setup.authority.toString(), + payer: setup.payer.toString(), + from: setup.user2.toString(), + to: setup.user3.toString(), + assetMint: mint, + amount: 1000, + decimals, + }); + let txnId = await sendAndConfirmTransaction( + setup.provider.connection, + new Transaction().add(transferTokensIx), + [setup.payerKp, setup.user2Kp], + ); + expect(txnId).toBeTruthy(); + transferTokensIx = await getTransferTokensIx({ + authority: setup.authority.toString(), + payer: setup.payer.toString(), + from: setup.user3.toString(), + to: setup.user1.toString(), + assetMint: mint, + amount: 1000, + decimals, + }); + txnId = await sendAndConfirmTransaction( + setup.provider.connection, + new Transaction().add(transferTokensIx), + [setup.payerKp, setup.user3Kp], + ); + expect(txnId).toBeTruthy(); + }); + + test("transfer 10 tokens 3 times from user1, fail 3rd time", async () => { + let transferTokensIx = await getTransferTokensIx({ + authority: setup.authority.toString(), + payer: setup.payer.toString(), + from: setup.user1.toString(), + to: setup.user2.toString(), + assetMint: mint, + amount: 10, + decimals, + }); + let txnId = await sendAndConfirmTransaction( + setup.provider.connection, + new Transaction().add(transferTokensIx), + [setup.payerKp, setup.user1Kp], + ); + expect(txnId).toBeTruthy(); + transferTokensIx = await getTransferTokensIx({ + authority: setup.authority.toString(), + payer: setup.payer.toString(), + from: setup.user1.toString(), + to: setup.user2.toString(), + assetMint: mint, + amount: 10, + decimals, + }); + txnId = await sendAndConfirmTransaction( + setup.provider.connection, + new Transaction().add(transferTokensIx), + [setup.payerKp, setup.user1Kp], + ); + expect(txnId).toBeTruthy(); + transferTokensIx = await getTransferTokensIx({ + authority: setup.authority.toString(), + payer: setup.payer.toString(), + from: setup.user1.toString(), + to: setup.user2.toString(), + assetMint: mint, + amount: 10, + decimals, + }); + void expect(sendAndConfirmTransaction( + setup.provider.connection, + new Transaction().add(transferTokensIx), + [setup.payerKp, setup.user1Kp], + )).rejects.toThrowError(); + }); +}); \ No newline at end of file diff --git a/clients/rwa-token-sdk/tests/policies/identity_approval.test.ts b/clients/rwa-token-sdk/tests/policies/identity_approval.test.ts new file mode 100644 index 0000000..0a9cf9a --- /dev/null +++ b/clients/rwa-token-sdk/tests/policies/identity_approval.test.ts @@ -0,0 +1,17 @@ + +import { BN, Wallet } from "@coral-xyz/anchor"; +import { + getPolicyAccountPda, getPolicyEngineProgram, getTransferTokensIx, + RwaClient, +} from ".././src"; +import { setupTests } from "../setup"; +import { ConfirmOptions, Connection, Transaction, sendAndConfirmTransaction } from "@solana/web3.js"; +import { expect, test, describe } from "vitest"; +import { Config } from "../../src/classes/types"; + + +// setup identity approval policy and check all values match +// update identity approval policy and check all values match +// check identity approval policy logic is being enforced for users +// check identity approval policy isnt being enforced for users with skip level +// check identity approval policy can be removed and no data is being left behind \ No newline at end of file diff --git a/clients/rwa-token-sdk/tests/policies/transaction_amount_limit.test.ts b/clients/rwa-token-sdk/tests/policies/transaction_amount_limit.test.ts new file mode 100644 index 0000000..9b76179 --- /dev/null +++ b/clients/rwa-token-sdk/tests/policies/transaction_amount_limit.test.ts @@ -0,0 +1,5 @@ +// setup txn amount limit policy and check all values match +// update txn amount limit policy policy and check all values match +// get user to max amount that policy enforce +// check txn amount limit policy isnt being enforced for users with skip level +// check txn amount limit policy can be removed and no data is being left behind \ No newline at end of file diff --git a/clients/rwa-token-sdk/tests/policies/transaction_amount_velocity.test.ts b/clients/rwa-token-sdk/tests/policies/transaction_amount_velocity.test.ts new file mode 100644 index 0000000..e69de29 diff --git a/clients/rwa-token-sdk/tests/policies/transaction_count_velocity.test.ts b/clients/rwa-token-sdk/tests/policies/transaction_count_velocity.test.ts new file mode 100644 index 0000000..e69de29 diff --git a/clients/rwa-token-sdk/tests/setup.ts b/clients/rwa-token-sdk/tests/setup.ts index c551b9c..365ad66 100644 --- a/clients/rwa-token-sdk/tests/setup.ts +++ b/clients/rwa-token-sdk/tests/setup.ts @@ -1,15 +1,30 @@ -import { Keypair } from "@solana/web3.js"; -import { getProvider } from "../src/utils"; +import { getProvider } from "@coral-xyz/anchor"; +import { Keypair, LAMPORTS_PER_SOL } from "@solana/web3.js"; import "dotenv/config"; -export function setupTests() { +export async function setupTests() { const payerKp = new Keypair(); const authorityKp = payerKp; const delegateKp = authorityKp; - const provider = getProvider(); const user1Kp = new Keypair(); const user2Kp = new Keypair(); const user3Kp = new Keypair(); + const provider = getProvider(); + + + // airdrop to all users + const txns = await Promise.all([ + provider.connection.requestAirdrop(payerKp.publicKey, LAMPORTS_PER_SOL), + provider.connection.requestAirdrop(authorityKp.publicKey, LAMPORTS_PER_SOL), + provider.connection.requestAirdrop(delegateKp.publicKey, LAMPORTS_PER_SOL), + provider.connection.requestAirdrop(user1Kp.publicKey, LAMPORTS_PER_SOL), + provider.connection.requestAirdrop(user2Kp.publicKey, LAMPORTS_PER_SOL), + provider.connection.requestAirdrop(user3Kp.publicKey, LAMPORTS_PER_SOL), + ]); + + await Promise.all(txns.map((txn) => provider.connection.confirmTransaction(txn, "finalized"))); + + return { payerKp, payer: payerKp.publicKey, diff --git a/clients/rwa-token-sdk/tests/tracker.test.ts b/clients/rwa-token-sdk/tests/tracker.test.ts new file mode 100644 index 0000000..074daf5 --- /dev/null +++ b/clients/rwa-token-sdk/tests/tracker.test.ts @@ -0,0 +1,232 @@ +import { BN, Wallet } from "@coral-xyz/anchor"; +import { + type AttachPolicyArgs, + type CreateDataAccountArgs, + getPolicyAccountPda, + getTrackerAccount, + getTrackerAccountPda, + type IssueTokenArgs, + type SetupUserArgs, + type TransferTokensArgs, + type UpdateDataAccountArgs, + type VoidTokensArgs, +} from "../src"; +import { setupTests } from "./setup"; +import { + type ConfirmOptions, + Connection, + Transaction, + sendAndConfirmTransaction, +} from "@solana/web3.js"; +import { expect, test, describe } from "vitest"; +import { type Config } from "../src/classes/types"; +import { RwaClient } from "../src/classes"; + +describe("test suite to test tracker account is being updated correctly on transfers, data is correctly being stored and discarded and to test the limit of transfers that can be tracked", async () => { + let rwaClient: RwaClient; + let mint: string; + const setup = await setupTests(); + const decimals = 9; + + test("setup provider", async () => { + const connectionUrl = process.env.RPC_URL ?? "http://localhost:8899"; + const connection = new Connection(connectionUrl); + + const confirmationOptions: ConfirmOptions = { + skipPreflight: false, + maxRetries: 3, + }; + + const config: Config = { + connection, + rpcUrl: connectionUrl, + confirmationOptions, + }; + + rwaClient = new RwaClient(config, new Wallet(setup.payerKp)); + }); + + test("initalize asset controller", async () => { + const setupAssetControllerArgs = { + decimals, + payer: setup.payer.toString(), + authority: setup.authority.toString(), + name: "Test Class Asset", + uri: "https://test.com", + symbol: "TFT", + }; + + const setupIx = await rwaClient.assetController.setupNewRegistry( + setupAssetControllerArgs + ); + const txnId = await sendAndConfirmTransaction( + rwaClient.provider.connection, + new Transaction().add(...setupIx.ixs), + [setup.payerKp, ...setupIx.signers] + ); + mint = setupIx.signers[0].publicKey.toString(); + expect(txnId).toBeTruthy(); + }); + + test("setup user1 and user2", async () => { + const setupUser1Args: SetupUserArgs = { + payer: setup.payer.toString(), + owner: setup.user1.toString(), + signer: setup.authority.toString(), + assetMint: mint, + level: 1, + }; + const setupIx1 = await rwaClient.identityRegistry.setupUserIxns( + setupUser1Args + ); + const txnId1 = await sendAndConfirmTransaction( + rwaClient.provider.connection, + new Transaction().add(...setupIx1.ixs), + [setup.payerKp, setup.authorityKp] + ); + expect(txnId1).toBeTruthy(); + const setupUser2Args: SetupUserArgs = { + payer: setup.payer.toString(), + owner: setup.user2.toString(), + signer: setup.authority.toString(), + assetMint: mint, + level: 1, + }; + const setupIx2 = await rwaClient.identityRegistry.setupUserIxns( + setupUser2Args + ); + const txnId2 = await sendAndConfirmTransaction( + rwaClient.provider.connection, + new Transaction().add(...setupIx2.ixs), + [setup.payerKp, setup.authorityKp] + ); + expect(txnId2).toBeTruthy(); + const trackerAccount1 = await getTrackerAccount( + mint, + setup.user1.toString(), + rwaClient.provider + ); + expect(trackerAccount1).toBeTruthy(); + expect(trackerAccount1!.assetMint.toString()).toBe(mint); + expect(trackerAccount1!.owner.toString()).toBe(setup.user1.toString()); + const trackerAccount2 = await getTrackerAccount( + mint, + setup.user2.toString(), + rwaClient.provider + ); + expect(trackerAccount2).toBeTruthy(); + expect(trackerAccount2!.assetMint.toString()).toBe(mint); + }); + + test("issue tokens", async () => { + const issueArgs: IssueTokenArgs = { + authority: setup.authority.toString(), + payer: setup.payer.toString(), + owner: setup.user1.toString(), + assetMint: mint, + amount: 1000000, + }; + const issueIx = await rwaClient.assetController.issueTokenIxns(issueArgs); + const txnId = await sendAndConfirmTransaction( + rwaClient.provider.connection, + new Transaction().add(issueIx), + [setup.payerKp, setup.authorityKp] + ); + expect(txnId).toBeTruthy(); + console.log("issue tokens signature: ", txnId); + }); + + test("transfer tokens", async () => { + const transferArgs: TransferTokensArgs = { + payer: setup.payer.toString(), + from: setup.user1.toString(), + to: setup.user2.toString(), + assetMint: mint, + amount: 100, + decimals, + }; + + const transferIx = await rwaClient.assetController.transfer(transferArgs); + const txnId = await sendAndConfirmTransaction( + rwaClient.provider.connection, + new Transaction().add(transferIx), + [setup.payerKp, setup.user1Kp] + ); + expect(txnId).toBeTruthy(); + const trackerAccount = await getTrackerAccount( + mint, + setup.user1.toString(), + rwaClient.provider + ); + // length of transfers should be 0 since any policies haven;t beeen attached uet + expect(trackerAccount!.transfers.length).toBe(0); + }); + + test("attach transfer amount limit policy", async () => { + const attachPolicyArgs: AttachPolicyArgs = { + payer: setup.payer.toString(), + assetMint: mint, + authority: setup.authority.toString(), + identityFilter: { + identityLevels: [1], + comparisionType: {or: {}} + }, + policyType: {transactionAmountVelocity: { limit: new BN(1000000000000), timeframe: new BN(1000000000000) }} // enough limit and timeframe to allow a lot of transfers + }; + const attachPolicyIx = await rwaClient.policyEngine.createPolicy( + attachPolicyArgs + ); + const txnId = await sendAndConfirmTransaction( + rwaClient.provider.connection, + new Transaction().add(...attachPolicyIx.ixs), + [setup.payerKp, setup.authorityKp] + ); + expect(txnId).toBeTruthy(); + }); + + test("do 25 transfers, fail for the 26th time because transfer history is full", async () => { + for(let i = 0; i < 25; i++) { + const transferArgs: TransferTokensArgs = { + payer: setup.payer.toString(), + from: setup.user1.toString(), + to: setup.user2.toString(), + assetMint: mint, + amount: 100, + decimals, + }; + + const transferIx = await rwaClient.assetController.transfer(transferArgs); + const txnId = await sendAndConfirmTransaction( + rwaClient.provider.connection, + new Transaction().add(transferIx), + [setup.payerKp, setup.user1Kp] + ); + expect(txnId).toBeTruthy(); + const trackerAccount = await getTrackerAccount( + mint, + setup.user1.toString(), + rwaClient.provider + ); + // length of transfers should be 0 since any policies haven;t beeen attached uet + expect(trackerAccount!.transfers.length).toBe(i); + expect(trackerAccount!.transfers.at(i)?.amount == 100); + } + const transferArgs: TransferTokensArgs = { + payer: setup.payer.toString(), + from: setup.user1.toString(), + to: setup.user2.toString(), + assetMint: mint, + amount: 100, + decimals, + }; + + const transferIx = await rwaClient.assetController.transfer(transferArgs); + expect(sendAndConfirmTransaction( + rwaClient.provider.connection, + new Transaction().add(transferIx), + [setup.payerKp, setup.user1Kp] + )).toThrow("hello"); + + }); + +}); diff --git a/clients/rwa-token-sdk/yarn.lock b/clients/rwa-token-sdk/yarn.lock index fa16618..609b04e 100644 --- a/clients/rwa-token-sdk/yarn.lock +++ b/clients/rwa-token-sdk/yarn.lock @@ -584,44 +584,44 @@ resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== -"@vitest/expect@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-1.5.0.tgz#961190510a2723bd4abf5540bcec0a4dfd59ef14" - integrity sha512-0pzuCI6KYi2SIC3LQezmxujU9RK/vwC1U9R0rLuGlNGcOuDWxqWKu6nUdFsX9tH1WU0SXtAxToOsEjeUn1s3hA== +"@vitest/expect@1.5.2": + version "1.5.2" + resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-1.5.2.tgz#04d1c0c94ca264e32fe43f564b04528f352a6083" + integrity sha512-rf7MTD1WCoDlN3FfYJ9Llfp0PbdtOMZ3FIF0AVkDnKbp3oiMW1c8AmvRZBcqbAhDUAvF52e9zx4WQM1r3oraVA== dependencies: - "@vitest/spy" "1.5.0" - "@vitest/utils" "1.5.0" + "@vitest/spy" "1.5.2" + "@vitest/utils" "1.5.2" chai "^4.3.10" -"@vitest/runner@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-1.5.0.tgz#1f7cb78ee4064e73e53d503a19c1b211c03dfe0c" - integrity sha512-7HWwdxXP5yDoe7DTpbif9l6ZmDwCzcSIK38kTSIt6CFEpMjX4EpCgT6wUmS0xTXqMI6E/ONmfgRKmaujpabjZQ== +"@vitest/runner@1.5.2": + version "1.5.2" + resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-1.5.2.tgz#acc9677aaca5c548e3a2746d97eb443c687f0d6f" + integrity sha512-7IJ7sJhMZrqx7HIEpv3WrMYcq8ZNz9L6alo81Y6f8hV5mIE6yVZsFoivLZmr0D777klm1ReqonE9LyChdcmw6g== dependencies: - "@vitest/utils" "1.5.0" + "@vitest/utils" "1.5.2" p-limit "^5.0.0" pathe "^1.1.1" -"@vitest/snapshot@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-1.5.0.tgz#cd2d611fd556968ce8fb6b356a09b4593c525947" - integrity sha512-qpv3fSEuNrhAO3FpH6YYRdaECnnRjg9VxbhdtPwPRnzSfHVXnNzzrpX4cJxqiwgRMo7uRMWDFBlsBq4Cr+rO3A== +"@vitest/snapshot@1.5.2": + version "1.5.2" + resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-1.5.2.tgz#d6f8a5d0da451e1c4dc211fcede600becf4851ed" + integrity sha512-CTEp/lTYos8fuCc9+Z55Ga5NVPKUgExritjF5VY7heRFUfheoAqBneUlvXSUJHUZPjnPmyZA96yLRJDP1QATFQ== dependencies: magic-string "^0.30.5" pathe "^1.1.1" pretty-format "^29.7.0" -"@vitest/spy@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-1.5.0.tgz#1369a1bec47f46f18eccfa45f1e8fbb9b5e15e77" - integrity sha512-vu6vi6ew5N5MMHJjD5PoakMRKYdmIrNJmyfkhRpQt5d9Ewhw9nZ5Aqynbi3N61bvk9UvZ5UysMT6ayIrZ8GA9w== +"@vitest/spy@1.5.2": + version "1.5.2" + resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-1.5.2.tgz#6b439a933b64522edbb8da878fa5b5b6361140ef" + integrity sha512-xCcPvI8JpCtgikT9nLpHPL1/81AYqZy1GCy4+MCHBE7xi8jgsYkULpW5hrx5PGLgOQjUpb6fd15lqcriJ40tfQ== dependencies: tinyspy "^2.2.0" -"@vitest/utils@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-1.5.0.tgz#90c9951f4516f6d595da24876b58e615f6c99863" - integrity sha512-BDU0GNL8MWkRkSRdNFvCUCAVOeHaUlVJ9Tx0TYBZyXaaOTmGtUFObzchCivIBrIwKzvZA7A9sCejVhXM2aY98A== +"@vitest/utils@1.5.2": + version "1.5.2" + resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-1.5.2.tgz#6a314daa8400a242b5509908cd8977a7bd66ef65" + integrity sha512-sWOmyofuXLJ85VvXNsroZur7mOJGiQeM0JN3/0D1uU8U9bGFM69X1iqHaRXl6R8BwaLY6yPCogP257zxTzkUdA== dependencies: diff-sequences "^29.6.3" estree-walker "^3.0.3" @@ -2043,10 +2043,10 @@ uuid@^8.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== -vite-node@1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-1.5.0.tgz#7f74dadfecb15bca016c5ce5ef85e5cc4b82abf2" - integrity sha512-tV8h6gMj6vPzVCa7l+VGq9lwoJjW8Y79vst8QZZGiuRAfijU+EEWuc0kFpmndQrWhMMhet1jdSF+40KSZUqIIw== +vite-node@1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-1.5.2.tgz#9e5fb28bd8bc68fe36e94f9156c3ae67796c002a" + integrity sha512-Y8p91kz9zU+bWtF7HGt6DVw2JbhyuB2RlZix3FPYAYmUyZ3n7iTp8eSyLyY6sxtPegvxQtmlTMhfPhUfCUF93A== dependencies: cac "^6.7.14" debug "^4.3.4" @@ -2065,16 +2065,16 @@ vite@^5.0.0: optionalDependencies: fsevents "~2.3.3" -vitest@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/vitest/-/vitest-1.5.0.tgz#6ebb396bd358650011a9c96c18fa614b668365c1" - integrity sha512-d8UKgR0m2kjdxDWX6911uwxout6GHS0XaGH1cksSIVVG8kRlE7G7aBw7myKQCvDI5dT4j7ZMa+l706BIORMDLw== - dependencies: - "@vitest/expect" "1.5.0" - "@vitest/runner" "1.5.0" - "@vitest/snapshot" "1.5.0" - "@vitest/spy" "1.5.0" - "@vitest/utils" "1.5.0" +vitest@^1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/vitest/-/vitest-1.5.2.tgz#bec4f413de40257d6be76183980273f6411068d0" + integrity sha512-l9gwIkq16ug3xY7BxHwcBQovLZG75zZL0PlsiYQbf76Rz6QGs54416UWMtC0jXeihvHvcHrf2ROEjkQRVpoZYw== + dependencies: + "@vitest/expect" "1.5.2" + "@vitest/runner" "1.5.2" + "@vitest/snapshot" "1.5.2" + "@vitest/spy" "1.5.2" + "@vitest/utils" "1.5.2" acorn-walk "^8.3.2" chai "^4.3.10" debug "^4.3.4" @@ -2088,7 +2088,7 @@ vitest@^1.5.0: tinybench "^2.5.1" tinypool "^0.8.3" vite "^5.0.0" - vite-node "1.5.0" + vite-node "1.5.2" why-is-node-running "^2.2.2" vscode-oniguruma@^1.7.0: diff --git a/programs/asset_controller/src/instructions/execute.rs b/programs/asset_controller/src/instructions/execute.rs index 5ed77d0..f0e6f81 100644 --- a/programs/asset_controller/src/instructions/execute.rs +++ b/programs/asset_controller/src/instructions/execute.rs @@ -1,12 +1,20 @@ +<<<<<<< macha/update_tests +use anchor_lang::prelude::*; +======= use anchor_lang::{ prelude::*, solana_program::sysvar::{self}, }; +>>>>>>> macha/security-fixes use anchor_spl::token_interface::{Mint, TokenAccount}; use identity_registry::{program::IdentityRegistry, IdentityAccount, SKIP_POLICY_LEVEL}; use policy_engine::{enforce_policy, program::PolicyEngine, PolicyAccount, PolicyEngineAccount}; +<<<<<<< macha/update_tests +use crate::{state::*, verify_pda}; +======= use crate::{state::*, verify_cpi_program_is_token22, verify_pda}; +>>>>>>> macha/security-fixes #[derive(Accounts)] #[instruction(amount: u64)] @@ -57,6 +65,14 @@ pub struct ExecuteTransferHook<'info> { #[account()] /// CHECK: internal ix checks pub policy_account: UncheckedAccount<'info>, +<<<<<<< macha/update_tests +} + +pub fn handler(ctx: Context, amount: u64) -> Result<()> { + let asset_mint = ctx.accounts.asset_mint.key(); + + msg!("verifying policy engine account pda"); +======= #[account(constraint = instructions_program.key() == sysvar::instructions::id())] /// CHECK: constraint check pub instructions_program: UncheckedAccount<'info>, @@ -66,6 +82,7 @@ pub fn handler(ctx: Context, amount: u64) -> Result<()> { verify_cpi_program_is_token22(&ctx.accounts.instructions_program.to_account_info(), amount)?; let asset_mint = ctx.accounts.asset_mint.key(); +>>>>>>> macha/security-fixes verify_pda( ctx.accounts.policy_engine_account.key(), @@ -73,6 +90,11 @@ pub fn handler(ctx: Context, amount: u64) -> Result<()> { &policy_engine::id(), )?; +<<<<<<< macha/update_tests + msg!("verifying policy account pda"); + +======= +>>>>>>> macha/security-fixes verify_pda( ctx.accounts.policy_account.key(), &[&ctx.accounts.policy_engine_account.key().to_bytes()], @@ -84,6 +106,14 @@ pub fn handler(ctx: Context, amount: u64) -> Result<()> { return Ok(()); } +<<<<<<< macha/update_tests + let policy_engine_account = PolicyEngineAccount::deserialize( + &mut &ctx.accounts.policy_engine_account.data.borrow_mut()[8..], + )?; + + let policy_account = + PolicyAccount::deserialize(&mut &ctx.accounts.policy_account.data.borrow_mut()[8..])?; +======= let policy_engine_account = Box::new(PolicyEngineAccount::deserialize( &mut &ctx.accounts.policy_engine_account.data.borrow_mut()[8..], )?); @@ -91,18 +121,29 @@ pub fn handler(ctx: Context, amount: u64) -> Result<()> { let policy_account = Box::new(PolicyAccount::deserialize( &mut &ctx.accounts.policy_account.data.borrow_mut()[8..], )?); +>>>>>>> macha/security-fixes // go through with transfer if there aren't any policies attached if policy_account.policies.is_empty() { return Ok(()); } +<<<<<<< macha/update_tests + msg!("verifying identity registry account pda"); + +======= +>>>>>>> macha/security-fixes // user must have identity account setup if there are policies attached verify_pda( ctx.accounts.identity_registry_account.key(), &[&asset_mint.to_bytes()], &identity_registry::id(), )?; +<<<<<<< macha/update_tests + + msg!("verifying identity account pda"); +======= +>>>>>>> macha/security-fixes verify_pda( ctx.accounts.identity_account.key(), &[ @@ -112,9 +153,17 @@ pub fn handler(ctx: Context, amount: u64) -> Result<()> { &identity_registry::id(), )?; +<<<<<<< macha/update_tests + let identity_account = IdentityAccount::deserialize( + &mut &ctx.accounts.identity_registry_account.data.borrow_mut()[8..], + )?; + + msg!("enforcing policy"); +======= let identity_account = Box::new(IdentityAccount::deserialize( &mut &ctx.accounts.identity_registry_account.data.borrow_mut()[8..], )?); +>>>>>>> macha/security-fixes // if user has identity skip level, skip enforcing policy if identity_account.levels.contains(&SKIP_POLICY_LEVEL) { From e1e339c654850200af7b263d6a370f259c155b4d Mon Sep 17 00:00:00 2001 From: Bhargava Sai Macha Date: Mon, 20 May 2024 15:54:19 -0400 Subject: [PATCH 3/8] update tests --- clients/rwa-token-sdk/package.json | 2 +- clients/rwa-token-sdk/tests/e2e.test.ts | 4 +- clients/rwa-token-sdk/tests/policies.test.ts | 19 ------ .../src/instructions/execute.rs | 58 ++----------------- programs/asset_controller/src/utils.rs | 11 ++-- programs/policy_engine/src/state/account.rs | 5 +- programs/policy_engine/src/state/engine.rs | 2 +- 7 files changed, 17 insertions(+), 84 deletions(-) diff --git a/clients/rwa-token-sdk/package.json b/clients/rwa-token-sdk/package.json index 23ff3d4..700b619 100644 --- a/clients/rwa-token-sdk/package.json +++ b/clients/rwa-token-sdk/package.json @@ -4,7 +4,7 @@ "description": "RWA Token SDK for the development of permissioned tokens on SVM blockchains.", "homepage": "https://github.com/bridgesplit/rwa-token#readme", "scripts": { - "test": "vitest run --testTimeout=120000 ./tests/tracker.test.ts", + "test": "vitest run --testTimeout=120000", "lint": "eslint . --ext .ts" }, "repository": { diff --git a/clients/rwa-token-sdk/tests/e2e.test.ts b/clients/rwa-token-sdk/tests/e2e.test.ts index 197587a..90c23e3 100644 --- a/clients/rwa-token-sdk/tests/e2e.test.ts +++ b/clients/rwa-token-sdk/tests/e2e.test.ts @@ -29,7 +29,6 @@ describe("e2e tests", async () => { const setup = await setupTests(); const decimals = 2; - const remainingAccounts: string[] = []; let dataAccount: string; test("setup provider", async () => { @@ -309,7 +308,7 @@ describe("e2e tests", async () => { test("update data account", async () => { const updateDataAccountArgs: UpdateDataAccountArgs = { dataAccount, - name: "Example Token Updatse", + name: "Example Token Updates", uri: "newUri", type: { tax: {} }, payer: setup.payer.toString(), @@ -336,7 +335,6 @@ describe("e2e tests", async () => { to: setup.authority.toString(), assetMint: mint, amount: 2000, - remainingAccounts, decimals, }; diff --git a/clients/rwa-token-sdk/tests/policies.test.ts b/clients/rwa-token-sdk/tests/policies.test.ts index 8f31beb..672d44e 100644 --- a/clients/rwa-token-sdk/tests/policies.test.ts +++ b/clients/rwa-token-sdk/tests/policies.test.ts @@ -33,25 +33,6 @@ describe("test policy setup", async () => { }; rwaClient = new RwaClient(config, new Wallet(setup.payerKp)); - - await rwaClient.provider.connection.confirmTransaction( - await rwaClient.provider.connection.requestAirdrop( - setup.payerKp.publicKey, - 1000000000 - ) - ); - await rwaClient.provider.connection.confirmTransaction( - await rwaClient.provider.connection.requestAirdrop( - setup.authorityKp.publicKey, - 1000000000 - ) - ); - await rwaClient.provider.connection.confirmTransaction( - await rwaClient.provider.connection.requestAirdrop( - setup.delegateKp.publicKey, - 1000000000 - ) - ); }); test("setup registries", async () => { diff --git a/programs/asset_controller/src/instructions/execute.rs b/programs/asset_controller/src/instructions/execute.rs index f0e6f81..89c34b7 100644 --- a/programs/asset_controller/src/instructions/execute.rs +++ b/programs/asset_controller/src/instructions/execute.rs @@ -1,20 +1,12 @@ -<<<<<<< macha/update_tests -use anchor_lang::prelude::*; -======= use anchor_lang::{ prelude::*, solana_program::sysvar::{self}, }; ->>>>>>> macha/security-fixes use anchor_spl::token_interface::{Mint, TokenAccount}; use identity_registry::{program::IdentityRegistry, IdentityAccount, SKIP_POLICY_LEVEL}; use policy_engine::{enforce_policy, program::PolicyEngine, PolicyAccount, PolicyEngineAccount}; -<<<<<<< macha/update_tests -use crate::{state::*, verify_pda}; -======= use crate::{state::*, verify_cpi_program_is_token22, verify_pda}; ->>>>>>> macha/security-fixes #[derive(Accounts)] #[instruction(amount: u64)] @@ -65,14 +57,6 @@ pub struct ExecuteTransferHook<'info> { #[account()] /// CHECK: internal ix checks pub policy_account: UncheckedAccount<'info>, -<<<<<<< macha/update_tests -} - -pub fn handler(ctx: Context, amount: u64) -> Result<()> { - let asset_mint = ctx.accounts.asset_mint.key(); - - msg!("verifying policy engine account pda"); -======= #[account(constraint = instructions_program.key() == sysvar::instructions::id())] /// CHECK: constraint check pub instructions_program: UncheckedAccount<'info>, @@ -82,7 +66,6 @@ pub fn handler(ctx: Context, amount: u64) -> Result<()> { verify_cpi_program_is_token22(&ctx.accounts.instructions_program.to_account_info(), amount)?; let asset_mint = ctx.accounts.asset_mint.key(); ->>>>>>> macha/security-fixes verify_pda( ctx.accounts.policy_engine_account.key(), @@ -90,11 +73,6 @@ pub fn handler(ctx: Context, amount: u64) -> Result<()> { &policy_engine::id(), )?; -<<<<<<< macha/update_tests - msg!("verifying policy account pda"); - -======= ->>>>>>> macha/security-fixes verify_pda( ctx.accounts.policy_account.key(), &[&ctx.accounts.policy_engine_account.key().to_bytes()], @@ -106,44 +84,25 @@ pub fn handler(ctx: Context, amount: u64) -> Result<()> { return Ok(()); } -<<<<<<< macha/update_tests let policy_engine_account = PolicyEngineAccount::deserialize( - &mut &ctx.accounts.policy_engine_account.data.borrow_mut()[8..], + &mut &ctx.accounts.policy_engine_account.data.borrow()[8..], )?; let policy_account = - PolicyAccount::deserialize(&mut &ctx.accounts.policy_account.data.borrow_mut()[8..])?; -======= - let policy_engine_account = Box::new(PolicyEngineAccount::deserialize( - &mut &ctx.accounts.policy_engine_account.data.borrow_mut()[8..], - )?); - - let policy_account = Box::new(PolicyAccount::deserialize( - &mut &ctx.accounts.policy_account.data.borrow_mut()[8..], - )?); ->>>>>>> macha/security-fixes + PolicyAccount::deserialize(&mut &ctx.accounts.policy_account.data.borrow()[8..])?; // go through with transfer if there aren't any policies attached if policy_account.policies.is_empty() { return Ok(()); } -<<<<<<< macha/update_tests - msg!("verifying identity registry account pda"); - -======= ->>>>>>> macha/security-fixes // user must have identity account setup if there are policies attached verify_pda( ctx.accounts.identity_registry_account.key(), &[&asset_mint.to_bytes()], &identity_registry::id(), )?; -<<<<<<< macha/update_tests - msg!("verifying identity account pda"); -======= ->>>>>>> macha/security-fixes verify_pda( ctx.accounts.identity_account.key(), &[ @@ -153,17 +112,8 @@ pub fn handler(ctx: Context, amount: u64) -> Result<()> { &identity_registry::id(), )?; -<<<<<<< macha/update_tests - let identity_account = IdentityAccount::deserialize( - &mut &ctx.accounts.identity_registry_account.data.borrow_mut()[8..], - )?; - - msg!("enforcing policy"); -======= - let identity_account = Box::new(IdentityAccount::deserialize( - &mut &ctx.accounts.identity_registry_account.data.borrow_mut()[8..], - )?); ->>>>>>> macha/security-fixes + let identity_account = + IdentityAccount::deserialize(&mut &ctx.accounts.identity_account.data.borrow()[8..])?; // if user has identity skip level, skip enforcing policy if identity_account.levels.contains(&SKIP_POLICY_LEVEL) { diff --git a/programs/asset_controller/src/utils.rs b/programs/asset_controller/src/utils.rs index a66da77..8679091 100644 --- a/programs/asset_controller/src/utils.rs +++ b/programs/asset_controller/src/utils.rs @@ -1,8 +1,10 @@ use anchor_lang::{ prelude::Result, solana_program::{ - program::invoke, pubkey::Pubkey, system_instruction::transfer, - sysvar::instructions::get_instruction_relative, + program::invoke, + pubkey::Pubkey, + system_instruction::transfer, + sysvar::{self, instructions::get_instruction_relative}, }, Lamports, }; @@ -57,6 +59,8 @@ pub fn get_extra_account_metas() -> Result> { false, false, )?, + // instructions program + ExtraAccountMeta::new_with_pubkey(&sysvar::instructions::id(), false, false)?, ]) } @@ -96,8 +100,7 @@ pub fn verify_cpi_program_is_token22( if ix_relative.program_id != token_2022::ID { return Err(AssetControllerErrors::InvalidCpiTransferProgram.into()); } - - if ix_relative.data != amount.to_le_bytes() { + if ix_relative.data[1..9] != amount.to_le_bytes() { return Err(AssetControllerErrors::InvalidCpiTransferAmount.into()); } diff --git a/programs/policy_engine/src/state/account.rs b/programs/policy_engine/src/state/account.rs index 7492007..47c6458 100644 --- a/programs/policy_engine/src/state/account.rs +++ b/programs/policy_engine/src/state/account.rs @@ -23,13 +23,14 @@ pub struct PolicyAccount { /// Engine account that the policy belongs to pub policy_engine: Pubkey, /// Different policies that can be applied to the policy account - #[max_len(3)] // initial max_len. There is an issue with policy account deserialization for any initial length < 3. TODO: investigate and fix + #[max_len(1)] + /// initial max len pub policies: Vec, } #[derive(AnchorSerialize, AnchorDeserialize, Clone, InitSpace)] pub struct Policy { - #[max_len(32)] + #[max_len(64)] pub hash: String, pub identity_filter: IdentityFilter, pub policy_type: PolicyType, diff --git a/programs/policy_engine/src/state/engine.rs b/programs/policy_engine/src/state/engine.rs index 96c44e3..52d842e 100644 --- a/programs/policy_engine/src/state/engine.rs +++ b/programs/policy_engine/src/state/engine.rs @@ -3,7 +3,7 @@ use anchor_lang::{prelude::*, AnchorSerialize}; use crate::PolicyType; #[account()] -#[derive(InitSpace)] +#[derive(InitSpace, Copy)] pub struct PolicyEngineAccount { /// version pub version: u8, From a78c80faf6b482a261f517401edf9328f2902cdc Mon Sep 17 00:00:00 2001 From: Luke Truitt <38018183+luke-truitt@users.noreply.github.com> Date: Mon, 20 May 2024 17:01:18 -0400 Subject: [PATCH 4/8] Update instructions.ts --- clients/rwa-token-sdk/src/policy-engine/instructions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clients/rwa-token-sdk/src/policy-engine/instructions.ts b/clients/rwa-token-sdk/src/policy-engine/instructions.ts index 12e6507..54c3d43 100644 --- a/clients/rwa-token-sdk/src/policy-engine/instructions.ts +++ b/clients/rwa-token-sdk/src/policy-engine/instructions.ts @@ -94,7 +94,7 @@ export async function getAttachToPolicyAccountIx( /** - * Generate instructions to detac an identity policy account to an asset. + * Generate instructions to detach an identity policy account to an asset. * * This function constructs an instruction to attach a policy account to an asset * using the provided arguments. It creates a new policy account, calls the policy From b668fc3f4797306a8c080054834cfe94b3bbca61 Mon Sep 17 00:00:00 2001 From: Bhargava Sai Macha Date: Mon, 20 May 2024 17:29:25 -0400 Subject: [PATCH 5/8] fix tests and update regsitry creates to use signer constraint --- .github/workflows/build.yml | 9 ++--- clients/rwa-token-sdk/package.json | 2 +- .../src/asset-controller/instructions.ts | 2 +- .../src/data-registry/instructions.ts | 2 + .../src/identity-registry/instructions.ts | 2 + .../src/policy-engine/instructions.ts | 2 + .../src/programs/idls/DataRegistry.json | 4 ++ .../src/programs/idls/IdentityRegistry.json | 4 ++ .../src/programs/idls/PolicyEngine.json | 7 +++- .../src/programs/types/DataRegistryTypes.ts | 4 ++ .../programs/types/IdentityRegistryTypes.ts | 4 ++ .../src/programs/types/PolicyEngineTypes.ts | 7 +++- clients/rwa-token-sdk/tests/e2e.test.ts | 19 ---------- .../tests/policies/identity_approval.test.ts | 17 --------- .../policies/transaction_amount_limit.test.ts | 5 --- .../transaction_amount_velocity.test.ts | 0 .../transaction_count_velocity.test.ts | 0 clients/rwa-token-sdk/tests/tracker.test.ts | 38 ++++++++++--------- programs/Cargo.lock | 2 - programs/asset_controller/Cargo.toml | 4 +- .../src/instructions/registry/create.rs | 6 ++- .../src/instructions/registry/create.rs | 6 ++- programs/move.sh | 31 +++++++++++++++ .../src/instructions/engine/create.rs | 6 ++- 24 files changed, 106 insertions(+), 77 deletions(-) delete mode 100644 clients/rwa-token-sdk/tests/policies/identity_approval.test.ts delete mode 100644 clients/rwa-token-sdk/tests/policies/transaction_amount_limit.test.ts delete mode 100644 clients/rwa-token-sdk/tests/policies/transaction_amount_velocity.test.ts delete mode 100644 clients/rwa-token-sdk/tests/policies/transaction_count_velocity.test.ts create mode 100755 programs/move.sh diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 66342d8..65d7a6d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,14 +2,11 @@ name: build env: cli-id: anchor-v0.30.0-solana-1.18.8 on: - push: - branches: - - '*' pull_request: branches: - - '*' + - 'staging' jobs: - build-cli-deps: + setup-tests: runs-on: ubicloud-standard-8 steps: - id: cache-cli-deps @@ -35,7 +32,7 @@ jobs: build: runs-on: ubicloud-standard-8 - needs: [build-cli-deps] + needs: [setup-tests] steps: - id: cache-cli-deps uses: actions/cache@v2 diff --git a/clients/rwa-token-sdk/package.json b/clients/rwa-token-sdk/package.json index 700b619..5d059e3 100644 --- a/clients/rwa-token-sdk/package.json +++ b/clients/rwa-token-sdk/package.json @@ -4,7 +4,7 @@ "description": "RWA Token SDK for the development of permissioned tokens on SVM blockchains.", "homepage": "https://github.com/bridgesplit/rwa-token#readme", "scripts": { - "test": "vitest run --testTimeout=120000", + "test": "vitest run --testTimeout=240000", "lint": "eslint . --ext .ts" }, "repository": { diff --git a/clients/rwa-token-sdk/src/asset-controller/instructions.ts b/clients/rwa-token-sdk/src/asset-controller/instructions.ts index e6c4a1b..4416fb3 100644 --- a/clients/rwa-token-sdk/src/asset-controller/instructions.ts +++ b/clients/rwa-token-sdk/src/asset-controller/instructions.ts @@ -290,7 +290,7 @@ export async function getSetupAssetControllerIxs( ): Promise { const mintKp = new Keypair(); const mint = mintKp.publicKey; - const updatedArgs = { ...args, assetMint: mint.toString() }; + const updatedArgs = { ...args, assetMint: mint.toString(), signer: args.authority }; // Get asset registry create ix const assetControllerCreateIx = await getCreateAssetControllerIx( updatedArgs, diff --git a/clients/rwa-token-sdk/src/data-registry/instructions.ts b/clients/rwa-token-sdk/src/data-registry/instructions.ts index 9cd3780..c218702 100644 --- a/clients/rwa-token-sdk/src/data-registry/instructions.ts +++ b/clients/rwa-token-sdk/src/data-registry/instructions.ts @@ -12,6 +12,7 @@ import { type AnchorProvider } from "@coral-xyz/anchor"; /** Represents arguments for creating an on chain data registry to store data accounts. */ export type CreateDataRegistryArgs = { authority: string; + signer: string; } & CommonArgs; /** @@ -31,6 +32,7 @@ export async function getCreateDataRegistryIx( ) .accountsStrict({ payer: args.payer, + signer: args.signer, assetMint: args.assetMint, dataRegistry: getDataRegistryPda(args.assetMint), systemProgram: SystemProgram.programId, diff --git a/clients/rwa-token-sdk/src/identity-registry/instructions.ts b/clients/rwa-token-sdk/src/identity-registry/instructions.ts index 0c9acdb..af0caea 100644 --- a/clients/rwa-token-sdk/src/identity-registry/instructions.ts +++ b/clients/rwa-token-sdk/src/identity-registry/instructions.ts @@ -14,6 +14,7 @@ import { type AnchorProvider } from "@coral-xyz/anchor"; /** Represents arguments for creating an on identity registry on chain. */ export type CreateIdentityRegistryArgs = { authority: string; + signer: string; } & CommonArgs; /** @@ -33,6 +34,7 @@ export async function getCreateIdentityRegistryIx( ) .accountsStrict({ payer: args.payer, + signer: args.signer, assetMint: args.assetMint, identityRegistryAccount: getIdentityRegistryPda(args.assetMint), systemProgram: SystemProgram.programId, diff --git a/clients/rwa-token-sdk/src/policy-engine/instructions.ts b/clients/rwa-token-sdk/src/policy-engine/instructions.ts index 54c3d43..5af7424 100644 --- a/clients/rwa-token-sdk/src/policy-engine/instructions.ts +++ b/clients/rwa-token-sdk/src/policy-engine/instructions.ts @@ -15,6 +15,7 @@ import { type AnchorProvider } from "@coral-xyz/anchor"; /** Represents the arguments required to create a policy engine account. */ export type CreatePolicyEngineArgs = { authority: string; + signer: string; } & CommonArgs; /** @@ -34,6 +35,7 @@ export async function getCreatePolicyEngineIx( ) .accountsStrict({ payer: args.payer, + signer: args.signer, assetMint: args.assetMint, policyEngine: getPolicyEnginePda(args.assetMint), systemProgram: SystemProgram.programId, diff --git a/clients/rwa-token-sdk/src/programs/idls/DataRegistry.json b/clients/rwa-token-sdk/src/programs/idls/DataRegistry.json index 2c6aaaf..85a43da 100644 --- a/clients/rwa-token-sdk/src/programs/idls/DataRegistry.json +++ b/clients/rwa-token-sdk/src/programs/idls/DataRegistry.json @@ -80,6 +80,10 @@ "writable": true, "signer": true }, + { + "name": "signer", + "signer": true + }, { "name": "asset_mint" }, diff --git a/clients/rwa-token-sdk/src/programs/idls/IdentityRegistry.json b/clients/rwa-token-sdk/src/programs/idls/IdentityRegistry.json index 8baeab6..dc25374 100644 --- a/clients/rwa-token-sdk/src/programs/idls/IdentityRegistry.json +++ b/clients/rwa-token-sdk/src/programs/idls/IdentityRegistry.json @@ -148,6 +148,10 @@ "writable": true, "signer": true }, + { + "name": "signer", + "signer": true + }, { "name": "asset_mint" }, diff --git a/clients/rwa-token-sdk/src/programs/idls/PolicyEngine.json b/clients/rwa-token-sdk/src/programs/idls/PolicyEngine.json index 800019c..4c2100b 100644 --- a/clients/rwa-token-sdk/src/programs/idls/PolicyEngine.json +++ b/clients/rwa-token-sdk/src/programs/idls/PolicyEngine.json @@ -159,6 +159,10 @@ "writable": true, "signer": true }, + { + "name": "signer", + "signer": true + }, { "name": "asset_mint" }, @@ -411,7 +415,8 @@ { "name": "policies", "docs": [ - "Different policies that can be applied to the policy account" + "Different policies that can be applied to the policy account", + "initial max len" ], "type": { "vec": { diff --git a/clients/rwa-token-sdk/src/programs/types/DataRegistryTypes.ts b/clients/rwa-token-sdk/src/programs/types/DataRegistryTypes.ts index 4ad23c1..afa9725 100644 --- a/clients/rwa-token-sdk/src/programs/types/DataRegistryTypes.ts +++ b/clients/rwa-token-sdk/src/programs/types/DataRegistryTypes.ts @@ -86,6 +86,10 @@ export type DataRegistry = { "writable": true, "signer": true }, + { + "name": "signer", + "signer": true + }, { "name": "assetMint" }, diff --git a/clients/rwa-token-sdk/src/programs/types/IdentityRegistryTypes.ts b/clients/rwa-token-sdk/src/programs/types/IdentityRegistryTypes.ts index a3c0387..1f0ceeb 100644 --- a/clients/rwa-token-sdk/src/programs/types/IdentityRegistryTypes.ts +++ b/clients/rwa-token-sdk/src/programs/types/IdentityRegistryTypes.ts @@ -154,6 +154,10 @@ export type IdentityRegistry = { "writable": true, "signer": true }, + { + "name": "signer", + "signer": true + }, { "name": "assetMint" }, diff --git a/clients/rwa-token-sdk/src/programs/types/PolicyEngineTypes.ts b/clients/rwa-token-sdk/src/programs/types/PolicyEngineTypes.ts index 7c0231b..dc8a034 100644 --- a/clients/rwa-token-sdk/src/programs/types/PolicyEngineTypes.ts +++ b/clients/rwa-token-sdk/src/programs/types/PolicyEngineTypes.ts @@ -165,6 +165,10 @@ export type PolicyEngine = { "writable": true, "signer": true }, + { + "name": "signer", + "signer": true + }, { "name": "assetMint" }, @@ -417,7 +421,8 @@ export type PolicyEngine = { { "name": "policies", "docs": [ - "Different policies that can be applied to the policy account" + "Different policies that can be applied to the policy account", + "initial max len" ], "type": { "vec": { diff --git a/clients/rwa-token-sdk/tests/e2e.test.ts b/clients/rwa-token-sdk/tests/e2e.test.ts index 90c23e3..4a936a1 100644 --- a/clients/rwa-token-sdk/tests/e2e.test.ts +++ b/clients/rwa-token-sdk/tests/e2e.test.ts @@ -48,25 +48,6 @@ describe("e2e tests", async () => { }; rwaClient = new RwaClient(config, new Wallet(setup.payerKp)); - - await rwaClient.provider.connection.confirmTransaction( - await rwaClient.provider.connection.requestAirdrop( - setup.payerKp.publicKey, - 1000000000 - ) - ); - await rwaClient.provider.connection.confirmTransaction( - await rwaClient.provider.connection.requestAirdrop( - setup.authorityKp.publicKey, - 1000000000 - ) - ); - await rwaClient.provider.connection.confirmTransaction( - await rwaClient.provider.connection.requestAirdrop( - setup.delegateKp.publicKey, - 1000000000 - ) - ); }); test("initalize asset controller", async () => { diff --git a/clients/rwa-token-sdk/tests/policies/identity_approval.test.ts b/clients/rwa-token-sdk/tests/policies/identity_approval.test.ts deleted file mode 100644 index 0a9cf9a..0000000 --- a/clients/rwa-token-sdk/tests/policies/identity_approval.test.ts +++ /dev/null @@ -1,17 +0,0 @@ - -import { BN, Wallet } from "@coral-xyz/anchor"; -import { - getPolicyAccountPda, getPolicyEngineProgram, getTransferTokensIx, - RwaClient, -} from ".././src"; -import { setupTests } from "../setup"; -import { ConfirmOptions, Connection, Transaction, sendAndConfirmTransaction } from "@solana/web3.js"; -import { expect, test, describe } from "vitest"; -import { Config } from "../../src/classes/types"; - - -// setup identity approval policy and check all values match -// update identity approval policy and check all values match -// check identity approval policy logic is being enforced for users -// check identity approval policy isnt being enforced for users with skip level -// check identity approval policy can be removed and no data is being left behind \ No newline at end of file diff --git a/clients/rwa-token-sdk/tests/policies/transaction_amount_limit.test.ts b/clients/rwa-token-sdk/tests/policies/transaction_amount_limit.test.ts deleted file mode 100644 index 9b76179..0000000 --- a/clients/rwa-token-sdk/tests/policies/transaction_amount_limit.test.ts +++ /dev/null @@ -1,5 +0,0 @@ -// setup txn amount limit policy and check all values match -// update txn amount limit policy policy and check all values match -// get user to max amount that policy enforce -// check txn amount limit policy isnt being enforced for users with skip level -// check txn amount limit policy can be removed and no data is being left behind \ No newline at end of file diff --git a/clients/rwa-token-sdk/tests/policies/transaction_amount_velocity.test.ts b/clients/rwa-token-sdk/tests/policies/transaction_amount_velocity.test.ts deleted file mode 100644 index e69de29..0000000 diff --git a/clients/rwa-token-sdk/tests/policies/transaction_count_velocity.test.ts b/clients/rwa-token-sdk/tests/policies/transaction_count_velocity.test.ts deleted file mode 100644 index e69de29..0000000 diff --git a/clients/rwa-token-sdk/tests/tracker.test.ts b/clients/rwa-token-sdk/tests/tracker.test.ts index 074daf5..6a659bc 100644 --- a/clients/rwa-token-sdk/tests/tracker.test.ts +++ b/clients/rwa-token-sdk/tests/tracker.test.ts @@ -1,18 +1,14 @@ import { BN, Wallet } from "@coral-xyz/anchor"; import { type AttachPolicyArgs, - type CreateDataAccountArgs, - getPolicyAccountPda, getTrackerAccount, - getTrackerAccountPda, type IssueTokenArgs, type SetupUserArgs, type TransferTokensArgs, - type UpdateDataAccountArgs, - type VoidTokensArgs, } from "../src"; import { setupTests } from "./setup"; import { + Commitment, type ConfirmOptions, Connection, Transaction, @@ -133,7 +129,6 @@ describe("test suite to test tracker account is being updated correctly on trans [setup.payerKp, setup.authorityKp] ); expect(txnId).toBeTruthy(); - console.log("issue tokens signature: ", txnId); }); test("transfer tokens", async () => { @@ -158,7 +153,7 @@ describe("test suite to test tracker account is being updated correctly on trans setup.user1.toString(), rwaClient.provider ); - // length of transfers should be 0 since any policies haven;t beeen attached uet + // length of transfers should be 0 since any policies haven;t beeen attached yet expect(trackerAccount!.transfers.length).toBe(0); }); @@ -196,20 +191,28 @@ describe("test suite to test tracker account is being updated correctly on trans }; const transferIx = await rwaClient.assetController.transfer(transferArgs); + let commitment: Commitment = "processed"; + if (i < 4) { + commitment = "finalized"; + } const txnId = await sendAndConfirmTransaction( rwaClient.provider.connection, new Transaction().add(transferIx), - [setup.payerKp, setup.user1Kp] + [setup.payerKp, setup.user1Kp], + { + commitment, + } ); expect(txnId).toBeTruthy(); - const trackerAccount = await getTrackerAccount( - mint, - setup.user1.toString(), - rwaClient.provider - ); - // length of transfers should be 0 since any policies haven;t beeen attached uet - expect(trackerAccount!.transfers.length).toBe(i); - expect(trackerAccount!.transfers.at(i)?.amount == 100); + if(i<4) { // dont need to check for all 25 transfers + const trackerAccount = await getTrackerAccount( + mint, + setup.user1.toString(), + rwaClient.provider + ); + expect(trackerAccount!.transfers.length).toBe(i + 1); + expect(trackerAccount!.transfers.at(i)?.amount == 100); + } } const transferArgs: TransferTokensArgs = { payer: setup.payer.toString(), @@ -225,8 +228,7 @@ describe("test suite to test tracker account is being updated correctly on trans rwaClient.provider.connection, new Transaction().add(transferIx), [setup.payerKp, setup.user1Kp] - )).toThrow("hello"); - + )).rejects.toThrowErrorMatchingInlineSnapshot("failed ({\"err\":{\"InstructionError\":[0,{\"Custom\":6006}]}})"); }); }); diff --git a/programs/Cargo.lock b/programs/Cargo.lock index 79ef670..4138667 100644 --- a/programs/Cargo.lock +++ b/programs/Cargo.lock @@ -405,10 +405,8 @@ dependencies = [ "anchor-spl", "identity_registry", "policy_engine", - "proc-macro2", "spl-tlv-account-resolution 0.4.0", "spl-transfer-hook-interface 0.5.1", - "syn 2.0.58", ] [[package]] diff --git a/programs/asset_controller/Cargo.toml b/programs/asset_controller/Cargo.toml index c7c1c82..9bd0f2c 100644 --- a/programs/asset_controller/Cargo.toml +++ b/programs/asset_controller/Cargo.toml @@ -23,6 +23,4 @@ anchor-spl = { git = "https://github.com/coral-xyz/anchor", features = ["token_2 spl-transfer-hook-interface = { version = "0.5.0" } spl-tlv-account-resolution = "0.4.0" policy_engine = { path = "../policy_engine", features = ["cpi"] } -identity_registry = { path = "../identity_registry", features = ["cpi"] } -syn = "=2.0.58" -proc-macro2 = "=1.0.79" \ No newline at end of file +identity_registry = { path = "../identity_registry", features = ["cpi"] } \ No newline at end of file diff --git a/programs/data_registry/src/instructions/registry/create.rs b/programs/data_registry/src/instructions/registry/create.rs index d973aac..f83e04c 100644 --- a/programs/data_registry/src/instructions/registry/create.rs +++ b/programs/data_registry/src/instructions/registry/create.rs @@ -1,5 +1,5 @@ use crate::{state::*, TOKEN22}; -use anchor_lang::prelude::*; +use anchor_lang::{prelude::*, solana_program::program_option::COption}; use anchor_spl::token_interface::Mint; #[derive(Accounts)] @@ -7,6 +7,10 @@ use anchor_spl::token_interface::Mint; pub struct CreateDataRegistry<'info> { #[account(mut)] pub payer: Signer<'info>, + #[account( + constraint = asset_mint.mint_authority == COption::Some(signer.key()), + )] + pub signer: Signer<'info>, #[account( mint::token_program = TOKEN22, )] diff --git a/programs/identity_registry/src/instructions/registry/create.rs b/programs/identity_registry/src/instructions/registry/create.rs index 789fa34..91b4f06 100644 --- a/programs/identity_registry/src/instructions/registry/create.rs +++ b/programs/identity_registry/src/instructions/registry/create.rs @@ -1,5 +1,5 @@ use crate::state::*; -use anchor_lang::prelude::*; +use anchor_lang::{prelude::*, solana_program::program_option::COption}; use anchor_spl::token_interface::Mint; #[derive(Accounts)] @@ -7,6 +7,10 @@ use anchor_spl::token_interface::Mint; pub struct CreateIdentityRegistry<'info> { #[account(mut)] pub payer: Signer<'info>, + #[account( + constraint = asset_mint.mint_authority == COption::Some(signer.key()), + )] + pub signer: Signer<'info>, #[account()] pub asset_mint: Box>, #[account( diff --git a/programs/move.sh b/programs/move.sh new file mode 100755 index 0000000..d228a03 --- /dev/null +++ b/programs/move.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +# Function to convert snake_case to PascalCase +convert_to_pascal_case() { + echo "$1" | awk -F'_' '{for(i=1;i<=NF;i++) $i=toupper(substr($i,1,1)) tolower(substr($i,2))}1' OFS='' +} + +# Define source and destination directories +source_dir="target/idl" +destination_dir="../clients/rwa-token-sdk/src/programs/idls" + +# Iterate over all JSON files in the source directory +for file in "$source_dir"/*.json; do + filename=$(basename "$file" .json) + pascal_case_filename=$(convert_to_pascal_case "$filename") + cp "$file" "$destination_dir/$pascal_case_filename.json" +done + + +# Define source and destination directories +source_dir="target/types" +destination_dir="../clients/rwa-token-sdk/src/programs/types" + + +# Iterate over all ts files in the source directory +for file in "$source_dir"/*.ts; do + filename=$(basename "$file" .ts) + pascal_case_filename=$(convert_to_pascal_case "$filename") + new_filename="${pascal_case_filename}Types.ts" + cp "$file" "$destination_dir/$new_filename" +done diff --git a/programs/policy_engine/src/instructions/engine/create.rs b/programs/policy_engine/src/instructions/engine/create.rs index 09ec82d..ad3a934 100644 --- a/programs/policy_engine/src/instructions/engine/create.rs +++ b/programs/policy_engine/src/instructions/engine/create.rs @@ -1,4 +1,4 @@ -use anchor_lang::prelude::*; +use anchor_lang::{prelude::*, solana_program::program_option::COption}; use anchor_spl::token_interface::Mint; use crate::state::*; @@ -8,6 +8,10 @@ use crate::state::*; pub struct CreatePolicyEngine<'info> { #[account(mut)] pub payer: Signer<'info>, + #[account( + constraint = asset_mint.mint_authority == COption::Some(signer.key()), + )] + pub signer: Signer<'info>, #[account( mint::token_program = TOKEN22 )] From b9d26b811743ed9e1eea33cd758250817c431ab5 Mon Sep 17 00:00:00 2001 From: Bhargava Sai Macha Date: Mon, 20 May 2024 17:30:38 -0400 Subject: [PATCH 6/8] update actions naming --- .github/workflows/{build.yml => tests.yml} | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename .github/workflows/{build.yml => tests.yml} (95%) diff --git a/.github/workflows/build.yml b/.github/workflows/tests.yml similarity index 95% rename from .github/workflows/build.yml rename to .github/workflows/tests.yml index 65d7a6d..95880b2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/tests.yml @@ -7,7 +7,7 @@ on: - 'staging' jobs: setup-tests: - runs-on: ubicloud-standard-8 + runs-on: ubicloud-standard-16 steps: - id: cache-cli-deps uses: actions/cache@v2 @@ -30,8 +30,8 @@ jobs: if: steps.cache-cli-deps.outputs.cache-hit != 'true' run: cargo install --git https://github.com/bridgesplit/anchor anchor-cli --locked - build: - runs-on: ubicloud-standard-8 + run-tests: + runs-on: ubicloud-standard-16 needs: [setup-tests] steps: - id: cache-cli-deps From d6863bf2a59ca854ced325febdb444b1340002d8 Mon Sep 17 00:00:00 2001 From: Bhargava Sai Macha Date: Mon, 20 May 2024 17:55:53 -0400 Subject: [PATCH 7/8] fix regex test --- .github/workflows/tests.yml | 2 +- clients/rwa-token-sdk/tests/tracker.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 95880b2..da33464 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,4 +1,4 @@ -name: build +name: tests env: cli-id: anchor-v0.30.0-solana-1.18.8 on: diff --git a/clients/rwa-token-sdk/tests/tracker.test.ts b/clients/rwa-token-sdk/tests/tracker.test.ts index 6a659bc..e07775a 100644 --- a/clients/rwa-token-sdk/tests/tracker.test.ts +++ b/clients/rwa-token-sdk/tests/tracker.test.ts @@ -228,7 +228,7 @@ describe("test suite to test tracker account is being updated correctly on trans rwaClient.provider.connection, new Transaction().add(transferIx), [setup.payerKp, setup.user1Kp] - )).rejects.toThrowErrorMatchingInlineSnapshot("failed ({\"err\":{\"InstructionError\":[0,{\"Custom\":6006}]}})"); + )).rejects.toThrowError(/failed \(\{"err":\{"InstructionError":\[0,\{"Custom":6006\}\]\}\}\)/); }); }); From a46e5961f99f0bd5585fd372a25ac19c11baca30 Mon Sep 17 00:00:00 2001 From: Bhargava Sai Macha Date: Mon, 20 May 2024 18:17:58 -0400 Subject: [PATCH 8/8] enable immutable installs --- clients/rwa-token-sdk/.yarnrc.yml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 clients/rwa-token-sdk/.yarnrc.yml diff --git a/clients/rwa-token-sdk/.yarnrc.yml b/clients/rwa-token-sdk/.yarnrc.yml new file mode 100644 index 0000000..1075270 --- /dev/null +++ b/clients/rwa-token-sdk/.yarnrc.yml @@ -0,0 +1,3 @@ +{ + enableImmutableInstalls: false +} \ No newline at end of file