From 3c448e002e1609c97873d13dc0603988aea98f75 Mon Sep 17 00:00:00 2001 From: Bhargava Sai Macha Date: Sun, 6 Oct 2024 10:37:38 -0400 Subject: [PATCH 1/8] add more policies --- clients/rwa-token-sdk/package.json | 4 +- .../src/asset-controller/instructions.ts | 26 ++- clients/rwa-token-sdk/tests/policies.test.ts | 6 +- .../tests/policies/transaction_count.test.ts | 182 ++++++++++++++++++ programs/identity_registry/src/error.rs | 2 + .../src/instructions/account/add.rs | 17 ++ .../src/instructions/account/remove.rs | 9 +- .../src/instructions/limit.rs | 37 ++++ .../identity_registry/src/instructions/mod.rs | 2 + programs/identity_registry/src/lib.rs | 9 + programs/identity_registry/src/state/limit.rs | 42 ++++ programs/identity_registry/src/state/mod.rs | 2 + programs/policy_engine/src/error.rs | 6 + .../policy_engine/src/instructions/execute.rs | 15 +- programs/policy_engine/src/state/account.rs | 88 ++++++++- programs/policy_engine/src/utils.rs | 53 ----- 16 files changed, 432 insertions(+), 68 deletions(-) create mode 100644 clients/rwa-token-sdk/tests/policies/transaction_count.test.ts create mode 100644 programs/identity_registry/src/instructions/limit.rs create mode 100644 programs/identity_registry/src/state/limit.rs diff --git a/clients/rwa-token-sdk/package.json b/clients/rwa-token-sdk/package.json index 70a1b5d..1252405 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.27", + "version": "0.0.31", "description": "RWA Token SDK for the development of permissioned tokens on SVM blockchains.", "homepage": "https://github.com/bridgesplit/rwa-token#readme", "main": "dist/index", @@ -12,7 +12,7 @@ "dist" ], "scripts": { - "test": "vitest run --testTimeout=240000", + "test": "vitest run tests/policies/* --testTimeout=240000", "lint": "eslint . --ext .ts", "build": "tsc" }, diff --git a/clients/rwa-token-sdk/src/asset-controller/instructions.ts b/clients/rwa-token-sdk/src/asset-controller/instructions.ts index c6b9fc0..2ddca1a 100644 --- a/clients/rwa-token-sdk/src/asset-controller/instructions.ts +++ b/clients/rwa-token-sdk/src/asset-controller/instructions.ts @@ -22,6 +22,7 @@ import { getCreateIdentityAccountIx, getIdentityAccountPda, getIdentityRegistryPda, + getAddLevelToIdentityAccount, } from "../identity-registry"; import { type CommonArgs, @@ -368,7 +369,7 @@ export type SetupUserArgs = { owner: string; signer: string; assetMint: string; - level: number; + levels: number[]; }; /** @@ -382,16 +383,34 @@ export async function getSetupUserIxs( args: SetupUserArgs, provider: AnchorProvider ): Promise { + const ixs: TransactionInstruction[] = []; const identityAccountIx = await getCreateIdentityAccountIx( { payer: args.payer, signer: args.signer, assetMint: args.assetMint, owner: args.owner, - level: args.level, + level: args.levels[0], }, provider ); + ixs.push(identityAccountIx); + if (args.levels.length > 1) { + for (let i = 1; i < args.levels.length; i++) { + const addLevelIx = await getAddLevelToIdentityAccount( + { + authority: args.signer, + owner: args.owner, + assetMint: args.assetMint, + level: args.levels[i], + signer: args.signer, + payer: args.payer, + }, + provider + ); + ixs.push(addLevelIx); + } + } const trackerAccountIx = await getCreateTrackerAccountIx( { payer: args.payer, @@ -400,8 +419,9 @@ export async function getSetupUserIxs( }, provider ); + ixs.push(trackerAccountIx); return { - ixs: [identityAccountIx, trackerAccountIx], + ixs, signers: [], }; } diff --git a/clients/rwa-token-sdk/tests/policies.test.ts b/clients/rwa-token-sdk/tests/policies.test.ts index b0b6045..e543df5 100644 --- a/clients/rwa-token-sdk/tests/policies.test.ts +++ b/clients/rwa-token-sdk/tests/policies.test.ts @@ -174,7 +174,7 @@ describe("test policy setup", async () => { payer: setup.payer.toString(), owner: setup.user1.toString(), assetMint: mint, - level: 1, + levels: [1], signer: setup.authorityKp.publicKey.toString() }); const txnId = await sendAndConfirmTransaction(setup.provider.connection, new Transaction().add(...setupUser.ixs), [setup.payerKp, setup.authorityKp, ...setupUser.signers]); @@ -186,7 +186,7 @@ describe("test policy setup", async () => { payer: setup.payer.toString(), owner: setup.user2.toString(), assetMint: mint, - level: 2, + levels: [2], signer: setup.authorityKp.publicKey.toString() }); const txnId = await sendAndConfirmTransaction(setup.provider.connection, new Transaction().add(...setupUser.ixs), [setup.payerKp, setup.authorityKp, ...setupUser.signers]); @@ -198,7 +198,7 @@ describe("test policy setup", async () => { payer: setup.payer.toString(), owner: setup.user3.toString(), assetMint: mint, - level: 255, // Skips all policies + levels: [255], // Skips all policies signer: setup.authorityKp.publicKey.toString() }); const txnId = await sendAndConfirmTransaction(setup.provider.connection, new Transaction().add(...setupUser.ixs), [setup.payerKp, setup.authorityKp, ...setupUser.signers]); diff --git a/clients/rwa-token-sdk/tests/policies/transaction_count.test.ts b/clients/rwa-token-sdk/tests/policies/transaction_count.test.ts new file mode 100644 index 0000000..f625138 --- /dev/null +++ b/clients/rwa-token-sdk/tests/policies/transaction_count.test.ts @@ -0,0 +1,182 @@ +import { BN, Wallet } from "@coral-xyz/anchor"; +import { + getPolicyAccountPda, + getPolicyEngineProgram, + getTransferTokensIxs, + 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 transaction count velocity policy", async () => { + let rwaClient: RwaClient; + let mint: string; + const setup = await setupTests(); + + const decimals = 2; + + test("setup environment", 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)); + + // Create asset controller + 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, setup.authorityKp, ...setupAssetController.signers] + ); + mint = setupAssetController.signers[0].publicKey.toString(); + expect(txnId).toBeTruthy(); + + // Setup users + const setupUser1 = await rwaClient.identityRegistry.setupUserIxns({ + payer: setup.payer.toString(), + owner: setup.user1.toString(), + assetMint: mint, + levels: [1], + signer: setup.authorityKp.publicKey.toString() + }); + await sendAndConfirmTransaction( + setup.provider.connection, + new Transaction().add(...setupUser1.ixs), + [setup.payerKp, setup.authorityKp, ...setupUser1.signers] + ); + + const setupUser2 = await rwaClient.identityRegistry.setupUserIxns({ + payer: setup.payer.toString(), + owner: setup.user2.toString(), + assetMint: mint, + levels: [1], + signer: setup.authorityKp.publicKey.toString() + }); + await sendAndConfirmTransaction( + setup.provider.connection, + new Transaction().add(...setupUser2.ixs), + [setup.payerKp, setup.authorityKp, ...setupUser2.signers] + ); + + // Issue tokens to user1 + const issueTokens = await rwaClient.assetController.issueTokenIxns({ + authority: setup.authority.toString(), + payer: setup.payer.toString(), + owner: setup.user1.toString(), + assetMint: mint, + amount: 1000000, + }); + await sendAndConfirmTransaction( + setup.provider.connection, + new Transaction().add(...issueTokens), + [setup.payerKp, setup.authorityKp] + ); + }); + + test("attach transaction count velocity policy", async () => { + const attachPolicy = await rwaClient.policyEngine.createPolicy({ + payer: setup.payer.toString(), + assetMint: mint, + authority: setup.authority.toString(), + identityFilter: { + identityLevels: [1], + comparisionType: { or: {} }, + }, + policyType: { + transactionCountVelocity: { + limit: new BN(3), + timeframe: new BN(60), // 60 seconds + }, + }, + }); + const txnId = await sendAndConfirmTransaction( + setup.provider.connection, + new Transaction().add(...attachPolicy.ixs), + [setup.payerKp, setup.authorityKp, ...attachPolicy.signers] + ); + expect(txnId).toBeTruthy(); + + const policyAccount = await getPolicyEngineProgram(setup.provider).account.policyAccount.fetch(getPolicyAccountPda(mint)); + expect(policyAccount.policies.length).toBe(1); + }); + + test("transfer tokens within limit", async () => { + for (let i = 0; i < 3; i++) { + const transferTokensIxs = await getTransferTokensIxs({ + from: setup.user1.toString(), + to: setup.user2.toString(), + assetMint: mint, + amount: 10, + decimals, + createTa: i == 0, + }, rwaClient.provider); + + const txnId = await sendAndConfirmTransaction( + setup.provider.connection, + new Transaction().add(...transferTokensIxs), + [setup.user1Kp], + ); + expect(txnId).toBeTruthy(); + } + }); + + test("transfer tokens exceeding limit", async () => { + const transferTokensIxs = await getTransferTokensIxs({ + from: setup.user1.toString(), + to: setup.user2.toString(), + assetMint: mint, + amount: 10, + decimals, + }, rwaClient.provider); + + expect(sendAndConfirmTransaction( + setup.provider.connection, + new Transaction().add(...transferTokensIxs), + [setup.user1Kp], + )).rejects.toThrowError(/custom program error: 0x1773/); + }); + + test("wait for policy reset and transfer again", async () => { + // Wait for 60 seconds to reset the policy + await new Promise(resolve => setTimeout(resolve, 61000)); + + const transferTokensIxs = await getTransferTokensIxs({ + from: setup.user1.toString(), + to: setup.user2.toString(), + assetMint: mint, + amount: 10, + decimals, + }, rwaClient.provider); + + const txnId = await sendAndConfirmTransaction( + setup.provider.connection, + new Transaction().add(...transferTokensIxs), + [setup.user1Kp], + ); + expect(txnId).toBeTruthy(); + }); +}); \ No newline at end of file diff --git a/programs/identity_registry/src/error.rs b/programs/identity_registry/src/error.rs index fd583c3..c0c1168 100644 --- a/programs/identity_registry/src/error.rs +++ b/programs/identity_registry/src/error.rs @@ -10,4 +10,6 @@ pub enum IdentityRegistryErrors { LevelNotFound, #[msg("Unauthorized signer")] UnauthorizedSigner, + #[msg("Identity limit reached")] + LimitReached, } diff --git a/programs/identity_registry/src/instructions/account/add.rs b/programs/identity_registry/src/instructions/account/add.rs index 68a63e3..e2761d3 100644 --- a/programs/identity_registry/src/instructions/account/add.rs +++ b/programs/identity_registry/src/instructions/account/add.rs @@ -1,3 +1,5 @@ +use std::u64; + use crate::state::*; use anchor_lang::prelude::*; @@ -22,10 +24,25 @@ pub struct AddLevelToIdentityAccount<'info> { constraint = level != 0, )] pub identity_account: Box>, + #[account( + init_if_needed, + payer = payer, + space = 8 + IdentityLimitAccount::INIT_SPACE, + seeds = [&[level], identity_account.identity_registry.as_ref()], + bump, + )] + pub limit_account: Box>, pub system_program: Program<'info, System>, } pub fn handler(ctx: Context, level: u8) -> Result<()> { + // init limit account if level is not present + if ctx.accounts.limit_account.level == 0 { + ctx.accounts + .limit_account + .new(ctx.accounts.identity_registry.key(), level, u64::MAX); + } ctx.accounts.identity_account.add_level(level)?; + ctx.accounts.limit_account.add_user()?; Ok(()) } diff --git a/programs/identity_registry/src/instructions/account/remove.rs b/programs/identity_registry/src/instructions/account/remove.rs index 117589e..fedd0e2 100644 --- a/programs/identity_registry/src/instructions/account/remove.rs +++ b/programs/identity_registry/src/instructions/account/remove.rs @@ -2,7 +2,7 @@ use crate::state::*; use anchor_lang::prelude::*; #[derive(Accounts)] -#[instruction()] +#[instruction(level: u8)] pub struct RemoveLevelFromIdentityAccount<'info> { #[account(mut)] pub payer: Signer<'info>, @@ -21,10 +21,17 @@ pub struct RemoveLevelFromIdentityAccount<'info> { realloc::payer = payer, )] pub identity_account: Box>, + #[account( + mut, + seeds = [&[level], identity_account.identity_registry.as_ref()], + bump, + )] + pub limit_account: Box>, pub system_program: Program<'info, System>, } pub fn handler(ctx: Context, level: u8) -> Result<()> { ctx.accounts.identity_account.remove_level(level)?; + ctx.accounts.limit_account.remove_user()?; Ok(()) } diff --git a/programs/identity_registry/src/instructions/limit.rs b/programs/identity_registry/src/instructions/limit.rs new file mode 100644 index 0000000..843faf0 --- /dev/null +++ b/programs/identity_registry/src/instructions/limit.rs @@ -0,0 +1,37 @@ +use std::u64; + +use crate::state::*; +use anchor_lang::prelude::*; + +#[derive(Accounts)] +#[instruction(level: u8)] +pub struct EditLevelLimit<'info> { + #[account(mut)] + pub payer: Signer<'info>, + #[account( + constraint = identity_registry.authority == signer.key() || identity_registry.delegate == signer.key() + )] + pub signer: Signer<'info>, + #[account()] + pub identity_registry: Box>, + #[account( + init_if_needed, + payer = payer, + space = 8 + IdentityLimitAccount::INIT_SPACE, + seeds = [&[level], identity_registry.key().as_ref()], + bump, + )] + pub limit_account: Box>, + pub system_program: Program<'info, System>, +} + +pub fn handler(ctx: Context, level: u8, max_allowed: Option) -> Result<()> { + let max_allowed = max_allowed.unwrap_or(u64::MAX); + // init limit account if level is not present + if ctx.accounts.limit_account.level == 0 { + ctx.accounts + .limit_account + .new(ctx.accounts.identity_registry.key(), level, max_allowed); + } + Ok(()) +} diff --git a/programs/identity_registry/src/instructions/mod.rs b/programs/identity_registry/src/instructions/mod.rs index c026a4c..b0f88eb 100644 --- a/programs/identity_registry/src/instructions/mod.rs +++ b/programs/identity_registry/src/instructions/mod.rs @@ -1,5 +1,7 @@ pub mod account; +pub mod limit; pub mod registry; pub use account::*; +pub use limit::*; pub use registry::*; diff --git a/programs/identity_registry/src/lib.rs b/programs/identity_registry/src/lib.rs index f5dd5a3..6ffd65d 100644 --- a/programs/identity_registry/src/lib.rs +++ b/programs/identity_registry/src/lib.rs @@ -70,4 +70,13 @@ pub mod identity_registry { // no extra steps needed, identity account is being properly closed Ok(()) } + + /// edit level limit + pub fn edit_level_limit( + ctx: Context, + level: u8, + max_allowed: Option, + ) -> Result<()> { + instructions::limit::handler(ctx, level, max_allowed) + } } diff --git a/programs/identity_registry/src/state/limit.rs b/programs/identity_registry/src/state/limit.rs new file mode 100644 index 0000000..0552ea4 --- /dev/null +++ b/programs/identity_registry/src/state/limit.rs @@ -0,0 +1,42 @@ +use anchor_lang::prelude::*; + +use crate::IdentityRegistryErrors; + +#[account()] +#[derive(InitSpace)] +pub struct IdentityLimitAccount { + /// version of the account + pub version: u8, + /// identity registry to which the account belongs + pub identity_registry: Pubkey, + /// identity level + pub level: u8, + /// current number of users + pub current_users: u64, + /// max number of users + pub max_users: u64, +} + +impl IdentityLimitAccount { + pub const VERSION: u8 = 1; + pub fn new(&mut self, identity_registry: Pubkey, level: u8, max_users: u64) { + self.identity_registry = identity_registry; + self.level = level; + self.version = Self::VERSION; + self.current_users = 0; + self.max_users = max_users; + } + + pub fn add_user(&mut self) -> Result<()> { + if self.current_users >= self.max_users { + return Err(IdentityRegistryErrors::LimitReached.into()); + } + self.current_users += 1; + Ok(()) + } + + pub fn remove_user(&mut self) -> Result<()> { + self.current_users -= 1; + Ok(()) + } +} diff --git a/programs/identity_registry/src/state/mod.rs b/programs/identity_registry/src/state/mod.rs index 8952e82..5c85e32 100644 --- a/programs/identity_registry/src/state/mod.rs +++ b/programs/identity_registry/src/state/mod.rs @@ -1,7 +1,9 @@ pub mod account; +pub mod limit; pub mod registry; pub use account::*; +pub use limit::*; pub use registry::*; use anchor_lang::{solana_program::program_error::ProgramError, AnchorDeserialize, Discriminator}; diff --git a/programs/policy_engine/src/error.rs b/programs/policy_engine/src/error.rs index a433c5a..dcbd68f 100644 --- a/programs/policy_engine/src/error.rs +++ b/programs/policy_engine/src/error.rs @@ -34,4 +34,10 @@ pub enum PolicyEngineErrors { InvalidPdaPassedIn, #[msg("Transfer history full")] TransferHistoryFull, + #[msg("All Transfers have been paused")] + TransferPaused, + #[msg("Expected source account to transfer full amount")] + ForceFullTransfer, + #[msg("Holder limit exceeded")] + HolderLimitExceeded, } diff --git a/programs/policy_engine/src/instructions/execute.rs b/programs/policy_engine/src/instructions/execute.rs index c911bcb..5698e3b 100644 --- a/programs/policy_engine/src/instructions/execute.rs +++ b/programs/policy_engine/src/instructions/execute.rs @@ -1,6 +1,6 @@ use crate::{ - enforce_policy, get_asset_controller_account_pda, verify_cpi_program_is_token22, verify_pda, - PolicyAccount, PolicyEngineAccount, TrackerAccount, + get_asset_controller_account_pda, verify_cpi_program_is_token22, verify_pda, PolicyAccount, + PolicyEngineAccount, TrackerAccount, }; use anchor_lang::{ prelude::*, @@ -86,7 +86,7 @@ pub fn handler(ctx: Context, amount: u64) -> Result<()> { &mut &ctx.accounts.policy_engine_account.data.borrow()[8..], )?; - let policy_account = + let mut policy_account = PolicyAccount::deserialize(&mut &ctx.accounts.policy_account.data.borrow()[8..])?; // go through with transfer if there aren't any policies attached @@ -137,11 +137,11 @@ pub fn handler(ctx: Context, amount: u64) -> Result<()> { }; // evaluate policies - enforce_policy( - policy_account.policies.clone(), + policy_account.enforce_policy( amount, Clock::get()?.unix_timestamp, &levels, + ctx.accounts.source_account.amount, ctx.accounts.destination_account.amount, &transfers, )?; @@ -159,5 +159,10 @@ pub fn handler(ctx: Context, amount: u64) -> Result<()> { .copy_from_slice(&tracker_account_data); } + let policy_account_data = policy_account.try_to_vec()?; + let policy_account_data_len = policy_account_data.len(); + ctx.accounts.policy_account.data.borrow_mut()[8..8 + policy_account_data_len] + .copy_from_slice(&policy_account_data); + Ok(()) } diff --git a/programs/policy_engine/src/state/account.rs b/programs/policy_engine/src/state/account.rs index c309d8c..3430352 100644 --- a/programs/policy_engine/src/state/account.rs +++ b/programs/policy_engine/src/state/account.rs @@ -2,7 +2,10 @@ use anchor_lang::prelude::*; use num_enum::IntoPrimitive; use serde::{Deserialize, Serialize}; -use crate::PolicyEngineErrors; +use crate::{ + enforce_identity_filter, get_total_amount_transferred_in_timeframe, + get_total_transactions_in_timeframe, PolicyEngineErrors, Transfer, +}; #[derive( AnchorDeserialize, AnchorSerialize, Clone, InitSpace, Copy, Debug, Serialize, Deserialize, @@ -67,6 +70,9 @@ pub enum PolicyType { TransactionAmountVelocity { limit: u64, timeframe: i64 }, TransactionCountVelocity { limit: u64, timeframe: i64 }, MaxBalance { limit: u64 }, + TransferPause, + ForceFullTransfer, + HolderLimit { limit: u64, current_number: u64 }, } impl PolicyAccount { @@ -126,4 +132,84 @@ impl PolicyAccount { self.policies.retain(|policy| policy.hash != hash); Ok(policy_type) } + + /// enforces different types of policies + #[inline(never)] + pub fn enforce_policy( + &mut self, + transfer_amount: u64, + timestamp: i64, + identity: &[u8], + source_balance: u64, + receiver_balance: u64, + transfers: &Vec, + ) -> Result<()> { + for policy in self.policies.iter_mut() { + match &mut policy.policy_type { + PolicyType::IdentityApproval => { + enforce_identity_filter(identity, policy.identity_filter)?; + } + PolicyType::TransactionAmountLimit { limit } => { + if enforce_identity_filter(identity, policy.identity_filter).is_ok() + && transfer_amount > *limit + { + return Err(PolicyEngineErrors::TransactionAmountLimitExceeded.into()); + } + } + PolicyType::TransactionAmountVelocity { limit, timeframe } => { + if enforce_identity_filter(identity, policy.identity_filter).is_ok() { + let total_amount_transferred = get_total_amount_transferred_in_timeframe( + transfers, *timeframe, timestamp, + ); + + if total_amount_transferred + transfer_amount > *limit { + return Err( + PolicyEngineErrors::TransactionAmountVelocityExceeded.into() + ); + } + } + } + PolicyType::TransactionCountVelocity { limit, timeframe } => { + if enforce_identity_filter(identity, policy.identity_filter).is_ok() { + let total_transactions = + get_total_transactions_in_timeframe(transfers, *timeframe, timestamp); + if total_transactions + 1 > *limit { + return Err(PolicyEngineErrors::TransactionCountVelocityExceeded.into()); + } + } + } + PolicyType::MaxBalance { limit } => { + if enforce_identity_filter(identity, policy.identity_filter).is_ok() + && transfer_amount + receiver_balance > *limit + { + return Err(PolicyEngineErrors::MaxBalanceExceeded.into()); + } + } + PolicyType::TransferPause => { + return Err(PolicyEngineErrors::TransferPaused.into()); + } + PolicyType::ForceFullTransfer => { + if enforce_identity_filter(identity, policy.identity_filter).is_ok() + && source_balance != transfer_amount + { + return Err(PolicyEngineErrors::ForceFullTransfer.into()); + } + } + PolicyType::HolderLimit { + limit, + current_number, + } => { + if enforce_identity_filter(identity, policy.identity_filter).is_ok() { + if receiver_balance == 0 && source_balance != transfer_amount { + *current_number += 1; + } + if current_number > limit { + return Err(PolicyEngineErrors::HolderLimitExceeded.into()); + } + } + } + } + } + Ok(()) + } } diff --git a/programs/policy_engine/src/utils.rs b/programs/policy_engine/src/utils.rs index 2a66fd2..3f46acb 100644 --- a/programs/policy_engine/src/utils.rs +++ b/programs/policy_engine/src/utils.rs @@ -67,59 +67,6 @@ pub struct Transfer { pub timestamp: i64, } -/// enforces different types of policies -#[inline(never)] -pub fn enforce_policy( - policies: Vec, - amount: u64, - timestamp: i64, - identity: &[u8], - balance: u64, - transfers: &Vec, -) -> Result<()> { - for policy in policies.iter() { - match policy.policy_type { - PolicyType::IdentityApproval => { - enforce_identity_filter(identity, policy.identity_filter)?; - } - PolicyType::TransactionAmountLimit { limit } => { - if enforce_identity_filter(identity, policy.identity_filter).is_ok() - && amount > limit - { - return Err(PolicyEngineErrors::TransactionAmountLimitExceeded.into()); - } - } - PolicyType::TransactionAmountVelocity { limit, timeframe } => { - if enforce_identity_filter(identity, policy.identity_filter).is_ok() { - let total_amount_transferred = - get_total_amount_transferred_in_timeframe(transfers, timeframe, timestamp); - - if total_amount_transferred + amount > limit { - return Err(PolicyEngineErrors::TransactionAmountVelocityExceeded.into()); - } - } - } - PolicyType::TransactionCountVelocity { limit, timeframe } => { - if enforce_identity_filter(identity, policy.identity_filter).is_ok() { - let total_transactions = - get_total_transactions_in_timeframe(transfers, timeframe, timestamp); - if total_transactions + 1 > limit { - return Err(PolicyEngineErrors::TransactionCountVelocityExceeded.into()); - } - } - } - PolicyType::MaxBalance { limit } => { - if enforce_identity_filter(identity, policy.identity_filter).is_ok() - && amount + balance > limit - { - return Err(PolicyEngineErrors::MaxBalanceExceeded.into()); - } - } - } - } - Ok(()) -} - pub const TRANSFER_HOOK_MINT_INDEX: usize = 1; pub fn verify_cpi_program_is_token22( From 22cc44983253974f1f1f27041181f7499901ae7b Mon Sep 17 00:00:00 2001 From: Bhargava Sai Macha Date: Mon, 7 Oct 2024 09:21:20 -0400 Subject: [PATCH 2/8] rename limit to metadata --- .../identity_registry/src/instructions/account/add.rs | 4 ++-- .../identity_registry/src/instructions/account/remove.rs | 2 +- .../src/instructions/{limit.rs => metadata.rs} | 8 ++++---- programs/identity_registry/src/instructions/mod.rs | 4 ++-- programs/identity_registry/src/lib.rs | 8 ++++---- .../identity_registry/src/state/{limit.rs => metadata.rs} | 4 ++-- 6 files changed, 15 insertions(+), 15 deletions(-) rename programs/identity_registry/src/instructions/{limit.rs => metadata.rs} (76%) rename programs/identity_registry/src/state/{limit.rs => metadata.rs} (93%) diff --git a/programs/identity_registry/src/instructions/account/add.rs b/programs/identity_registry/src/instructions/account/add.rs index e2761d3..5b08dfa 100644 --- a/programs/identity_registry/src/instructions/account/add.rs +++ b/programs/identity_registry/src/instructions/account/add.rs @@ -27,11 +27,11 @@ pub struct AddLevelToIdentityAccount<'info> { #[account( init_if_needed, payer = payer, - space = 8 + IdentityLimitAccount::INIT_SPACE, + space = 8 + IdentityMetadataAccount::INIT_SPACE, seeds = [&[level], identity_account.identity_registry.as_ref()], bump, )] - pub limit_account: Box>, + pub limit_account: Box>, pub system_program: Program<'info, System>, } diff --git a/programs/identity_registry/src/instructions/account/remove.rs b/programs/identity_registry/src/instructions/account/remove.rs index fedd0e2..c681450 100644 --- a/programs/identity_registry/src/instructions/account/remove.rs +++ b/programs/identity_registry/src/instructions/account/remove.rs @@ -26,7 +26,7 @@ pub struct RemoveLevelFromIdentityAccount<'info> { seeds = [&[level], identity_account.identity_registry.as_ref()], bump, )] - pub limit_account: Box>, + pub limit_account: Box>, pub system_program: Program<'info, System>, } diff --git a/programs/identity_registry/src/instructions/limit.rs b/programs/identity_registry/src/instructions/metadata.rs similarity index 76% rename from programs/identity_registry/src/instructions/limit.rs rename to programs/identity_registry/src/instructions/metadata.rs index 843faf0..eb77025 100644 --- a/programs/identity_registry/src/instructions/limit.rs +++ b/programs/identity_registry/src/instructions/metadata.rs @@ -5,7 +5,7 @@ use anchor_lang::prelude::*; #[derive(Accounts)] #[instruction(level: u8)] -pub struct EditLevelLimit<'info> { +pub struct EditIdentityMetadata<'info> { #[account(mut)] pub payer: Signer<'info>, #[account( @@ -17,15 +17,15 @@ pub struct EditLevelLimit<'info> { #[account( init_if_needed, payer = payer, - space = 8 + IdentityLimitAccount::INIT_SPACE, + space = 8 + IdentityMetadataAccount::INIT_SPACE, seeds = [&[level], identity_registry.key().as_ref()], bump, )] - pub limit_account: Box>, + pub limit_account: Box>, pub system_program: Program<'info, System>, } -pub fn handler(ctx: Context, level: u8, max_allowed: Option) -> Result<()> { +pub fn handler(ctx: Context, level: u8, max_allowed: Option) -> Result<()> { let max_allowed = max_allowed.unwrap_or(u64::MAX); // init limit account if level is not present if ctx.accounts.limit_account.level == 0 { diff --git a/programs/identity_registry/src/instructions/mod.rs b/programs/identity_registry/src/instructions/mod.rs index b0f88eb..fe6ea0c 100644 --- a/programs/identity_registry/src/instructions/mod.rs +++ b/programs/identity_registry/src/instructions/mod.rs @@ -1,7 +1,7 @@ pub mod account; -pub mod limit; +pub mod metadata; pub mod registry; pub use account::*; -pub use limit::*; +pub use metadata::*; pub use registry::*; diff --git a/programs/identity_registry/src/lib.rs b/programs/identity_registry/src/lib.rs index 6ffd65d..9ad1ade 100644 --- a/programs/identity_registry/src/lib.rs +++ b/programs/identity_registry/src/lib.rs @@ -71,12 +71,12 @@ pub mod identity_registry { Ok(()) } - /// edit level limit - pub fn edit_level_limit( - ctx: Context, + /// edit identity metadata + pub fn edit_identity_metdata( + ctx: Context, level: u8, max_allowed: Option, ) -> Result<()> { - instructions::limit::handler(ctx, level, max_allowed) + instructions::metadata::handler(ctx, level, max_allowed) } } diff --git a/programs/identity_registry/src/state/limit.rs b/programs/identity_registry/src/state/metadata.rs similarity index 93% rename from programs/identity_registry/src/state/limit.rs rename to programs/identity_registry/src/state/metadata.rs index 0552ea4..6ecd85c 100644 --- a/programs/identity_registry/src/state/limit.rs +++ b/programs/identity_registry/src/state/metadata.rs @@ -4,7 +4,7 @@ use crate::IdentityRegistryErrors; #[account()] #[derive(InitSpace)] -pub struct IdentityLimitAccount { +pub struct IdentityMetadataAccount { /// version of the account pub version: u8, /// identity registry to which the account belongs @@ -17,7 +17,7 @@ pub struct IdentityLimitAccount { pub max_users: u64, } -impl IdentityLimitAccount { +impl IdentityMetadataAccount { pub const VERSION: u8 = 1; pub fn new(&mut self, identity_registry: Pubkey, level: u8, max_users: u64) { self.identity_registry = identity_registry; From 8d40b6f9331726dc09256b37dc1e16c1e817ce06 Mon Sep 17 00:00:00 2001 From: Bhargava Sai Macha Date: Mon, 7 Oct 2024 09:23:36 -0400 Subject: [PATCH 3/8] subtract 1 if source balance is zeroed out --- programs/identity_registry/src/state/mod.rs | 4 ++-- programs/policy_engine/src/state/account.rs | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/programs/identity_registry/src/state/mod.rs b/programs/identity_registry/src/state/mod.rs index 5c85e32..900825e 100644 --- a/programs/identity_registry/src/state/mod.rs +++ b/programs/identity_registry/src/state/mod.rs @@ -1,9 +1,9 @@ pub mod account; -pub mod limit; +pub mod metadata; pub mod registry; pub use account::*; -pub use limit::*; +pub use metadata::*; pub use registry::*; use anchor_lang::{solana_program::program_error::ProgramError, AnchorDeserialize, Discriminator}; diff --git a/programs/policy_engine/src/state/account.rs b/programs/policy_engine/src/state/account.rs index 3430352..32edca0 100644 --- a/programs/policy_engine/src/state/account.rs +++ b/programs/policy_engine/src/state/account.rs @@ -203,6 +203,9 @@ impl PolicyAccount { if receiver_balance == 0 && source_balance != transfer_amount { *current_number += 1; } + if source_balance == transfer_amount { + *current_number -= 1; + } if current_number > limit { return Err(PolicyEngineErrors::HolderLimitExceeded.into()); } From 4294274e029bfd57c38a6ba190da9bc0ef608197 Mon Sep 17 00:00:00 2001 From: Bhargava Sai Macha Date: Mon, 7 Oct 2024 09:43:34 -0400 Subject: [PATCH 4/8] add balance limit --- clients/rwa-token-sdk/package.json | 2 +- .../src/instructions/metadata.rs | 6 +- programs/policy_engine/src/error.rs | 2 + .../policy_engine/src/instructions/execute.rs | 34 +++++++++-- programs/policy_engine/src/state/account.rs | 56 ++++++++++++------- 5 files changed, 73 insertions(+), 27 deletions(-) diff --git a/clients/rwa-token-sdk/package.json b/clients/rwa-token-sdk/package.json index 1252405..52f7ef0 100644 --- a/clients/rwa-token-sdk/package.json +++ b/clients/rwa-token-sdk/package.json @@ -12,7 +12,7 @@ "dist" ], "scripts": { - "test": "vitest run tests/policies/* --testTimeout=240000", + "test": "vitest run --testTimeout=240000", "lint": "eslint . --ext .ts", "build": "tsc" }, diff --git a/programs/identity_registry/src/instructions/metadata.rs b/programs/identity_registry/src/instructions/metadata.rs index eb77025..9989c95 100644 --- a/programs/identity_registry/src/instructions/metadata.rs +++ b/programs/identity_registry/src/instructions/metadata.rs @@ -25,7 +25,11 @@ pub struct EditIdentityMetadata<'info> { pub system_program: Program<'info, System>, } -pub fn handler(ctx: Context, level: u8, max_allowed: Option) -> Result<()> { +pub fn handler( + ctx: Context, + level: u8, + max_allowed: Option, +) -> Result<()> { let max_allowed = max_allowed.unwrap_or(u64::MAX); // init limit account if level is not present if ctx.accounts.limit_account.level == 0 { diff --git a/programs/policy_engine/src/error.rs b/programs/policy_engine/src/error.rs index dcbd68f..4749339 100644 --- a/programs/policy_engine/src/error.rs +++ b/programs/policy_engine/src/error.rs @@ -40,4 +40,6 @@ pub enum PolicyEngineErrors { ForceFullTransfer, #[msg("Holder limit exceeded")] HolderLimitExceeded, + #[msg("Balance limit exceeded")] + BalanceLimitExceeded, } diff --git a/programs/policy_engine/src/instructions/execute.rs b/programs/policy_engine/src/instructions/execute.rs index 5698e3b..236713f 100644 --- a/programs/policy_engine/src/instructions/execute.rs +++ b/programs/policy_engine/src/instructions/execute.rs @@ -41,7 +41,7 @@ pub struct ExecuteTransferHook<'info> { /// CHECK: internal ix checks pub identity_registry_account: UncheckedAccount<'info>, /// CHECK: internal ix checks - pub identity_account: UncheckedAccount<'info>, + pub receiver_identity_account: UncheckedAccount<'info>, #[account(mut)] /// CHECK: internal ix checks pub tracker_account: UncheckedAccount<'info>, @@ -51,6 +51,8 @@ pub struct ExecuteTransferHook<'info> { #[account(constraint = instructions_program.key() == sysvar::instructions::id())] /// CHECK: constraint check pub instructions_program: UncheckedAccount<'info>, + /// CHECK: internal ix checks + pub source_identity_account: UncheckedAccount<'info>, } pub fn handler(ctx: Context, amount: u64) -> Result<()> { @@ -102,7 +104,7 @@ pub fn handler(ctx: Context, amount: u64) -> Result<()> { )?; verify_pda( - ctx.accounts.identity_account.key(), + ctx.accounts.receiver_identity_account.key(), &[ &ctx.accounts.identity_registry_account.key().to_bytes(), &ctx.accounts.destination_account.owner.to_bytes(), @@ -110,14 +112,33 @@ pub fn handler(ctx: Context, amount: u64) -> Result<()> { &identity_registry::id(), )?; - let levels = if !ctx.accounts.identity_account.data_is_empty() { - IdentityAccount::deserialize(&mut &ctx.accounts.identity_account.data.borrow()[8..])?.levels + let receiver_levels = if !ctx.accounts.receiver_identity_account.data_is_empty() { + IdentityAccount::deserialize( + &mut &ctx.accounts.receiver_identity_account.data.borrow()[8..], + )? + .levels + } else { + vec![NO_IDENTITY_LEVEL] + }; + + verify_pda( + ctx.accounts.source_identity_account.key(), + &[ + &ctx.accounts.identity_registry_account.key().to_bytes(), + &ctx.accounts.source_account.owner.to_bytes(), + ], + &identity_registry::id(), + )?; + + let source_levels = if !ctx.accounts.source_identity_account.data_is_empty() { + IdentityAccount::deserialize(&mut &ctx.accounts.source_identity_account.data.borrow()[8..])? + .levels } else { vec![NO_IDENTITY_LEVEL] }; // if user has identity skip level, skip enforcing policy - if levels.contains(&SKIP_POLICY_LEVEL) { + if receiver_levels.contains(&SKIP_POLICY_LEVEL) { return Ok(()); } @@ -140,7 +161,8 @@ pub fn handler(ctx: Context, amount: u64) -> Result<()> { policy_account.enforce_policy( amount, Clock::get()?.unix_timestamp, - &levels, + &source_levels, + &receiver_levels, ctx.accounts.source_account.amount, ctx.accounts.destination_account.amount, &transfers, diff --git a/programs/policy_engine/src/state/account.rs b/programs/policy_engine/src/state/account.rs index 32edca0..834b424 100644 --- a/programs/policy_engine/src/state/account.rs +++ b/programs/policy_engine/src/state/account.rs @@ -72,7 +72,8 @@ pub enum PolicyType { MaxBalance { limit: u64 }, TransferPause, ForceFullTransfer, - HolderLimit { limit: u64, current_number: u64 }, + HolderLimit { limit: u64, current_holders: u64 }, + BalanceLimit { limit: u128, current_balance: u128 }, } impl PolicyAccount { @@ -139,7 +140,8 @@ impl PolicyAccount { &mut self, transfer_amount: u64, timestamp: i64, - identity: &[u8], + source_identity: &[u8], + receiver_identity: &[u8], source_balance: u64, receiver_balance: u64, transfers: &Vec, @@ -147,17 +149,17 @@ impl PolicyAccount { for policy in self.policies.iter_mut() { match &mut policy.policy_type { PolicyType::IdentityApproval => { - enforce_identity_filter(identity, policy.identity_filter)?; + enforce_identity_filter(receiver_identity, policy.identity_filter)?; } PolicyType::TransactionAmountLimit { limit } => { - if enforce_identity_filter(identity, policy.identity_filter).is_ok() + if enforce_identity_filter(receiver_identity, policy.identity_filter).is_ok() && transfer_amount > *limit { return Err(PolicyEngineErrors::TransactionAmountLimitExceeded.into()); } } PolicyType::TransactionAmountVelocity { limit, timeframe } => { - if enforce_identity_filter(identity, policy.identity_filter).is_ok() { + if enforce_identity_filter(receiver_identity, policy.identity_filter).is_ok() { let total_amount_transferred = get_total_amount_transferred_in_timeframe( transfers, *timeframe, timestamp, ); @@ -170,7 +172,7 @@ impl PolicyAccount { } } PolicyType::TransactionCountVelocity { limit, timeframe } => { - if enforce_identity_filter(identity, policy.identity_filter).is_ok() { + if enforce_identity_filter(receiver_identity, policy.identity_filter).is_ok() { let total_transactions = get_total_transactions_in_timeframe(transfers, *timeframe, timestamp); if total_transactions + 1 > *limit { @@ -179,7 +181,7 @@ impl PolicyAccount { } } PolicyType::MaxBalance { limit } => { - if enforce_identity_filter(identity, policy.identity_filter).is_ok() + if enforce_identity_filter(receiver_identity, policy.identity_filter).is_ok() && transfer_amount + receiver_balance > *limit { return Err(PolicyEngineErrors::MaxBalanceExceeded.into()); @@ -189,7 +191,7 @@ impl PolicyAccount { return Err(PolicyEngineErrors::TransferPaused.into()); } PolicyType::ForceFullTransfer => { - if enforce_identity_filter(identity, policy.identity_filter).is_ok() + if enforce_identity_filter(receiver_identity, policy.identity_filter).is_ok() && source_balance != transfer_amount { return Err(PolicyEngineErrors::ForceFullTransfer.into()); @@ -197,18 +199,34 @@ impl PolicyAccount { } PolicyType::HolderLimit { limit, - current_number, + current_holders, } => { - if enforce_identity_filter(identity, policy.identity_filter).is_ok() { - if receiver_balance == 0 && source_balance != transfer_amount { - *current_number += 1; - } - if source_balance == transfer_amount { - *current_number -= 1; - } - if current_number > limit { - return Err(PolicyEngineErrors::HolderLimitExceeded.into()); - } + if enforce_identity_filter(receiver_identity, policy.identity_filter).is_ok() + && receiver_balance == 0 + { + *current_holders += 1; + } + if enforce_identity_filter(source_identity, policy.identity_filter).is_ok() + && source_balance == transfer_amount + { + *current_holders -= 1; + } + if current_holders > limit { + return Err(PolicyEngineErrors::HolderLimitExceeded.into()); + } + } + PolicyType::BalanceLimit { + limit, + current_balance, + } => { + if enforce_identity_filter(receiver_identity, policy.identity_filter).is_ok() { + *current_balance += transfer_amount as u128; + } + if enforce_identity_filter(source_identity, policy.identity_filter).is_ok() { + *current_balance -= transfer_amount as u128; + } + if current_balance > limit { + return Err(PolicyEngineErrors::HolderLimitExceeded.into()); } } } From bd66459efc0aaf8d7a2e67b79c555796db2beb47 Mon Sep 17 00:00:00 2001 From: Bhargava Sai Macha Date: Mon, 7 Oct 2024 11:02:50 -0400 Subject: [PATCH 5/8] fix tests --- .github/workflows/tests.yml | 8 +- programs/Anchor.toml => Anchor.toml | 10 +- programs/Cargo.lock => Cargo.lock | 0 Cargo.toml | 27 +++ .../src/asset-controller/instructions.ts | 10 + .../src/programs/idls/DataRegistry.json | 1 + .../src/programs/idls/IdentityRegistry.json | 119 +++++++++++ .../src/programs/idls/PolicyEngine.json | 191 +++++++++--------- .../src/programs/types/DataRegistryTypes.ts | 1 + .../programs/types/IdentityRegistryTypes.ts | 119 +++++++++++ .../src/programs/types/PolicyEngineTypes.ts | 191 +++++++++--------- clients/rwa-token-sdk/tests/e2e.test.ts | 6 +- .../rwa-token-sdk/tests/extensions.test.ts | 2 +- clients/rwa-token-sdk/tests/tracker.test.ts | 4 +- programs/move.sh => move.sh | 0 programs/Cargo.toml | 18 -- programs/asset_controller/Cargo.toml | 6 +- programs/data_registry/Cargo.toml | 6 +- programs/identity_registry/Cargo.toml | 6 +- .../src/instructions/account/add.rs | 14 +- .../src/instructions/account/remove.rs | 4 +- .../src/instructions/metadata.rs | 12 +- programs/policy_engine/Cargo.toml | 6 +- programs/policy_engine/src/state/account.rs | 1 + programs/policy_engine/src/utils.rs | 16 +- {programs/rwa_utils => rwa_utils}/Cargo.toml | 0 .../rwa_utils => rwa_utils}/src/constants.rs | 0 .../rwa_utils => rwa_utils}/src/geyser.rs | 0 {programs/rwa_utils => rwa_utils}/src/lib.rs | 0 29 files changed, 529 insertions(+), 249 deletions(-) rename programs/Anchor.toml => Anchor.toml (77%) rename programs/Cargo.lock => Cargo.lock (100%) create mode 100644 Cargo.toml rename programs/move.sh => move.sh (100%) delete mode 100644 programs/Cargo.toml rename {programs/rwa_utils => rwa_utils}/Cargo.toml (100%) rename {programs/rwa_utils => rwa_utils}/src/constants.rs (100%) rename {programs/rwa_utils => rwa_utils}/src/geyser.rs (100%) rename {programs/rwa_utils => rwa_utils}/src/lib.rs (100%) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4911042..e585d0e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -60,7 +60,7 @@ jobs: - run: cd clients/rwa-token-sdk && yarn install - run: cd clients/rwa-token-sdk && yarn lint - run: solana-keygen new --no-bip39-passphrase - - run: cd programs && cargo fmt -- --check - - run: cd programs && cargo clippy -- -D warnings - - run: cd programs && anchor build - - run: cd programs && anchor test \ No newline at end of file + - run: cargo fmt -- --check + - run: cargo clippy -- -D warnings + - run: anchor build + - run: anchor test \ No newline at end of file diff --git a/programs/Anchor.toml b/Anchor.toml similarity index 77% rename from programs/Anchor.toml rename to Anchor.toml index 0e5558b..421f2a2 100644 --- a/programs/Anchor.toml +++ b/Anchor.toml @@ -10,10 +10,10 @@ wallet = "~/.config/solana/id.json" [workspace] members = [ - "asset_controller", - "data_registry", - "identity_registry", - "policy_engine", + "programs/asset_controller", + "programs/data_registry", + "programs/identity_registry", + "programs/policy_engine", ] [programs.localnet] @@ -23,7 +23,7 @@ identity_registry = "idtynCMYbdisCTv4FrCWPSQboZb1uM4TV2cPi79yxQf" policy_engine = "po1cPf1eyUJJPqULw4so3T4JU9pdFn83CDyuLEKFAau" [scripts] -test = "cd ../clients/rwa-token-sdk && yarn test" +test = "cd clients/rwa-token-sdk && yarn test" [test.validator] url = "https://api.mainnet-beta.solana.com" diff --git a/programs/Cargo.lock b/Cargo.lock similarity index 100% rename from programs/Cargo.lock rename to Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..2ecbd18 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,27 @@ +[workspace] +members = [ + "programs/asset_controller", + "programs/data_registry", + "programs/identity_registry", + "programs/policy_engine", + "rwa_utils" +] +resolver = "2" + +[profile.dev] +overflow-checks = true + +[profile.release] +overflow-checks = true +lto = "fat" +codegen-units = 1 + +[profile.release.build-override] +opt-level = 3 +incremental = false +codegen-units = 1 + +[workspace.dependencies] +anchor-lang = { git = "https://github.com/bridgesplit/anchor" } +anchor-spl = { git = "https://github.com/bridgesplit/anchor" } +rwa_utils = { path = "./rwa_utils" } \ No newline at end of file diff --git a/clients/rwa-token-sdk/src/asset-controller/instructions.ts b/clients/rwa-token-sdk/src/asset-controller/instructions.ts index 2ddca1a..48eb740 100644 --- a/clients/rwa-token-sdk/src/asset-controller/instructions.ts +++ b/clients/rwa-token-sdk/src/asset-controller/instructions.ts @@ -265,6 +265,11 @@ export async function getTransferTokensIxs( isWritable: false, isSigner: false, }, + { + pubkey: getIdentityAccountPda(args.assetMint, args.from), + isWritable: false, + isSigner: false, + } ]; const ixs: TransactionInstruction[] = []; @@ -661,6 +666,11 @@ export async function getRevokeTokensIx( isWritable: false, isSigner: false, }, + { + pubkey: getIdentityAccountPda(args.assetMint, args.owner), + isWritable: false, + isSigner: false, + } ]; const ixs: TransactionInstruction[] = []; const ix = await assetProgram.methods diff --git a/clients/rwa-token-sdk/src/programs/idls/DataRegistry.json b/clients/rwa-token-sdk/src/programs/idls/DataRegistry.json index b94f0cf..52192d8 100644 --- a/clients/rwa-token-sdk/src/programs/idls/DataRegistry.json +++ b/clients/rwa-token-sdk/src/programs/idls/DataRegistry.json @@ -167,6 +167,7 @@ "accounts": [ { "name": "signer", + "writable": true, "signer": true }, { diff --git a/clients/rwa-token-sdk/src/programs/idls/IdentityRegistry.json b/clients/rwa-token-sdk/src/programs/idls/IdentityRegistry.json index dc25374..6d06b1b 100644 --- a/clients/rwa-token-sdk/src/programs/idls/IdentityRegistry.json +++ b/clients/rwa-token-sdk/src/programs/idls/IdentityRegistry.json @@ -53,6 +53,10 @@ ] } }, + { + "name": "identity_metadata_account", + "writable": true + }, { "name": "system_program", "address": "11111111111111111111111111111111" @@ -217,6 +221,56 @@ } ] }, + { + "name": "edit_identity_metdata", + "docs": [ + "edit identity metadata" + ], + "discriminator": [ + 140, + 213, + 200, + 29, + 44, + 134, + 114, + 206 + ], + "accounts": [ + { + "name": "payer", + "writable": true, + "signer": true + }, + { + "name": "signer", + "signer": true + }, + { + "name": "identity_registry" + }, + { + "name": "identity_metadata_account", + "writable": true + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "level", + "type": "u8" + }, + { + "name": "max_allowed", + "type": { + "option": "u64" + } + } + ] + }, { "name": "remove_level_from_identity_account", "docs": [ @@ -262,6 +316,10 @@ ] } }, + { + "name": "identity_metadata_account", + "writable": true + }, { "name": "system_program", "address": "11111111111111111111111111111111" @@ -351,6 +409,19 @@ 158 ] }, + { + "name": "IdentityMetadataAccount", + "discriminator": [ + 170, + 131, + 34, + 74, + 232, + 122, + 201, + 9 + ] + }, { "name": "IdentityRegistryAccount", "discriminator": [ @@ -385,6 +456,11 @@ "code": 6003, "name": "UnauthorizedSigner", "msg": "Unauthorized signer" + }, + { + "code": 6004, + "name": "LimitReached", + "msg": "Identity limit reached" } ], "types": [ @@ -421,6 +497,49 @@ ] } }, + { + "name": "IdentityMetadataAccount", + "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": "level", + "docs": [ + "identity level" + ], + "type": "u8" + }, + { + "name": "current_users", + "docs": [ + "current number of users" + ], + "type": "u64" + }, + { + "name": "max_users", + "docs": [ + "max number of users" + ], + "type": "u64" + } + ] + } + }, { "name": "IdentityRegistryAccount", "type": { diff --git a/clients/rwa-token-sdk/src/programs/idls/PolicyEngine.json b/clients/rwa-token-sdk/src/programs/idls/PolicyEngine.json index 90c410d..a3e50d7 100644 --- a/clients/rwa-token-sdk/src/programs/idls/PolicyEngine.json +++ b/clients/rwa-token-sdk/src/programs/idls/PolicyEngine.json @@ -167,10 +167,46 @@ "name": "asset_mint" }, { - "name": "policy_engine", + "name": "policy_engine_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "asset_mint" + } + ] + } + }, + { + "name": "extra_metas_account", "writable": true, "pda": { "seeds": [ + { + "kind": "const", + "value": [ + 101, + 120, + 116, + 114, + 97, + 45, + 97, + 99, + 99, + 111, + 117, + 110, + 116, + 45, + 109, + 101, + 116, + 97, + 115 + ] + }, { "kind": "account", "path": "asset_mint" @@ -348,93 +384,7 @@ ], "accounts": [ { - "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": "source_account" }, { "name": "asset_mint" @@ -480,10 +430,6 @@ ] } }, - { - "name": "policy_engine", - "address": "po1cPf1eyUJJPqULw4so3T4JU9pdFn83CDyuLEKFAau" - }, { "name": "policy_engine_account" }, @@ -495,18 +441,21 @@ "name": "identity_registry_account" }, { - "name": "identity_account", - "writable": true + "name": "receiver_identity_account" }, { "name": "tracker_account", "writable": true }, { - "name": "policy_account" + "name": "policy_account", + "writable": true }, { "name": "instructions_program" + }, + { + "name": "source_identity_account" } ], "args": [ @@ -638,6 +587,26 @@ "code": 6015, "name": "TransferHistoryFull", "msg": "Transfer history full" + }, + { + "code": 6016, + "name": "TransferPaused", + "msg": "All Transfers have been paused" + }, + { + "code": 6017, + "name": "ForceFullTransfer", + "msg": "Expected source account to transfer full amount" + }, + { + "code": 6018, + "name": "HolderLimitExceeded", + "msg": "Holder limit exceeded" + }, + { + "code": 6019, + "name": "BalanceLimitExceeded", + "msg": "Balance limit exceeded" } ], "types": [ @@ -838,6 +807,38 @@ "type": "u64" } ] + }, + { + "name": "TransferPause" + }, + { + "name": "ForceFullTransfer" + }, + { + "name": "HolderLimit", + "fields": [ + { + "name": "limit", + "type": "u64" + }, + { + "name": "current_holders", + "type": "u64" + } + ] + }, + { + "name": "BalanceLimit", + "fields": [ + { + "name": "limit", + "type": "u128" + }, + { + "name": "current_balance", + "type": "u128" + } + ] } ] } diff --git a/clients/rwa-token-sdk/src/programs/types/DataRegistryTypes.ts b/clients/rwa-token-sdk/src/programs/types/DataRegistryTypes.ts index 90f61f7..9d7b17a 100644 --- a/clients/rwa-token-sdk/src/programs/types/DataRegistryTypes.ts +++ b/clients/rwa-token-sdk/src/programs/types/DataRegistryTypes.ts @@ -173,6 +173,7 @@ export type DataRegistry = { "accounts": [ { "name": "signer", + "writable": true, "signer": true }, { diff --git a/clients/rwa-token-sdk/src/programs/types/IdentityRegistryTypes.ts b/clients/rwa-token-sdk/src/programs/types/IdentityRegistryTypes.ts index 1f0ceeb..cfceb5b 100644 --- a/clients/rwa-token-sdk/src/programs/types/IdentityRegistryTypes.ts +++ b/clients/rwa-token-sdk/src/programs/types/IdentityRegistryTypes.ts @@ -59,6 +59,10 @@ export type IdentityRegistry = { ] } }, + { + "name": "identityMetadataAccount", + "writable": true + }, { "name": "systemProgram", "address": "11111111111111111111111111111111" @@ -223,6 +227,56 @@ export type IdentityRegistry = { } ] }, + { + "name": "editIdentityMetdata", + "docs": [ + "edit identity metadata" + ], + "discriminator": [ + 140, + 213, + 200, + 29, + 44, + 134, + 114, + 206 + ], + "accounts": [ + { + "name": "payer", + "writable": true, + "signer": true + }, + { + "name": "signer", + "signer": true + }, + { + "name": "identityRegistry" + }, + { + "name": "identityMetadataAccount", + "writable": true + }, + { + "name": "systemProgram", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "level", + "type": "u8" + }, + { + "name": "maxAllowed", + "type": { + "option": "u64" + } + } + ] + }, { "name": "removeLevelFromIdentityAccount", "docs": [ @@ -268,6 +322,10 @@ export type IdentityRegistry = { ] } }, + { + "name": "identityMetadataAccount", + "writable": true + }, { "name": "systemProgram", "address": "11111111111111111111111111111111" @@ -357,6 +415,19 @@ export type IdentityRegistry = { 158 ] }, + { + "name": "identityMetadataAccount", + "discriminator": [ + 170, + 131, + 34, + 74, + 232, + 122, + 201, + 9 + ] + }, { "name": "identityRegistryAccount", "discriminator": [ @@ -391,6 +462,11 @@ export type IdentityRegistry = { "code": 6003, "name": "unauthorizedSigner", "msg": "Unauthorized signer" + }, + { + "code": 6004, + "name": "limitReached", + "msg": "Identity limit reached" } ], "types": [ @@ -427,6 +503,49 @@ export type IdentityRegistry = { ] } }, + { + "name": "identityMetadataAccount", + "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": "level", + "docs": [ + "identity level" + ], + "type": "u8" + }, + { + "name": "currentUsers", + "docs": [ + "current number of users" + ], + "type": "u64" + }, + { + "name": "maxUsers", + "docs": [ + "max number of users" + ], + "type": "u64" + } + ] + } + }, { "name": "identityRegistryAccount", "type": { diff --git a/clients/rwa-token-sdk/src/programs/types/PolicyEngineTypes.ts b/clients/rwa-token-sdk/src/programs/types/PolicyEngineTypes.ts index 098a58b..fb4b042 100644 --- a/clients/rwa-token-sdk/src/programs/types/PolicyEngineTypes.ts +++ b/clients/rwa-token-sdk/src/programs/types/PolicyEngineTypes.ts @@ -173,10 +173,46 @@ export type PolicyEngine = { "name": "assetMint" }, { - "name": "policyEngine", + "name": "policyEngineAccount", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "assetMint" + } + ] + } + }, + { + "name": "extraMetasAccount", "writable": true, "pda": { "seeds": [ + { + "kind": "const", + "value": [ + 101, + 120, + 116, + 114, + 97, + 45, + 97, + 99, + 99, + 111, + 117, + 110, + 116, + 45, + 109, + 101, + 116, + 97, + 115 + ] + }, { "kind": "account", "path": "assetMint" @@ -354,93 +390,7 @@ export type PolicyEngine = { ], "accounts": [ { - "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": "sourceAccount" }, { "name": "assetMint" @@ -486,10 +436,6 @@ export type PolicyEngine = { ] } }, - { - "name": "policyEngine", - "address": "po1cPf1eyUJJPqULw4so3T4JU9pdFn83CDyuLEKFAau" - }, { "name": "policyEngineAccount" }, @@ -501,18 +447,21 @@ export type PolicyEngine = { "name": "identityRegistryAccount" }, { - "name": "identityAccount", - "writable": true + "name": "receiverIdentityAccount" }, { "name": "trackerAccount", "writable": true }, { - "name": "policyAccount" + "name": "policyAccount", + "writable": true }, { "name": "instructionsProgram" + }, + { + "name": "sourceIdentityAccount" } ], "args": [ @@ -644,6 +593,26 @@ export type PolicyEngine = { "code": 6015, "name": "transferHistoryFull", "msg": "Transfer history full" + }, + { + "code": 6016, + "name": "transferPaused", + "msg": "All Transfers have been paused" + }, + { + "code": 6017, + "name": "forceFullTransfer", + "msg": "Expected source account to transfer full amount" + }, + { + "code": 6018, + "name": "holderLimitExceeded", + "msg": "Holder limit exceeded" + }, + { + "code": 6019, + "name": "balanceLimitExceeded", + "msg": "Balance limit exceeded" } ], "types": [ @@ -844,6 +813,38 @@ export type PolicyEngine = { "type": "u64" } ] + }, + { + "name": "transferPause" + }, + { + "name": "forceFullTransfer" + }, + { + "name": "holderLimit", + "fields": [ + { + "name": "limit", + "type": "u64" + }, + { + "name": "currentHolders", + "type": "u64" + } + ] + }, + { + "name": "balanceLimit", + "fields": [ + { + "name": "limit", + "type": "u128" + }, + { + "name": "currentBalance", + "type": "u128" + } + ] } ] } diff --git a/clients/rwa-token-sdk/tests/e2e.test.ts b/clients/rwa-token-sdk/tests/e2e.test.ts index 3d22955..37886b8 100644 --- a/clients/rwa-token-sdk/tests/e2e.test.ts +++ b/clients/rwa-token-sdk/tests/e2e.test.ts @@ -72,21 +72,21 @@ describe("e2e tests", async () => { payer: setup.payer.toString(), owner: setup.user1.toString(), signer: setup.authority.toString(), - level: 1, + levels: [1], }, rwaClient.provider); const setupUser2Ixs = await getSetupUserIxs({ assetMint: mint, payer: setup.payer.toString(), owner: setup.user2.toString(), signer: setup.authority.toString(), - level: 1, + levels: [1], }, rwaClient.provider); const setupUser3Ixs = await getSetupUserIxs({ assetMint: mint, payer: setup.payer.toString(), owner: setup.authority.toString(), signer: setup.authority.toString(), - level: 255, + levels: [255], }, rwaClient.provider); const txnId = await sendAndConfirmTransaction( rwaClient.provider.connection, diff --git a/clients/rwa-token-sdk/tests/extensions.test.ts b/clients/rwa-token-sdk/tests/extensions.test.ts index 769ab32..1b0b554 100644 --- a/clients/rwa-token-sdk/tests/extensions.test.ts +++ b/clients/rwa-token-sdk/tests/extensions.test.ts @@ -66,7 +66,7 @@ describe("extension tests", async () => { payer: setup.payer.toString(), owner: setup.authority.toString(), signer: setup.authority.toString(), - level: 255, + levels: [255], }, rwaClient.provider ); diff --git a/clients/rwa-token-sdk/tests/tracker.test.ts b/clients/rwa-token-sdk/tests/tracker.test.ts index 2b68b69..badde57 100644 --- a/clients/rwa-token-sdk/tests/tracker.test.ts +++ b/clients/rwa-token-sdk/tests/tracker.test.ts @@ -70,7 +70,7 @@ describe("test suite to test tracker account is being updated correctly on trans owner: setup.user1.toString(), signer: setup.authority.toString(), assetMint: mint, - level: 1, + levels: [1], }; const setupIx1 = await rwaClient.identityRegistry.setupUserIxns( setupUser1Args @@ -86,7 +86,7 @@ describe("test suite to test tracker account is being updated correctly on trans owner: setup.user2.toString(), signer: setup.authority.toString(), assetMint: mint, - level: 1, + levels: [1], }; const setupIx2 = await rwaClient.identityRegistry.setupUserIxns( setupUser2Args diff --git a/programs/move.sh b/move.sh similarity index 100% rename from programs/move.sh rename to move.sh diff --git a/programs/Cargo.toml b/programs/Cargo.toml deleted file mode 100644 index 46e8c32..0000000 --- a/programs/Cargo.toml +++ /dev/null @@ -1,18 +0,0 @@ -[workspace] -members = [ - "asset_controller", - "data_registry", - "identity_registry", - "policy_engine", - "rwa_utils" -] - -[profile.release] -overflow-checks = true -lto = "fat" -codegen-units = 1 - -[profile.release.build-override] -opt-level = 3 -incremental = false -codegen-units = 1 diff --git a/programs/asset_controller/Cargo.toml b/programs/asset_controller/Cargo.toml index 995b8f4..fac068c 100644 --- a/programs/asset_controller/Cargo.toml +++ b/programs/asset_controller/Cargo.toml @@ -18,12 +18,12 @@ default = [] idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] [dependencies] -anchor-lang = { git = "https://github.com/bridgesplit/anchor", features = ["init-if-needed", "event-cpi"] } -anchor-spl = { git = "https://github.com/bridgesplit/anchor", features = ["token_2022_extensions", "token_2022"] } +anchor-lang = { workspace = true, features = ["init-if-needed", "event-cpi"] } +anchor-spl = { workspace = true, features = ["token_2022_extensions", "token_2022"] } policy_engine = { path = "../policy_engine", features = ["cpi"] } identity_registry = { path = "../identity_registry", features = ["cpi"] } data_registry = { path = "../data_registry", features = ["cpi"] } -rwa_utils = { path = "../rwa_utils" } +rwa_utils = { workspace = true } serde_json = "1.0" spl-token-2022 = { version = "3.0.2", features = ["serde-traits"]} spl-transfer-hook-interface = { version = "0.6.3" } diff --git a/programs/data_registry/Cargo.toml b/programs/data_registry/Cargo.toml index 5e768b6..d4fb7ce 100644 --- a/programs/data_registry/Cargo.toml +++ b/programs/data_registry/Cargo.toml @@ -20,6 +20,6 @@ default = [] idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] [dependencies] -anchor-lang = { git = "https://github.com/bridgesplit/anchor" } -anchor-spl = { git = "https://github.com/bridgesplit/anchor" } -rwa_utils = { path = "../rwa_utils" } +anchor-lang = { workspace = true } +anchor-spl = { workspace = true } +rwa_utils = { workspace = true } diff --git a/programs/identity_registry/Cargo.toml b/programs/identity_registry/Cargo.toml index 4633b47..d9aa674 100644 --- a/programs/identity_registry/Cargo.toml +++ b/programs/identity_registry/Cargo.toml @@ -20,6 +20,6 @@ default = [] idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] [dependencies] -anchor-lang = { git = "https://github.com/bridgesplit/anchor" } -anchor-spl = { git = "https://github.com/bridgesplit/anchor" } -rwa_utils = { path = "../rwa_utils" } +anchor-lang = { workspace = true, features = ["init-if-needed"] } +anchor-spl = { workspace = true } +rwa_utils = { workspace = true } diff --git a/programs/identity_registry/src/instructions/account/add.rs b/programs/identity_registry/src/instructions/account/add.rs index 5b08dfa..9152fe3 100644 --- a/programs/identity_registry/src/instructions/account/add.rs +++ b/programs/identity_registry/src/instructions/account/add.rs @@ -31,18 +31,20 @@ pub struct AddLevelToIdentityAccount<'info> { seeds = [&[level], identity_account.identity_registry.as_ref()], bump, )] - pub limit_account: Box>, + pub identity_metadata_account: Box>, pub system_program: Program<'info, System>, } pub fn handler(ctx: Context, level: u8) -> Result<()> { // init limit account if level is not present - if ctx.accounts.limit_account.level == 0 { - ctx.accounts - .limit_account - .new(ctx.accounts.identity_registry.key(), level, u64::MAX); + if ctx.accounts.identity_metadata_account.level == 0 { + ctx.accounts.identity_metadata_account.new( + ctx.accounts.identity_registry.key(), + level, + u64::MAX, + ); } ctx.accounts.identity_account.add_level(level)?; - ctx.accounts.limit_account.add_user()?; + ctx.accounts.identity_metadata_account.add_user()?; Ok(()) } diff --git a/programs/identity_registry/src/instructions/account/remove.rs b/programs/identity_registry/src/instructions/account/remove.rs index c681450..621694b 100644 --- a/programs/identity_registry/src/instructions/account/remove.rs +++ b/programs/identity_registry/src/instructions/account/remove.rs @@ -26,12 +26,12 @@ pub struct RemoveLevelFromIdentityAccount<'info> { seeds = [&[level], identity_account.identity_registry.as_ref()], bump, )] - pub limit_account: Box>, + pub identity_metadata_account: Box>, pub system_program: Program<'info, System>, } pub fn handler(ctx: Context, level: u8) -> Result<()> { ctx.accounts.identity_account.remove_level(level)?; - ctx.accounts.limit_account.remove_user()?; + ctx.accounts.identity_metadata_account.remove_user()?; Ok(()) } diff --git a/programs/identity_registry/src/instructions/metadata.rs b/programs/identity_registry/src/instructions/metadata.rs index 9989c95..5c2a42d 100644 --- a/programs/identity_registry/src/instructions/metadata.rs +++ b/programs/identity_registry/src/instructions/metadata.rs @@ -21,7 +21,7 @@ pub struct EditIdentityMetadata<'info> { seeds = [&[level], identity_registry.key().as_ref()], bump, )] - pub limit_account: Box>, + pub identity_metadata_account: Box>, pub system_program: Program<'info, System>, } @@ -32,10 +32,12 @@ pub fn handler( ) -> Result<()> { let max_allowed = max_allowed.unwrap_or(u64::MAX); // init limit account if level is not present - if ctx.accounts.limit_account.level == 0 { - ctx.accounts - .limit_account - .new(ctx.accounts.identity_registry.key(), level, max_allowed); + if ctx.accounts.identity_metadata_account.level == 0 { + ctx.accounts.identity_metadata_account.new( + ctx.accounts.identity_registry.key(), + level, + max_allowed, + ); } Ok(()) } diff --git a/programs/policy_engine/Cargo.toml b/programs/policy_engine/Cargo.toml index df8833d..764eb41 100644 --- a/programs/policy_engine/Cargo.toml +++ b/programs/policy_engine/Cargo.toml @@ -17,9 +17,9 @@ default = [] idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] [dependencies] -anchor-lang = { git = "https://github.com/bridgesplit/anchor", features = ["interface-instructions", "event-cpi"] } -anchor-spl = { git = "https://github.com/bridgesplit/anchor", features = ["token_2022_extensions"] } -rwa_utils = { path = "../rwa_utils" } +anchor-lang = { workspace = true, features = ["interface-instructions", "event-cpi"] } +anchor-spl = { workspace = true, features = ["token_2022_extensions"] } +rwa_utils = { workspace = true } identity_registry = { path = "../identity_registry", features = ["no-entrypoint"] } serde = "1.0" diff --git a/programs/policy_engine/src/state/account.rs b/programs/policy_engine/src/state/account.rs index 834b424..c5e4e55 100644 --- a/programs/policy_engine/src/state/account.rs +++ b/programs/policy_engine/src/state/account.rs @@ -136,6 +136,7 @@ impl PolicyAccount { /// enforces different types of policies #[inline(never)] + #[allow(clippy::too_many_arguments)] pub fn enforce_policy( &mut self, transfer_amount: u64, diff --git a/programs/policy_engine/src/utils.rs b/programs/policy_engine/src/utils.rs index 3f46acb..185c4fb 100644 --- a/programs/policy_engine/src/utils.rs +++ b/programs/policy_engine/src/utils.rs @@ -119,7 +119,7 @@ pub fn get_extra_account_metas() -> Result> { false, false, )?, - // user identity account + // receiver identity account ExtraAccountMeta::new_external_pda_with_seeds( 6, &[ @@ -151,5 +151,19 @@ pub fn get_extra_account_metas() -> Result> { ExtraAccountMeta::new_with_seeds(&[Seed::AccountKey { index: 5 }], false, true)?, // instructions program ExtraAccountMeta::new_with_pubkey(&sysvar::instructions::id(), false, false)?, + // source identity account + ExtraAccountMeta::new_external_pda_with_seeds( + 6, + &[ + Seed::AccountKey { index: 7 }, + Seed::AccountData { + account_index: 0, + data_index: 32, + length: 32, + }, + ], + false, + false, + )?, ]) } diff --git a/programs/rwa_utils/Cargo.toml b/rwa_utils/Cargo.toml similarity index 100% rename from programs/rwa_utils/Cargo.toml rename to rwa_utils/Cargo.toml diff --git a/programs/rwa_utils/src/constants.rs b/rwa_utils/src/constants.rs similarity index 100% rename from programs/rwa_utils/src/constants.rs rename to rwa_utils/src/constants.rs diff --git a/programs/rwa_utils/src/geyser.rs b/rwa_utils/src/geyser.rs similarity index 100% rename from programs/rwa_utils/src/geyser.rs rename to rwa_utils/src/geyser.rs diff --git a/programs/rwa_utils/src/lib.rs b/rwa_utils/src/lib.rs similarity index 100% rename from programs/rwa_utils/src/lib.rs rename to rwa_utils/src/lib.rs From bb4b969640fbf48083b59c1178a1a6ec660a682f Mon Sep 17 00:00:00 2001 From: Bhargava Sai Macha Date: Mon, 7 Oct 2024 12:15:42 -0400 Subject: [PATCH 6/8] add more tests --- clients/rwa-token-sdk/package.json | 4 +- .../tests/policies/balance_limit.test.ts | 245 ++++++++++++++++++ .../policies/force_full_transfer.test.ts | 153 +++++++++++ .../tests/policies/holder_limit.test.ts | 171 ++++++++++++ .../tests/policies/transfer_pause.test.ts | 134 ++++++++++ .../policy_engine/src/instructions/execute.rs | 4 +- programs/policy_engine/src/state/account.rs | 4 +- 7 files changed, 709 insertions(+), 6 deletions(-) create mode 100644 clients/rwa-token-sdk/tests/policies/balance_limit.test.ts create mode 100644 clients/rwa-token-sdk/tests/policies/force_full_transfer.test.ts create mode 100644 clients/rwa-token-sdk/tests/policies/holder_limit.test.ts create mode 100644 clients/rwa-token-sdk/tests/policies/transfer_pause.test.ts diff --git a/clients/rwa-token-sdk/package.json b/clients/rwa-token-sdk/package.json index 52f7ef0..d771007 100644 --- a/clients/rwa-token-sdk/package.json +++ b/clients/rwa-token-sdk/package.json @@ -44,8 +44,8 @@ "author": "Standard Labs, Inc.", "contributors": [ "Luke Truitt ", - "Bhargava Sai Macha ", - "Chris Hagedorn " + "Bhargava Sai Macha ", + "Chris Hagedorn " ], "license": "MIT" } diff --git a/clients/rwa-token-sdk/tests/policies/balance_limit.test.ts b/clients/rwa-token-sdk/tests/policies/balance_limit.test.ts new file mode 100644 index 0000000..b6f25bc --- /dev/null +++ b/clients/rwa-token-sdk/tests/policies/balance_limit.test.ts @@ -0,0 +1,245 @@ +import { BN, Wallet } from "@coral-xyz/anchor"; +import { + getTransferTokensIxs, + 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 additional policies", async () => { + let rwaClient: RwaClient; + let mint: string; + const setup = await setupTests(); + const decimals = 2; + + test("setup environment", 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)); + + // Create asset controller + 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, setup.authorityKp, ...setupAssetController.signers] + ); + mint = setupAssetController.signers[0].publicKey.toString(); + expect(txnId).toBeTruthy(); + + // Setup users + const setupUser1 = await rwaClient.identityRegistry.setupUserIxns({ + payer: setup.payer.toString(), + owner: setup.user1.toString(), + assetMint: mint, + levels: [1], + signer: setup.authorityKp.publicKey.toString() + }); + await sendAndConfirmTransaction( + setup.provider.connection, + new Transaction().add(...setupUser1.ixs), + [setup.payerKp, setup.authorityKp, ...setupUser1.signers] + ); + + const setupUser2 = await rwaClient.identityRegistry.setupUserIxns({ + payer: setup.payer.toString(), + owner: setup.user2.toString(), + assetMint: mint, + levels: [1], + signer: setup.authorityKp.publicKey.toString() + }); + await sendAndConfirmTransaction( + setup.provider.connection, + new Transaction().add(...setupUser2.ixs), + [setup.payerKp, setup.authorityKp, ...setupUser2.signers] + ); + + // Issue tokens to user1 + const issueTokens = await rwaClient.assetController.issueTokenIxns({ + authority: setup.authority.toString(), + payer: setup.payer.toString(), + owner: setup.user1.toString(), + assetMint: mint, + amount: 1000000, + }); + await sendAndConfirmTransaction( + setup.provider.connection, + new Transaction().add(...issueTokens), + [setup.payerKp, setup.authorityKp] + ); + }); + + describe("test BalanceLimit policy", async () => { + const balanceLimit = new BN(1500000); // 15,000 tokens with 2 decimals + let currentBalance = new BN(1000000); // Starting balance for the policy (10,000 tokens) + + test("attach BalanceLimit policy", async () => { + const attachPolicy = await rwaClient.policyEngine.createPolicy({ + payer: setup.payer.toString(), + assetMint: mint, + authority: setup.authority.toString(), + identityFilter: { + identityLevels: [1], + comparisionType: { or: {} }, + }, + policyType: { + balanceLimit: { + limit: balanceLimit, + currentBalance: currentBalance + } + }, + }); + const txnId = await sendAndConfirmTransaction( + setup.provider.connection, + new Transaction().add(...attachPolicy.ixs), + [setup.payerKp, setup.authorityKp, ...attachPolicy.signers] + ); + expect(txnId).toBeTruthy(); + }); + + test("transfer within group (should not affect total balance)", async () => { + const transferAmount = 200000; // 2,000 tokens + const transferTokensIxs = await getTransferTokensIxs({ + from: setup.user1.toString(), + to: setup.user2.toString(), + assetMint: mint, + amount: transferAmount, + decimals, + createTa: true, + }, rwaClient.provider); + + const txnId = await sendAndConfirmTransaction( + setup.provider.connection, + new Transaction().add(...transferTokensIxs), + [setup.user1Kp], + ); + expect(txnId).toBeTruthy(); + // currentBalance remains unchanged as this is a transfer within the group + }); + + test("transfer into group within limit", async () => { + // Create and setup user3 without identity level 1 + const setupUser3 = await rwaClient.identityRegistry.setupUserIxns({ + payer: setup.payer.toString(), + owner: setup.user3.toString(), + assetMint: mint, + levels: [2], // Different identity level + signer: setup.authorityKp.publicKey.toString() + }); + await sendAndConfirmTransaction( + setup.provider.connection, + new Transaction().add(...setupUser3.ixs), + [setup.payerKp, setup.authorityKp, ...setupUser3.signers] + ); + + // Issue tokens to user3 + const issueAmount = 400000; // 4,000 tokens + const issueTokens = await rwaClient.assetController.issueTokenIxns({ + authority: setup.authority.toString(), + payer: setup.payer.toString(), + owner: setup.user3.toString(), + assetMint: mint, + amount: issueAmount, + }); + await sendAndConfirmTransaction( + setup.provider.connection, + new Transaction().add(...issueTokens), + [setup.payerKp, setup.authorityKp] + ); + + // Transfer from user3 to user1 (into the group) + const transferTokensIxs = await getTransferTokensIxs({ + from: setup.user3.toString(), + to: setup.user1.toString(), + assetMint: mint, + amount: issueAmount, + decimals, + }, rwaClient.provider); + + const txnId = await sendAndConfirmTransaction( + setup.provider.connection, + new Transaction().add(...transferTokensIxs), + [setup.user3Kp], + ); + expect(txnId).toBeTruthy(); + currentBalance = currentBalance.add(new BN(issueAmount)); + }); + + test("attempt transfer into group exceeding limit", async () => { + // Issue more tokens to user3 + const issueAmount = 200000; // 2,000 tokens + const issueTokens = await rwaClient.assetController.issueTokenIxns({ + authority: setup.authority.toString(), + payer: setup.payer.toString(), + owner: setup.user3.toString(), + assetMint: mint, + amount: issueAmount, + }); + await sendAndConfirmTransaction( + setup.provider.connection, + new Transaction().add(...issueTokens), + [setup.payerKp, setup.authorityKp] + ); + + // Attempt to transfer from user3 to user1 (which would exceed the limit) + const transferTokensIxs = await getTransferTokensIxs({ + from: setup.user3.toString(), + to: setup.user1.toString(), + assetMint: mint, + amount: issueAmount, + decimals, + }, rwaClient.provider); + + await expect(sendAndConfirmTransaction( + setup.provider.connection, + new Transaction().add(...transferTokensIxs), + [setup.user3Kp], + )).rejects.toThrow(/custom program error: 0x1782/); + }); + + test("transfer out of group (should reduce total balance)", async () => { + const transferAmount = 200000; // 2,000 tokens + const transferTokensIxs = await getTransferTokensIxs({ + from: setup.user1.toString(), + to: setup.user3.toString(), // user3 is not in the identity group + assetMint: mint, + amount: transferAmount, + decimals, + }, rwaClient.provider); + + const txnId = await sendAndConfirmTransaction( + setup.provider.connection, + new Transaction().add(...transferTokensIxs), + [setup.user1Kp], + ); + expect(txnId).toBeTruthy(); + currentBalance = currentBalance.sub(new BN(transferAmount)); + }); + }); + +}); \ No newline at end of file diff --git a/clients/rwa-token-sdk/tests/policies/force_full_transfer.test.ts b/clients/rwa-token-sdk/tests/policies/force_full_transfer.test.ts new file mode 100644 index 0000000..bece563 --- /dev/null +++ b/clients/rwa-token-sdk/tests/policies/force_full_transfer.test.ts @@ -0,0 +1,153 @@ +import { Wallet } from "@coral-xyz/anchor"; +import { + getTransferTokensIxs, + 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 additional policies", async () => { + let rwaClient: RwaClient; + let mint: string; + const setup = await setupTests(); + const decimals = 2; + + test("setup environment", 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)); + + // Create asset controller + 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, setup.authorityKp, ...setupAssetController.signers] + ); + mint = setupAssetController.signers[0].publicKey.toString(); + expect(txnId).toBeTruthy(); + + // Setup users + const setupUser1 = await rwaClient.identityRegistry.setupUserIxns({ + payer: setup.payer.toString(), + owner: setup.user1.toString(), + assetMint: mint, + levels: [1], + signer: setup.authorityKp.publicKey.toString() + }); + await sendAndConfirmTransaction( + setup.provider.connection, + new Transaction().add(...setupUser1.ixs), + [setup.payerKp, setup.authorityKp, ...setupUser1.signers] + ); + + const setupUser2 = await rwaClient.identityRegistry.setupUserIxns({ + payer: setup.payer.toString(), + owner: setup.user2.toString(), + assetMint: mint, + levels: [1], + signer: setup.authorityKp.publicKey.toString() + }); + await sendAndConfirmTransaction( + setup.provider.connection, + new Transaction().add(...setupUser2.ixs), + [setup.payerKp, setup.authorityKp, ...setupUser2.signers] + ); + + // Issue tokens to user1 + const issueTokens = await rwaClient.assetController.issueTokenIxns({ + authority: setup.authority.toString(), + payer: setup.payer.toString(), + owner: setup.user1.toString(), + assetMint: mint, + amount: 1000000, + }); + await sendAndConfirmTransaction( + setup.provider.connection, + new Transaction().add(...issueTokens), + [setup.payerKp, setup.authorityKp] + ); + }); + + describe("test ForceFullTransfer policy", async () => { + test("attach ForceFullTransfer policy", async () => { + const attachPolicy = await rwaClient.policyEngine.createPolicy({ + payer: setup.payer.toString(), + assetMint: mint, + authority: setup.authority.toString(), + identityFilter: { + identityLevels: [1], + comparisionType: { or: {} }, + }, + policyType: { forceFullTransfer: {} }, + }); + const txnId = await sendAndConfirmTransaction( + setup.provider.connection, + new Transaction().add(...attachPolicy.ixs), + [setup.payerKp, setup.authorityKp, ...attachPolicy.signers] + ); + expect(txnId).toBeTruthy(); + }); + + test("attempt partial transfer", async () => { + const transferTokensIxs = await getTransferTokensIxs({ + from: setup.user1.toString(), + to: setup.user2.toString(), + assetMint: mint, + amount: 10, // Partial amount + decimals, + createTa: true, + }, rwaClient.provider); + + await expect(sendAndConfirmTransaction( + setup.provider.connection, + new Transaction().add(...transferTokensIxs), + [setup.user1Kp], + )).rejects.toThrowError(/custom program error: 0x1781/); // ForceFullTransfer error + }); + + test("perform full transfer", async () => { + const transferTokensIxs = await getTransferTokensIxs({ + from: setup.user1.toString(), + to: setup.user2.toString(), + assetMint: mint, + amount: 1000000, // Full amount + decimals, + createTa: true, + }, rwaClient.provider); + + const txnId = await sendAndConfirmTransaction( + setup.provider.connection, + new Transaction().add(...transferTokensIxs), + [setup.user1Kp], + ); + expect(txnId).toBeTruthy(); + }); + }); + +}); \ No newline at end of file diff --git a/clients/rwa-token-sdk/tests/policies/holder_limit.test.ts b/clients/rwa-token-sdk/tests/policies/holder_limit.test.ts new file mode 100644 index 0000000..490bd02 --- /dev/null +++ b/clients/rwa-token-sdk/tests/policies/holder_limit.test.ts @@ -0,0 +1,171 @@ +import { BN, Wallet } from "@coral-xyz/anchor"; +import { + getTransferTokensIxs, + 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 additional policies", async () => { + let rwaClient: RwaClient; + let mint: string; + const setup = await setupTests(); + const decimals = 2; + + test("setup environment", 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)); + + // Create asset controller + 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, setup.authorityKp, ...setupAssetController.signers] + ); + mint = setupAssetController.signers[0].publicKey.toString(); + expect(txnId).toBeTruthy(); + + // Setup users + const setupUser1 = await rwaClient.identityRegistry.setupUserIxns({ + payer: setup.payer.toString(), + owner: setup.user1.toString(), + assetMint: mint, + levels: [1], + signer: setup.authorityKp.publicKey.toString() + }); + await sendAndConfirmTransaction( + setup.provider.connection, + new Transaction().add(...setupUser1.ixs), + [setup.payerKp, setup.authorityKp, ...setupUser1.signers] + ); + + const setupUser2 = await rwaClient.identityRegistry.setupUserIxns({ + payer: setup.payer.toString(), + owner: setup.user2.toString(), + assetMint: mint, + levels: [1], + signer: setup.authorityKp.publicKey.toString() + }); + await sendAndConfirmTransaction( + setup.provider.connection, + new Transaction().add(...setupUser2.ixs), + [setup.payerKp, setup.authorityKp, ...setupUser2.signers] + ); + + // Issue tokens to user1 + const issueTokens = await rwaClient.assetController.issueTokenIxns({ + authority: setup.authority.toString(), + payer: setup.payer.toString(), + owner: setup.user1.toString(), + assetMint: mint, + amount: 1000000, + }); + await sendAndConfirmTransaction( + setup.provider.connection, + new Transaction().add(...issueTokens), + [setup.payerKp, setup.authorityKp] + ); + }); + describe("test HolderLimit policy", async () => { + const holderLimit = 2; + + test("attach HolderLimit policy", async () => { + const attachPolicy = await rwaClient.policyEngine.createPolicy({ + payer: setup.payer.toString(), + assetMint: mint, + authority: setup.authority.toString(), + identityFilter: { + identityLevels: [1], + comparisionType: { or: {} }, + }, + policyType: { + holderLimit: { + limit: new BN(holderLimit), + currentHolders: new BN(1) // Assuming user1 is the only holder at this point + } + }, + }); + const txnId = await sendAndConfirmTransaction( + setup.provider.connection, + new Transaction().add(...attachPolicy.ixs), + [setup.payerKp, setup.authorityKp, ...attachPolicy.signers] + ); + expect(txnId).toBeTruthy(); + }); + + test("transfer to new holder within limit", async () => { + const transferTokensIxs = await getTransferTokensIxs({ + from: setup.user1.toString(), + to: setup.user2.toString(), + assetMint: mint, + amount: 10, + decimals, + createTa: true, + }, rwaClient.provider); + + const txnId = await sendAndConfirmTransaction( + setup.provider.connection, + new Transaction().add(...transferTokensIxs), + [setup.user1Kp], + ); + expect(txnId).toBeTruthy(); + }); + + test("attempt transfer to new holder exceeding limit", async () => { + const setupUser3 = await rwaClient.identityRegistry.setupUserIxns({ + payer: setup.payer.toString(), + owner: setup.user3.toString(), + assetMint: mint, + levels: [1], + signer: setup.authorityKp.publicKey.toString() + }); + await sendAndConfirmTransaction( + setup.provider.connection, + new Transaction().add(...setupUser3.ixs), + [setup.payerKp, setup.authorityKp, ...setupUser3.signers] + ); + + const transferTokensIxs = await getTransferTokensIxs({ + from: setup.user1.toString(), + to: setup.user3.toString(), + assetMint: mint, + amount: 10, + decimals, + createTa: true, + }, rwaClient.provider); + + await expect(sendAndConfirmTransaction( + setup.provider.connection, + new Transaction().add(...transferTokensIxs), + [setup.user1Kp], + )).rejects.toThrowError(/custom program error: 0x1782/); // HolderLimitExceeded error + }); + }); +}); \ No newline at end of file diff --git a/clients/rwa-token-sdk/tests/policies/transfer_pause.test.ts b/clients/rwa-token-sdk/tests/policies/transfer_pause.test.ts new file mode 100644 index 0000000..966d4ba --- /dev/null +++ b/clients/rwa-token-sdk/tests/policies/transfer_pause.test.ts @@ -0,0 +1,134 @@ +import { Wallet } from "@coral-xyz/anchor"; +import { + getTransferTokensIxs, + 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 additional policies", async () => { + let rwaClient: RwaClient; + let mint: string; + const setup = await setupTests(); + const decimals = 2; + + test("setup environment", 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)); + + // Create asset controller + 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, setup.authorityKp, ...setupAssetController.signers] + ); + mint = setupAssetController.signers[0].publicKey.toString(); + expect(txnId).toBeTruthy(); + + // Setup users + const setupUser1 = await rwaClient.identityRegistry.setupUserIxns({ + payer: setup.payer.toString(), + owner: setup.user1.toString(), + assetMint: mint, + levels: [1], + signer: setup.authorityKp.publicKey.toString() + }); + await sendAndConfirmTransaction( + setup.provider.connection, + new Transaction().add(...setupUser1.ixs), + [setup.payerKp, setup.authorityKp, ...setupUser1.signers] + ); + + const setupUser2 = await rwaClient.identityRegistry.setupUserIxns({ + payer: setup.payer.toString(), + owner: setup.user2.toString(), + assetMint: mint, + levels: [1], + signer: setup.authorityKp.publicKey.toString() + }); + await sendAndConfirmTransaction( + setup.provider.connection, + new Transaction().add(...setupUser2.ixs), + [setup.payerKp, setup.authorityKp, ...setupUser2.signers] + ); + + // Issue tokens to user1 + const issueTokens = await rwaClient.assetController.issueTokenIxns({ + authority: setup.authority.toString(), + payer: setup.payer.toString(), + owner: setup.user1.toString(), + assetMint: mint, + amount: 1000000, + }); + await sendAndConfirmTransaction( + setup.provider.connection, + new Transaction().add(...issueTokens), + [setup.payerKp, setup.authorityKp] + ); + }); + + describe("test TransferPause policy", async () => { + test("attach TransferPause policy", async () => { + const attachPolicy = await rwaClient.policyEngine.createPolicy({ + payer: setup.payer.toString(), + assetMint: mint, + authority: setup.authority.toString(), + identityFilter: { + identityLevels: [1], + comparisionType: { or: {} }, + }, + policyType: { transferPause: {} }, + }); + const txnId = await sendAndConfirmTransaction( + setup.provider.connection, + new Transaction().add(...attachPolicy.ixs), + [setup.payerKp, setup.authorityKp, ...attachPolicy.signers] + ); + expect(txnId).toBeTruthy(); + }); + + test("attempt transfer while paused", async () => { + const transferTokensIxs = await getTransferTokensIxs({ + from: setup.user1.toString(), + to: setup.user2.toString(), + assetMint: mint, + amount: 10, + decimals, + createTa: true, + }, rwaClient.provider); + + await expect(sendAndConfirmTransaction( + setup.provider.connection, + new Transaction().add(...transferTokensIxs), + [setup.user1Kp], + )).rejects.toThrowError(/custom program error: 0x1780/); // TransferPaused error + }); + }); +}); \ No newline at end of file diff --git a/programs/policy_engine/src/instructions/execute.rs b/programs/policy_engine/src/instructions/execute.rs index 236713f..1ade820 100644 --- a/programs/policy_engine/src/instructions/execute.rs +++ b/programs/policy_engine/src/instructions/execute.rs @@ -163,8 +163,8 @@ pub fn handler(ctx: Context, amount: u64) -> Result<()> { Clock::get()?.unix_timestamp, &source_levels, &receiver_levels, - ctx.accounts.source_account.amount, - ctx.accounts.destination_account.amount, + ctx.accounts.source_account.amount + amount, // execute runs after transfer happens, hance balance must be updated + ctx.accounts.destination_account.amount - amount, &transfers, )?; diff --git a/programs/policy_engine/src/state/account.rs b/programs/policy_engine/src/state/account.rs index c5e4e55..5bc4ee5 100644 --- a/programs/policy_engine/src/state/account.rs +++ b/programs/policy_engine/src/state/account.rs @@ -192,7 +192,7 @@ impl PolicyAccount { return Err(PolicyEngineErrors::TransferPaused.into()); } PolicyType::ForceFullTransfer => { - if enforce_identity_filter(receiver_identity, policy.identity_filter).is_ok() + if enforce_identity_filter(source_identity, policy.identity_filter).is_ok() && source_balance != transfer_amount { return Err(PolicyEngineErrors::ForceFullTransfer.into()); @@ -224,7 +224,7 @@ impl PolicyAccount { *current_balance += transfer_amount as u128; } if enforce_identity_filter(source_identity, policy.identity_filter).is_ok() { - *current_balance -= transfer_amount as u128; + *current_balance = current_balance.saturating_sub(transfer_amount as u128); } if current_balance > limit { return Err(PolicyEngineErrors::HolderLimitExceeded.into()); From 174bbfb1f52aad16bcac9be0ac6d1338d8e73329 Mon Sep 17 00:00:00 2001 From: Bhargava Sai Macha Date: Mon, 7 Oct 2024 12:23:53 -0400 Subject: [PATCH 7/8] fix build --- .../src/identity-registry/instructions.ts | 4 ++++ .../src/identity-registry/utils.ts | 12 ++++++++++++ .../src/policy-engine/instructions.ts | 4 +++- .../src/programs/idls/IdentityRegistry.json | 4 ++++ .../src/programs/types/IdentityRegistryTypes.ts | 4 ++++ .../src/instructions/account/create.rs | 17 +++++++++++++++++ 6 files changed, 44 insertions(+), 1 deletion(-) diff --git a/clients/rwa-token-sdk/src/identity-registry/instructions.ts b/clients/rwa-token-sdk/src/identity-registry/instructions.ts index af0caea..5b9c361 100644 --- a/clients/rwa-token-sdk/src/identity-registry/instructions.ts +++ b/clients/rwa-token-sdk/src/identity-registry/instructions.ts @@ -8,6 +8,7 @@ import { getIdentityAccountPda, getIdentityRegistryProgram, getIdentityRegistryPda, + getIdentityMetadataPda, } from "./utils"; import { type AnchorProvider } from "@coral-xyz/anchor"; @@ -66,6 +67,7 @@ export async function getCreateIdentityAccountIx( identityRegistry: getIdentityRegistryPda(args.assetMint), identityAccount: getIdentityAccountPda(args.assetMint, args.owner), systemProgram: SystemProgram.programId, + identityMetadataAccount: getIdentityMetadataPda(args.assetMint, args.level), signer: args.signer ? args.signer : getIdentityRegistryPda(args.assetMint), @@ -97,6 +99,7 @@ export async function getAddLevelToIdentityAccount( signer: args.signer, identityRegistry: getIdentityRegistryPda(args.assetMint), identityAccount: getIdentityAccountPda(args.assetMint, args.owner), + identityMetadataAccount: getIdentityMetadataPda(args.assetMint, args.level), payer: args.payer, systemProgram: SystemProgram.programId, }) @@ -128,6 +131,7 @@ export async function getRemoveLevelFromIdentityAccount( : getIdentityRegistryPda(args.assetMint), identityRegistry: getIdentityRegistryPda(args.assetMint), identityAccount: getIdentityAccountPda(args.assetMint, args.owner), + identityMetadataAccount: getIdentityMetadataPda(args.assetMint, args.level), payer: args.signer, systemProgram: SystemProgram.programId, }) diff --git a/clients/rwa-token-sdk/src/identity-registry/utils.ts b/clients/rwa-token-sdk/src/identity-registry/utils.ts index a18dc4c..e3e061e 100644 --- a/clients/rwa-token-sdk/src/identity-registry/utils.ts +++ b/clients/rwa-token-sdk/src/identity-registry/utils.ts @@ -32,4 +32,16 @@ export const getIdentityAccountPda = (assetMint: string, owner: string) => Publi identityRegistryProgramId, )[0]; + +/** + * Retrieves the identity metadata account pda public key for a specific asset mint. + * @param assetMint - The string representation of the asset's mint address. + * @param level - The level of the identity account. + * @returns The identity metadata pda. + */ +export const getIdentityMetadataPda = (assetMint: string, level: number) => PublicKey.findProgramAddressSync( + [Buffer.from([level]), getIdentityRegistryPda(assetMint).toBuffer()], + identityRegistryProgramId, +)[0]; + export const POLICY_SKIP_LEVEL = 255; \ No newline at end of file diff --git a/clients/rwa-token-sdk/src/policy-engine/instructions.ts b/clients/rwa-token-sdk/src/policy-engine/instructions.ts index c0c9a9c..e780b10 100644 --- a/clients/rwa-token-sdk/src/policy-engine/instructions.ts +++ b/clients/rwa-token-sdk/src/policy-engine/instructions.ts @@ -5,6 +5,7 @@ import { } from "@solana/web3.js"; import { type CommonArgs, type IxReturn } from "../utils"; import { + getExtraMetasListPda, getPolicyAccountPda, getPolicyEnginePda, getPolicyEngineProgram, @@ -39,7 +40,8 @@ export async function getCreatePolicyEngineIx( payer: args.payer, signer: args.signer, assetMint: args.assetMint, - policyEngine: getPolicyEnginePda(args.assetMint), + policyEngineAccount: getPolicyEnginePda(args.assetMint), + extraMetasAccount: getExtraMetasListPda(args.assetMint), systemProgram: SystemProgram.programId, }) .instruction(); diff --git a/clients/rwa-token-sdk/src/programs/idls/IdentityRegistry.json b/clients/rwa-token-sdk/src/programs/idls/IdentityRegistry.json index 6d06b1b..1acdbe9 100644 --- a/clients/rwa-token-sdk/src/programs/idls/IdentityRegistry.json +++ b/clients/rwa-token-sdk/src/programs/idls/IdentityRegistry.json @@ -114,6 +114,10 @@ ] } }, + { + "name": "identity_metadata_account", + "writable": true + }, { "name": "system_program", "address": "11111111111111111111111111111111" diff --git a/clients/rwa-token-sdk/src/programs/types/IdentityRegistryTypes.ts b/clients/rwa-token-sdk/src/programs/types/IdentityRegistryTypes.ts index cfceb5b..dc13036 100644 --- a/clients/rwa-token-sdk/src/programs/types/IdentityRegistryTypes.ts +++ b/clients/rwa-token-sdk/src/programs/types/IdentityRegistryTypes.ts @@ -120,6 +120,10 @@ export type IdentityRegistry = { ] } }, + { + "name": "identityMetadataAccount", + "writable": true + }, { "name": "systemProgram", "address": "11111111111111111111111111111111" diff --git a/programs/identity_registry/src/instructions/account/create.rs b/programs/identity_registry/src/instructions/account/create.rs index 50f9518..e0b2b41 100644 --- a/programs/identity_registry/src/instructions/account/create.rs +++ b/programs/identity_registry/src/instructions/account/create.rs @@ -21,10 +21,27 @@ pub struct CreateIdentityAccount<'info> { constraint = level != 0, )] pub identity_account: Box>, + #[account( + init_if_needed, + payer = payer, + space = 8 + IdentityMetadataAccount::INIT_SPACE, + seeds = [&[level], identity_account.identity_registry.as_ref()], + bump, + )] + pub identity_metadata_account: Box>, pub system_program: Program<'info, System>, } pub fn handler(ctx: Context, owner: Pubkey, level: u8) -> Result<()> { + // init limit account if level is not present + if ctx.accounts.identity_metadata_account.level == 0 { + ctx.accounts.identity_metadata_account.new( + ctx.accounts.identity_registry.key(), + level, + u64::MAX, + ); + } + ctx.accounts.identity_account.add_level(level)?; ctx.accounts .identity_account .new(owner, ctx.accounts.identity_registry.key(), level); From d8e3926bdc0d5426e22502e15dc07141c7fc0aae Mon Sep 17 00:00:00 2001 From: Bhargava Sai Macha Date: Mon, 7 Oct 2024 13:01:36 -0400 Subject: [PATCH 8/8] fix seeds --- programs/identity_registry/src/instructions/account/create.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/programs/identity_registry/src/instructions/account/create.rs b/programs/identity_registry/src/instructions/account/create.rs index e0b2b41..616e692 100644 --- a/programs/identity_registry/src/instructions/account/create.rs +++ b/programs/identity_registry/src/instructions/account/create.rs @@ -25,7 +25,7 @@ pub struct CreateIdentityAccount<'info> { init_if_needed, payer = payer, space = 8 + IdentityMetadataAccount::INIT_SPACE, - seeds = [&[level], identity_account.identity_registry.as_ref()], + seeds = [&[level], identity_registry.key().as_ref()], bump, )] pub identity_metadata_account: Box>,