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) {