diff --git a/Cargo.lock b/Cargo.lock index 8cdbf106..a631985d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3054,9 +3054,9 @@ checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" [[package]] name = "libredox" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +checksum = "1580801010e535496706ba011c15f8532df6b42297d2e471fec38ceadd8c0638" dependencies = [ "bitflags 2.9.0", "libc", @@ -3679,6 +3679,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "oracle-mapping-state" +version = "0.1.0" +source = "git+https://github.com/anagrambuild/scope-oracle-mapper#128e8cfbde71ec17aa00fe123251495a681a4f2e" +dependencies = [ + "pinocchio 0.9.0 (git+https://github.com/anza-xyz/pinocchio.git)", + "shank 0.4.3", +] + [[package]] name = "overload" version = "0.1.1" @@ -3816,6 +3825,11 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5123fe61ac87a327d434d530eaddaaf65069a37e33e5c9f798feaed29e4974c8" +[[package]] +name = "pinocchio" +version = "0.9.0" +source = "git+https://github.com/anza-xyz/pinocchio.git#55ecb47a4faeaacdc28b08ffd1158ff3c18b2bad" + [[package]] name = "pinocchio-pubkey" version = "0.2.4" @@ -3833,7 +3847,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb0225638cadcbebae8932cb7f49cb5da7c15c21beb19f048f05a5ca7d93f065" dependencies = [ "five8_const", - "pinocchio 0.9.0", + "pinocchio 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "sha2-const-stable", ] @@ -3843,7 +3857,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "141ed5eafb4ab04568bb0e224e3dc9a9de13c933de4c004e0d1a553498be3a7c" dependencies = [ - "pinocchio 0.9.0", + "pinocchio 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "pinocchio-pubkey 0.3.0", ] @@ -4923,7 +4937,16 @@ name = "shank" version = "0.4.2" source = "git+https://github.com/anagrambuild/shank.git#d4f046b22b87c896fdb77e55256d74dad6a13a68" dependencies = [ - "shank_macro", + "shank_macro 0.4.2", +] + +[[package]] +name = "shank" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b886438a24e923491c6e0f55fef7a414262e8b6bc3a69e055f591d5106959f6e" +dependencies = [ + "shank_macro 0.4.3", ] [[package]] @@ -4933,8 +4956,21 @@ source = "git+https://github.com/anagrambuild/shank.git#d4f046b22b87c896fdb77e55 dependencies = [ "proc-macro2 1.0.94", "quote 1.0.40", - "shank_macro_impl", - "shank_render", + "shank_macro_impl 0.4.2", + "shank_render 0.4.2", + "syn 1.0.109", +] + +[[package]] +name = "shank_macro" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea4973210898ea3412d187d33a846ad79090acec61032faed89c20630c72e14" +dependencies = [ + "proc-macro2 1.0.94", + "quote 1.0.40", + "shank_macro_impl 0.4.3", + "shank_render 0.4.3", "syn 1.0.109", ] @@ -4950,6 +4986,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "shank_macro_impl" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77600cea17431d53e8aa47c44d340da7ff0601eec934aca3c6676de367e5ce83" +dependencies = [ + "anyhow", + "proc-macro2 1.0.94", + "quote 1.0.40", + "serde", + "syn 1.0.109", +] + [[package]] name = "shank_render" version = "0.4.2" @@ -4957,7 +5006,18 @@ source = "git+https://github.com/anagrambuild/shank.git#d4f046b22b87c896fdb77e55 dependencies = [ "proc-macro2 1.0.94", "quote 1.0.40", - "shank_macro_impl", + "shank_macro_impl 0.4.2", +] + +[[package]] +name = "shank_render" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0893a9854124be3e16295986df3d1a546b916d98b390212552ddf6eeba7042" +dependencies = [ + "proc-macro2 1.0.94", + "quote 1.0.40", + "shank_macro_impl 0.4.3", ] [[package]] @@ -7970,6 +8030,7 @@ dependencies = [ "alloy-signer", "alloy-signer-local", "anyhow", + "base64 0.22.1", "bincode", "bs58", "bytemuck", @@ -7982,12 +8043,13 @@ dependencies = [ "num_enum", "once_cell", "openssl", - "pinocchio 0.9.0", + "oracle-mapping-state", + "pinocchio 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "pinocchio-pubkey 0.3.0", "pinocchio-system", "pinocchio-token", "rand 0.9.0", - "shank", + "shank 0.4.2", "solana-client", "solana-clock", "solana-program", @@ -8008,7 +8070,7 @@ dependencies = [ name = "swig-assertions" version = "1.3.0" dependencies = [ - "pinocchio 0.9.0", + "pinocchio 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "pinocchio-pubkey 0.3.0", "pinocchio-system", ] @@ -8047,7 +8109,7 @@ name = "swig-compact-instructions" version = "1.3.0" dependencies = [ "bs58", - "pinocchio 0.9.0", + "pinocchio 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "pinocchio-system", "solana-program", ] @@ -8078,6 +8140,7 @@ dependencies = [ "litesvm", "litesvm-token", "openssl", + "oracle-mapping-state", "rand 0.8.5", "secp256k1", "solana-account-decoder-client-types", @@ -8103,7 +8166,7 @@ dependencies = [ "murmur3", "no-padding", "openssl", - "pinocchio 0.9.0", + "pinocchio 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "pinocchio-pubkey 0.3.0", "rand 0.9.0", "solana-secp256r1-program", diff --git a/cli/dumps/mapping.json b/cli/dumps/mapping.json new file mode 100644 index 00000000..6a52bc1a --- /dev/null +++ b/cli/dumps/mapping.json @@ -0,0 +1,14 @@ +{ + "pubkey": "FbeuRDWwLvZWEU3HNtaLoYKagw9rH1NvmjpRMpjMwhDw", + "account": { + "lamports": 1468560, + "data": [ + "ASgQWPWCd2TeA+fvXK6Z2qNd9rPpSvLyfN/8IrV2xhjoAAEACQApAP4ABpuIV/6rgYT7aH9jRhjANdrEOdwa6ztVmKDwAAAAAAEpAQkAAP////8=", + "base64" + ], + "owner": "9WM51wrB9xpRzFgYJHocYNnx4DF6G6ee2eB44ZGoZ8vg", + "executable": false, + "rentEpoch": 18446744073709551615, + "space": 83 + } +} diff --git a/cli/dumps/scope.json b/cli/dumps/scope.json new file mode 100644 index 00000000..c662b41b --- /dev/null +++ b/cli/dumps/scope.json @@ -0,0 +1,14 @@ +{ + "pubkey": "3NJYftD5sjVfxSnUdZ1wVML8f3aC6mp1CXCL6L7TnU8C", + "account": { + "lamports": 200726401, + "data": [ + "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", + "base64" + ], + "owner": "HFn8GnPADiny6XqUoWE8uRPPxb29ikn4yTuPa9MF2fWJ", + "executable": false, + "rentEpoch": 18446744073709551615, + "space": 28712 + } +} diff --git a/cli/src/commands.rs b/cli/src/commands.rs index 7b25707b..ce9e7ba8 100644 --- a/cli/src/commands.rs +++ b/cli/src/commands.rs @@ -239,6 +239,45 @@ pub fn parse_permission_from_json(permission_json: &Value) -> Result recurring, }) }, + Some("oracle") => { + // Oracle value-based limit, optional recurring + // Supported fields: + // - baseAsset: "USD" | "EUR" (preferred) OR baseAssetType: number (u8) + // - valueLimit: u64 + // - passthrough: bool (default false) + // - recurring: { window: u64 } (optional) + let base_asset_type = if let Some(base_str) = permission_json["baseAsset"].as_str() { + match base_str { + "USD" | "usd" => 0u8, + "EUR" | "eur" => 1u8, + other => return Err(anyhow!("Invalid baseAsset: {} (use USD or EUR)", other)), + } + } else { + permission_json["baseAssetType"].as_u64().unwrap_or(0) as u8 + }; + + let value_limit = permission_json["valueLimit"].as_u64().ok_or_else(|| { + anyhow!("valueLimit is required for oracle permission (u64 in base asset units)") + })?; + + let passthrough_check = permission_json["passthrough"].as_bool().unwrap_or(false); + + let recurring = if let Some(recurring) = permission_json.get("recurring") { + let window = recurring["window"].as_u64().ok_or_else(|| { + anyhow!("recurring.window is required when recurring is provided") + })?; + Some(swig_sdk::RecurringConfig::new(window)) + } else { + None + }; + + Ok(Permission::OracleLimit { + base_asset_type, + value_limit, + passthrough_check, + recurring, + }) + }, Some(unknown) => Err(anyhow!("Invalid permission type: {}", unknown)), None => Err(anyhow!("Permission type is required")), } @@ -883,6 +922,12 @@ pub fn run_command_mode(ctx: &mut SwigCliContext, cmd: Command) -> Result<()> { amount: 0, recurring: None, }), + "OracleLimit" | "Oracle" => Ok(Permission::OracleLimit { + base_asset_type: 0, + value_limit: 0, + passthrough_check: false, + recurring: None, + }), "Program" => Ok(Permission::Program { program_id: Pubkey::default(), }), diff --git a/cli/src/interactive.rs b/cli/src/interactive.rs index 4c143ff8..df7a175f 100644 --- a/cli/src/interactive.rs +++ b/cli/src/interactive.rs @@ -763,10 +763,11 @@ fn switch_authority_interactive(ctx: &mut SwigCliContext) -> Result<()> { // Store the authority keypair in the context ctx.authority = Some(authority.insecure_clone()); + let authority_static: &Keypair = Box::leak(Box::new(authority.insecure_clone())); ctx.wallet .as_mut() .unwrap() - .switch_authority(role_id, client_role, None)?; + .switch_authority(role_id, client_role, Some(authority_static))?; Ok(()) } @@ -794,7 +795,7 @@ fn transfer_interactive(ctx: &mut SwigCliContext) -> Result<()> { .interact_text()?; let transfer_instruction = transfer( - &ctx.wallet.as_ref().unwrap().get_swig_account()?, + &ctx.wallet.as_ref().unwrap().get_swig_wallet_address()?, &Pubkey::from_str(&recipient)?, amount, ); @@ -803,7 +804,7 @@ fn transfer_interactive(ctx: &mut SwigCliContext) -> Result<()> { .wallet .as_mut() .unwrap() - .sign(vec![transfer_instruction], None)?; + .sign_v2(vec![transfer_instruction], None)?; println!("Signature: {}", signature); @@ -855,6 +856,7 @@ pub fn get_permissions_interactive() -> Result> { "Sub Account (Sub-account management)", "Stake (Stake management permissions)", "Stake All (All stake management permissions)", + "Oracle (Value-based limit using price oracle)", ]; let mut permissions = Vec::new(); @@ -1069,6 +1071,53 @@ pub fn get_permissions_interactive() -> Result> { recurring: None, }, 13 => Permission::StakeAll, + 14 => { + // Oracle value-based limit + // Choose base asset + let base_assets = vec!["USD (6 decimals)", "EUR (6 decimals)"]; + let base_idx = Select::with_theme(&ColorfulTheme::default()) + .with_prompt("Choose base asset for oracle limit") + .items(&base_assets) + .default(0) + .interact()?; + let base_asset_type: u8 = match base_idx { + 0 => 0, + 1 => 1, + _ => 0, + }; + + // Value limit in base asset minor units (e.g., 6 decimals for USD) + let value_limit: u64 = Input::with_theme(&ColorfulTheme::default()) + .with_prompt("Enter value limit in base asset minor units (e.g., 100_000_000 for 100.000000)") + .interact_text()?; + + let passthrough_check = Confirm::with_theme(&ColorfulTheme::default()) + .with_prompt("Enable passthrough check (continue checking other actions)?") + .default(false) + .interact()?; + + // Recurring? + let is_recurring = Confirm::with_theme(&ColorfulTheme::default()) + .with_prompt("Make this a recurring oracle limit?") + .default(false) + .interact()?; + + let recurring = if is_recurring { + let window: u64 = Input::with_theme(&ColorfulTheme::default()) + .with_prompt("Enter time window in slots") + .interact_text()?; + Some(RecurringConfig::new(window)) + } else { + None + }; + + Permission::OracleLimit { + base_asset_type, + value_limit, + passthrough_check, + recurring, + } + }, _ => unreachable!(), }; diff --git a/cli/src/main.rs b/cli/src/main.rs index d8d388c3..edaa4fd7 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -430,6 +430,7 @@ fn get_permissions_interactive() -> Result> { "Sub Account (Sub-account management)", "Stake (Stake management permissions)", "Stake All (All stake management permissions)", + "Oracle (Value-based limit using price oracle)", ]; let mut permissions = Vec::new(); @@ -652,6 +653,53 @@ fn get_permissions_interactive() -> Result> { recurring: None, }, 13 => Permission::StakeAll, + 14 => { + // Oracle value-based limit + // Choose base asset + let base_assets = vec!["USD (6 decimals)", "EUR (6 decimals)"]; + let base_idx = Select::with_theme(&ColorfulTheme::default()) + .with_prompt("Choose base asset for oracle limit") + .items(&base_assets) + .default(0) + .interact()?; + let base_asset_type: u8 = match base_idx { + 0 => 0, + 1 => 1, + _ => 0, + }; + + // Value limit in base asset minor units (e.g., 6 decimals for USD) + let value_limit: u64 = Input::with_theme(&ColorfulTheme::default()) + .with_prompt("Enter value limit in base asset minor units (e.g., 100_000_000 for 100.000000)") + .interact_text()?; + + let passthrough_check = Confirm::with_theme(&ColorfulTheme::default()) + .with_prompt("Enable passthrough check (continue checking other actions)?") + .default(false) + .interact()?; + + // Recurring? + let is_recurring = Confirm::with_theme(&ColorfulTheme::default()) + .with_prompt("Make this a recurring oracle limit?") + .default(false) + .interact()?; + + let recurring = if is_recurring { + let window: u64 = Input::with_theme(&ColorfulTheme::default()) + .with_prompt("Enter time window in slots") + .interact_text()?; + Some(RecurringConfig::new(window)) + } else { + None + }; + + Permission::OracleLimit { + base_asset_type, + value_limit, + passthrough_check, + recurring, + } + }, _ => unreachable!(), }; diff --git a/interface/src/lib.rs b/interface/src/lib.rs index 91416c59..a080e442 100644 --- a/interface/src/lib.rs +++ b/interface/src/lib.rs @@ -23,7 +23,8 @@ pub use swig_compact_instructions::*; use swig_state::{ action::{ all::All, all_but_manage_authority::AllButManageAuthority, - manage_authority::ManageAuthority, program::Program, program_all::ProgramAll, + manage_authority::ManageAuthority, oracle_limits::OracleTokenLimit, + oracle_recurring_limit::OracleRecurringLimit, program::Program, program_all::ProgramAll, program_curated::ProgramCurated, program_scope::ProgramScope, sol_destination_limit::SolDestinationLimit, sol_limit::SolLimit, sol_recurring_destination_limit::SolRecurringDestinationLimit, @@ -61,6 +62,8 @@ pub enum ClientAction { StakeLimit(StakeLimit), StakeRecurringLimit(StakeRecurringLimit), StakeAll(StakeAll), + OracleTokenLimit(OracleTokenLimit), + OracleRecurringLimit(OracleRecurringLimit), } impl ClientAction { @@ -105,6 +108,12 @@ impl ClientAction { (Permission::StakeRecurringLimit, StakeRecurringLimit::LEN) }, ClientAction::StakeAll(_) => (Permission::StakeAll, StakeAll::LEN), + ClientAction::OracleTokenLimit(_) => { + (Permission::OracleTokenLimit, OracleTokenLimit::LEN) + }, + ClientAction::OracleRecurringLimit(_) => { + (Permission::OracleRecurringLimit, OracleRecurringLimit::LEN) + }, }; let offset = data.len() as u32; let header = Action::new( @@ -136,6 +145,8 @@ impl ClientAction { ClientAction::StakeLimit(action) => action.into_bytes(), ClientAction::StakeRecurringLimit(action) => action.into_bytes(), ClientAction::StakeAll(action) => action.into_bytes(), + ClientAction::OracleTokenLimit(action) => action.into_bytes(), + ClientAction::OracleRecurringLimit(action) => action.into_bytes(), }; data.extend_from_slice( bytes_res.map_err(|e| anyhow::anyhow!("Failed to serialize action {:?}", e))?, @@ -654,6 +665,19 @@ impl SignV2Instruction { let arg_bytes = args .into_bytes() .map_err(|e| anyhow::anyhow!("Failed to serialize args {:?}", e))?; + + // Appends the oracle accounts to the instruction + let oracle_accounts = vec![ + AccountMeta::new_readonly( + Pubkey::from_str_const("FbeuRDWwLvZWEU3HNtaLoYKagw9rH1NvmjpRMpjMwhDw"), + false, + ), + AccountMeta::new_readonly( + Pubkey::from_str_const("3NJYftD5sjVfxSnUdZ1wVML8f3aC6mp1CXCL6L7TnU8C"), + false, + ), + ]; + accounts.extend(oracle_accounts); Ok(Instruction { program_id: Pubkey::from(swig::ID), accounts, @@ -720,6 +744,19 @@ impl SignV2Instruction { .into_bytes() .map_err(|e| anyhow::anyhow!("Failed to serialize args {:?}", e))?; + // Appends the oracle accounts to the instruction + let oracle_accounts = vec![ + AccountMeta::new_readonly( + Pubkey::from_str_const("FbeuRDWwLvZWEU3HNtaLoYKagw9rH1NvmjpRMpjMwhDw"), + false, + ), + AccountMeta::new_readonly( + Pubkey::from_str_const("3NJYftD5sjVfxSnUdZ1wVML8f3aC6mp1CXCL6L7TnU8C"), + false, + ), + ]; + accounts.extend(oracle_accounts); + let mut account_payload_bytes = Vec::new(); for account in &accounts { account_payload_bytes.extend_from_slice( @@ -815,6 +852,19 @@ impl SignV2Instruction { .into_bytes() .map_err(|e| anyhow::anyhow!("Failed to serialize args {:?}", e))?; + // Appends the oracle accounts to the instruction + let oracle_accounts = vec![ + AccountMeta::new_readonly( + Pubkey::from_str_const("FbeuRDWwLvZWEU3HNtaLoYKagw9rH1NvmjpRMpjMwhDw"), + false, + ), + AccountMeta::new_readonly( + Pubkey::from_str_const("3NJYftD5sjVfxSnUdZ1wVML8f3aC6mp1CXCL6L7TnU8C"), + false, + ), + ]; + accounts.extend(oracle_accounts); + // Create the message hash for secp256r1 authentication let mut account_payload_bytes = Vec::new(); for account in &accounts { diff --git a/program/Cargo.toml b/program/Cargo.toml index 8d4f9b23..ba2c5ea7 100644 --- a/program/Cargo.toml +++ b/program/Cargo.toml @@ -27,6 +27,8 @@ bytemuck = { version = "1.13.1", features = ["derive"] } no-padding = { path = "../no-padding" } solana-security-txt = "=1.1.1" default-env = "=0.1.1" +oracle-mapping-state = { git = "https://github.com/anagrambuild/scope-oracle-mapper" } + [dev-dependencies] solana-sdk = "2" @@ -47,6 +49,7 @@ solana-client = "=2.2.4" solana-program = "=2.2.1" once_cell = "1.21.3" spl-memo = "=6.0.0" +base64 = "0.22.1" solana-secp256r1-program = "2.2.1" openssl = { version = "0.10.72", features = ["vendored"] } hex = "0.4.3" diff --git a/program/src/actions/sign_v1.rs b/program/src/actions/sign_v1.rs index 63b9ac28..1fbf2625 100644 --- a/program/src/actions/sign_v1.rs +++ b/program/src/actions/sign_v1.rs @@ -13,13 +13,17 @@ use pinocchio::{ sysvars::{clock::Clock, Sysvar}, ProgramResult, }; -use pinocchio_pubkey::from_str; +use pinocchio_pubkey::{from_str, pubkey}; use swig_assertions::*; use swig_compact_instructions::InstructionIterator; use swig_state::{ action::{ all::All, all_but_manage_authority::AllButManageAuthority, + oracle_limits::{ + BaseAsset, OracleTokenLimit, ORACLE_MAPPING_ACCOUNT, SCOPE_ACCOUNT, SOL_MINT, + }, + oracle_recurring_limit::OracleRecurringLimit, program::Program, program_all::ProgramAll, program_curated::ProgramCurated, @@ -49,7 +53,7 @@ use crate::{ accounts::{Context, SignV1Accounts}, SwigInstruction, }, - util::{build_restricted_keys, hash_except}, + util::{build_restricted_keys, calculate_token_value, get_price_data, hash_except}, AccountClassification, SPL_TOKEN_2022_ID, SPL_TOKEN_ID, SYSTEM_PROGRAM_ID, }; // use swig_instructions::InstructionIterator; @@ -467,6 +471,100 @@ pub fn sign_v1( // First check general SOL limits let mut general_limit_applied = false; + // Check oracle recurring limit + { + if let Some(action) = + RoleMut::get_action_mut::(actions, &[])? + { + let scope_data = unsafe { + let scope_account = + all_accounts.get_unchecked(all_accounts.len() - 1); + // also check if owner matches + if scope_account.key().as_ref() != &SCOPE_ACCOUNT { + return Err(SwigError::WrongScopeOracleAccount.into()); + } + scope_account.borrow_data_unchecked() + }; + + let mapping_registry = unsafe { + let mapping_account = + all_accounts.get_unchecked(all_accounts.len() - 2); + if mapping_account.key().as_ref() != &ORACLE_MAPPING_ACCOUNT { + return Err(SwigError::WrongOracleMappingAccount.into()); + } + mapping_account.borrow_data_unchecked() + }; + + let (price, exp, mint_decimal) = get_price_data( + mapping_registry, + scope_data, + &SOL_MINT, + &clock, + BaseAsset::try_from(action.base_asset_type)?, + )?; + + let token_value_in_base = calculate_token_value( + price, + exp, + action.get_base_asset_decimals(), + total_sol_spent, + mint_decimal, + action.get_base_asset_decimals(), + )?; + + action.run_for_sol(token_value_in_base, slot)?; + + if action.passthrough_check == 0 { + continue; + } + } else if let Some(action) = + RoleMut::get_action_mut::(actions, &[])? + { + let scope_data = unsafe { + let scope_account = + all_accounts.get_unchecked(all_accounts.len() - 1); + // also check if owner matches + if scope_account.key().as_ref() != &SCOPE_ACCOUNT { + return Err(SwigError::WrongScopeOracleAccount.into()); + } + scope_account.borrow_data_unchecked() + }; + + let mapping_registry = unsafe { + let mapping_account = + all_accounts.get_unchecked(all_accounts.len() - 2); + let owner = ORACLE_MAPPING_ACCOUNT; + if mapping_account.key().as_ref() != &owner { + return Err(SwigError::WrongOracleMappingAccount.into()); + } + mapping_account.borrow_data_unchecked() + }; + + let (price, exp, mint_decimal) = get_price_data( + mapping_registry, + scope_data, + &SOL_MINT, + &clock, + BaseAsset::try_from(action.base_asset_type)?, + )?; + + let token_value_in_base = calculate_token_value( + price, + exp, + action.get_base_asset_decimals(), + total_sol_spent, + mint_decimal, + action.get_base_asset_decimals(), + )?; + + action.run_for_sol(token_value_in_base)?; + + if !action.passthrough_check { + continue; + } + }; + } + if let Some(action) = RoleMut::get_action_mut::(actions, &[])? { action.run(total_sol_spent)?; general_limit_applied = true; @@ -629,6 +727,103 @@ pub fn sign_v1( continue 'account_loop; } + // Check oracle token limit + { + if let Some(action) = + RoleMut::get_action_mut::(actions, &[])? + { + let scope_data = unsafe { + let scope_account = + all_accounts.get_unchecked(all_accounts.len() - 1); + if scope_account.key().as_ref() != &SCOPE_ACCOUNT { + return Err(SwigError::WrongScopeOracleAccount.into()); + } + scope_account.borrow_data_unchecked() + }; + + let mapping_data = unsafe { + let mapping_account = + all_accounts.get_unchecked(all_accounts.len() - 2); + if mapping_account.key().as_ref() != &ORACLE_MAPPING_ACCOUNT { + return Err(SwigError::WrongOracleMappingAccount.into()); + } + mapping_account.borrow_data_unchecked() + }; + + let mint_bytes = + mint.try_into().map_err(|_| SwigError::OracleMintNotFound)?; + + let (price, exp, mint_decimal) = get_price_data( + mapping_data, + scope_data, + &mint_bytes, + &clock, + BaseAsset::try_from(action.base_asset_type)?, + )?; + + let token_value_in_base = calculate_token_value( + price, + exp, + action.get_base_asset_decimals(), + total_token_spent, + mint_decimal, + action.get_base_asset_decimals(), + )?; + + action.run_for_token(token_value_in_base, slot)?; + + if action.passthrough_check == 0 { + continue; + } + } else if let Some(action) = + RoleMut::get_action_mut::(actions, &[])? + { + let scope_data = unsafe { + let scope_account = + all_accounts.get_unchecked(all_accounts.len() - 1); + if scope_account.key().as_ref() != &SCOPE_ACCOUNT { + return Err(SwigError::WrongScopeOracleAccount.into()); + } + scope_account.borrow_data_unchecked() + }; + + let mapping_data = unsafe { + let mapping_account = + all_accounts.get_unchecked(all_accounts.len() - 2); + if mapping_account.key().as_ref() != &ORACLE_MAPPING_ACCOUNT { + return Err(SwigError::WrongOracleMappingAccount.into()); + } + mapping_account.borrow_data_unchecked() + }; + + let mint_bytes = + mint.try_into().map_err(|_| SwigError::OracleMintNotFound)?; + + let (price, exp, mint_decimal) = get_price_data( + mapping_data, + scope_data, + &mint_bytes, + &clock, + BaseAsset::try_from(action.base_asset_type)?, + )?; + + let token_value_in_base = calculate_token_value( + price, + exp, + action.get_base_asset_decimals(), + total_token_spent, + mint_decimal, + action.get_base_asset_decimals(), + )?; + + action.run_for_token(token_value_in_base)?; + + if !action.passthrough_check { + continue; + } + }; + } + // Check regular token limits for outgoing transfers if let Some(action) = RoleMut::get_action_mut::(actions, mint)? { diff --git a/program/src/actions/sign_v2.rs b/program/src/actions/sign_v2.rs index b684432d..2d8a59fc 100644 --- a/program/src/actions/sign_v2.rs +++ b/program/src/actions/sign_v2.rs @@ -15,10 +15,14 @@ use pinocchio::{ use pinocchio_pubkey::from_str; use swig_assertions::*; use swig_compact_instructions::InstructionIterator; +use swig_state::action::oracle_limits::{ + BaseAsset, OracleTokenLimit, ORACLE_MAPPING_ACCOUNT, SCOPE_ACCOUNT, SOL_MINT, +}; use swig_state::{ action::{ all::All, all_but_manage_authority::AllButManageAuthority, + oracle_recurring_limit::OracleRecurringLimit, program::Program, program_all::ProgramAll, program_curated::ProgramCurated, @@ -47,7 +51,7 @@ use crate::{ accounts::{Context, SignV2Accounts}, SwigInstruction, }, - util::hash_except, + util::{calculate_token_value, get_price_data, hash_except}, AccountClassification, SPL_TOKEN_2022_ID, SPL_TOKEN_ID, SYSTEM_PROGRAM_ID, }; // use swig_instructions::InstructionIterator; @@ -467,6 +471,99 @@ pub fn sign_v2( // First check general SOL limits let mut general_limit_applied = false; + // Check oracle recurring limit + { + if let Some(action) = + RoleMut::get_action_mut::(actions, &[])? + { + let scope_data = unsafe { + let scope_account = + all_accounts.get_unchecked(all_accounts.len() - 1); + // also check if owner matches + if scope_account.key().as_ref() != &SCOPE_ACCOUNT { + return Err(SwigError::WrongScopeOracleAccount.into()); + } + scope_account.borrow_data_unchecked() + }; + + let mapping_registry = unsafe { + let mapping_account = + all_accounts.get_unchecked(all_accounts.len() - 2); + if mapping_account.key().as_ref() != &ORACLE_MAPPING_ACCOUNT { + return Err(SwigError::WrongOracleMappingAccount.into()); + } + mapping_account.borrow_data_unchecked() + }; + + let (price, exp, mint_decimal) = get_price_data( + mapping_registry, + scope_data, + &SOL_MINT, + &clock, + BaseAsset::try_from(action.base_asset_type)?, + )?; + + let token_value_in_base = calculate_token_value( + price, + exp, + action.get_base_asset_decimals(), + total_sol_spent, + mint_decimal, + action.get_base_asset_decimals(), + )?; + + action.run_for_sol(token_value_in_base, slot)?; + + if action.passthrough_check == 0 { + continue; + } + } else if let Some(action) = + RoleMut::get_action_mut::(actions, &[])? + { + let scope_data = unsafe { + let scope_account = + all_accounts.get_unchecked(all_accounts.len() - 1); + // also check if owner matches + if scope_account.key().as_ref() != &SCOPE_ACCOUNT { + return Err(SwigError::WrongScopeOracleAccount.into()); + } + scope_account.borrow_data_unchecked() + }; + + let mapping_registry = unsafe { + let mapping_account = + all_accounts.get_unchecked(all_accounts.len() - 2); + if mapping_account.key().as_ref() != &ORACLE_MAPPING_ACCOUNT { + return Err(SwigError::WrongOracleMappingAccount.into()); + } + mapping_account.borrow_data_unchecked() + }; + + let (price, exp, mint_decimal) = get_price_data( + mapping_registry, + scope_data, + &SOL_MINT, + &clock, + BaseAsset::try_from(action.base_asset_type)?, + )?; + + let token_value_in_base = calculate_token_value( + price, + exp, + action.get_base_asset_decimals(), + total_sol_spent, + mint_decimal, + action.get_base_asset_decimals(), + )?; + + action.run_for_sol(token_value_in_base)?; + + if !action.passthrough_check { + continue; + } + }; + } + if let Some(action) = RoleMut::get_action_mut::(actions, &[])? { action.run(total_sol_spent)?; general_limit_applied = true; @@ -628,6 +725,103 @@ pub fn sign_v2( continue 'account_loop; } + // Check oracle token limit + { + if let Some(action) = + RoleMut::get_action_mut::(actions, &[])? + { + let scope_data = unsafe { + let scope_account = + all_accounts.get_unchecked(all_accounts.len() - 1); + if scope_account.key().as_ref() != &SCOPE_ACCOUNT { + return Err(SwigError::WrongScopeOracleAccount.into()); + } + scope_account.borrow_data_unchecked() + }; + + let mapping_data = unsafe { + let mapping_account = + all_accounts.get_unchecked(all_accounts.len() - 2); + if mapping_account.key().as_ref() != &ORACLE_MAPPING_ACCOUNT { + return Err(SwigError::WrongOracleMappingAccount.into()); + } + mapping_account.borrow_data_unchecked() + }; + + let mint_bytes = + mint.try_into().map_err(|_| SwigError::OracleMintNotFound)?; + + let (price, exp, mint_decimal) = get_price_data( + mapping_data, + scope_data, + &mint_bytes, + &clock, + BaseAsset::try_from(action.base_asset_type)?, + )?; + + let token_value_in_base = calculate_token_value( + price, + exp, + action.get_base_asset_decimals(), + total_token_spent, + mint_decimal, + action.get_base_asset_decimals(), + )?; + + action.run_for_token(token_value_in_base, slot)?; + + if action.passthrough_check == 0 { + continue; + } + } else if let Some(action) = + RoleMut::get_action_mut::(actions, &[])? + { + let scope_data = unsafe { + let scope_account = + all_accounts.get_unchecked(all_accounts.len() - 1); + if scope_account.key().as_ref() != &SCOPE_ACCOUNT { + return Err(SwigError::WrongScopeOracleAccount.into()); + } + scope_account.borrow_data_unchecked() + }; + + let mapping_data = unsafe { + let mapping_account = + all_accounts.get_unchecked(all_accounts.len() - 2); + if mapping_account.key().as_ref() != &ORACLE_MAPPING_ACCOUNT { + return Err(SwigError::WrongOracleMappingAccount.into()); + } + mapping_account.borrow_data_unchecked() + }; + + let mint_bytes = + mint.try_into().map_err(|_| SwigError::OracleMintNotFound)?; + + let (price, exp, mint_decimal) = get_price_data( + mapping_data, + scope_data, + &mint_bytes, + &clock, + BaseAsset::try_from(action.base_asset_type)?, + )?; + + let token_value_in_base = calculate_token_value( + price, + exp, + action.get_base_asset_decimals(), + total_token_spent, + mint_decimal, + action.get_base_asset_decimals(), + )?; + + action.run_for_token(token_value_in_base)?; + + if !action.passthrough_check { + continue; + } + }; + } + // Check regular token limits for outgoing transfers if let Some(action) = RoleMut::get_action_mut::(actions, mint)? { diff --git a/program/src/error.rs b/program/src/error.rs index 7fbba81e..be75bc99 100644 --- a/program/src/error.rs +++ b/program/src/error.rs @@ -116,6 +116,18 @@ pub enum SwigError { SignV1CannotBeUsedWithSwigV2, /// SignV2 instruction cannot be used with Swig v1 accounts SignV2CannotBeUsedWithSwigV1, + /// Wrong oracle program account + WrongScopeOracleAccount, + /// Wrong oracle mapping account + WrongOracleMappingAccount, + /// Oracle mint not found + OracleMintNotFound, + /// Oracle value overflow during calculation + OracleValueOverflow, + /// Oracle price chain empty + OraclePriceChainEmpty, + /// Oracle price is stale + OraclePriceStale, } /// Implements conversion from SwigError to ProgramError. diff --git a/program/src/util/mod.rs b/program/src/util/mod.rs index 25e5f2a3..e3427b3e 100644 --- a/program/src/util/mod.rs +++ b/program/src/util/mod.rs @@ -4,6 +4,7 @@ //! - Program scope caching and lookup //! - Account balance reading //! - Token transfer operations +//! - Oracle Program for token price fetch //! The utilities are optimized for performance and safety. use std::mem::MaybeUninit; @@ -14,12 +15,15 @@ use pinocchio::{ instruction::{AccountMeta, Instruction, Signer}, msg, program_error::ProgramError, - pubkey::Pubkey, + pubkey::{self, Pubkey}, syscalls::sol_sha256, + sysvars::{clock::Clock, Sysvar}, ProgramResult, }; +use pinocchio_pubkey::pubkey; use swig_state::{ action::{ + oracle_limits::BaseAsset, program_scope::{NumericType, ProgramScope}, Action, Permission, }, @@ -415,3 +419,244 @@ pub fn hash_except( data_payload_hash } + +use oracle_mapping_state::{DataLen, MintMapping, ScopeMappingRegistry}; + +/// Calculate token value with configurable precision +/// +/// # Arguments +/// * `base_price` - Oracle price value +/// * `base_exponent` - Oracle price exponent +/// * `oracle_base_decimal` - Oracle decimal places +/// * `mint_amount` - Token amount in mint decimals +/// * `mint_decimal` - Token decimal places +/// * `target_precision` - Target precision for result +/// +/// # Returns +/// Token value in target precision +pub fn calculate_token_value( + base_price: u64, + base_exponent: u8, + oracle_base_decimal: u8, + mint_amount: u64, + mint_decimal: u8, + target_precision: u8, +) -> Result { + let price = base_price as u128; + let amount = mint_amount as u128; + let base_exp = base_exponent as u32; + let mint_dec = mint_decimal as u32; + let target_prec = target_precision as u32; + + // value = (amount * price * 10^target_precision) / (10^mint_decimal * 10^base_exponent) + let numerator = amount + .saturating_mul(price) + .saturating_mul(10u128.pow(target_prec)); + let denominator = 10u128.pow(mint_dec).saturating_mul(10u128.pow(base_exp)); + + if denominator == 0 { + return Ok(0); + } + let value = numerator / denominator; + if value > u64::MAX as u128 { + return Err(SwigError::OracleValueOverflow); + } + Ok(value as u64) +} + +pub const NULL_PUBKEY: [u8; 32] = [ + 11, 193, 238, 216, 208, 116, 241, 195, 55, 212, 76, 22, 75, 202, 40, 216, 76, 206, 27, 169, + 138, 64, 177, 28, 19, 90, 156, 0, 0, 0, 0, 0, +]; + +pub fn get_price_data( + mapping_registry: &[u8], + scope_data: &[u8], + mint: &[u8; 32], + clock: &Clock, + base_asset: BaseAsset, +) -> Result<(u64, u8, u8), SwigError> { + let mut mapping = MintMapping::get_mapping_details(&mapping_registry, mint) + .map_err(|_| SwigError::OracleMintNotFound)?; + + // Inject the base asset scope index if it exists + if let Some(base_asset_scope_index) = base_asset.get_scope_index() { + // Replace the first u16::MAX with the base asset scope index + mapping.scope_details = mapping.scope_details.map(|mut scope_details| { + if let Some(pos) = scope_details.iter().position(|&x| x == u16::MAX) { + scope_details[pos] = base_asset_scope_index; + } + scope_details + }); + } + + let (mut scope_price, mut scope_exp) = get_scope_price_data( + scope_data, + mapping.scope_details.ok_or(SwigError::OracleMintNotFound)?, + clock.slot, + )?; + + Ok((scope_price, scope_exp, mapping.decimals)) +} + +fn get_scope_price_data( + data: &[u8], + price_chain: [u16; 3], + current_slot: u64, +) -> Result<(u64, u8), SwigError> { + let prices_start = 8 + 32; + + const SCOPE_PRICE_FEED_LEN: usize = 56; + + // Check if price_chain is valid + if price_chain == [u16::MAX, u16::MAX, u16::MAX] { + return Err(SwigError::OraclePriceChainEmpty); + } + let mut price_chain_raw = Vec::new(); + + for &token_id in &price_chain { + if token_id == u16::MAX { + break; + } + + let start_offset = prices_start + (token_id as usize * SCOPE_PRICE_FEED_LEN); + let end_offset = start_offset + SCOPE_PRICE_FEED_LEN; + + if end_offset > data.len() { + return Err(SwigError::OraclePriceChainEmpty); + } + + let price_data = unsafe { data.get_unchecked(start_offset..end_offset) }; + let value = + u64::from_le_bytes(unsafe { price_data.get_unchecked(0..8).try_into().unwrap() }); + let exp = + u64::from_le_bytes(unsafe { price_data.get_unchecked(8..16).try_into().unwrap() }); + let last_updated_slot = + u64::from_le_bytes(unsafe { price_data.get_unchecked(16..24).try_into().unwrap() }); + let unix_timestamp = + u64::from_le_bytes(unsafe { price_data.get_unchecked(24..32).try_into().unwrap() }); + + // time to allow (for Test): 120 seconds = 120 seconds / 0.4ms per slot = 300 slots + #[cfg(test)] + if last_updated_slot < current_slot - 300 { + return Err(SwigError::OraclePriceStale); + } + // time to allow: 60 seconds = 60 seconds / 0.4ms per slot = 150 slots + #[cfg(not(test))] + if last_updated_slot < current_slot - 150 { + return Err(SwigError::OraclePriceStale); + } + + price_chain_raw.push((value, exp, unix_timestamp)); + } + + if price_chain_raw.is_empty() { + return Err(SwigError::OraclePriceChainEmpty); + } + + let last_updated_slot: u64 = u64::from_le_bytes(unsafe { + data.get_unchecked( + prices_start + (price_chain[0] as usize * SCOPE_PRICE_FEED_LEN) + 16 + ..prices_start + (price_chain[0] as usize * SCOPE_PRICE_FEED_LEN) + 24, + ) + .try_into() + .unwrap() + }); + + // If only one price in chain, return it directly + if price_chain_raw.len() == 1 { + let (value, exp, unix_timestamp) = price_chain_raw[0]; + return Ok((value, exp as u8)); + } + + // Chain multiple prices together by multiplying them + let mut chained_value: u128 = 1; + let mut chained_exp: u64 = 0; + + for (value, exp, _) in price_chain_raw { + let value_u128 = value as u128; + + // Pre-scale values if they're too large to prevent overflow + let mut scaled_value = value_u128; + let mut scaled_exp = exp; + + // Scale down the input value if it's too large + while scaled_value > u64::MAX as u128 && scaled_exp > 0 { + scaled_value /= 10; + scaled_exp = scaled_exp + .checked_sub(1) + .ok_or(SwigError::OraclePriceChainEmpty)?; + } + + // Also scale down the current chained value if it's too large + while chained_value > u64::MAX as u128 && chained_exp > 0 { + chained_value /= 10; + chained_exp = chained_exp + .checked_sub(1) + .ok_or(SwigError::OraclePriceChainEmpty)?; + } + + // For the first value (SOL/USD), multiply + // For subsequent values (exchange rates), divide to get the final currency + if chained_value == 1 && chained_exp == 0 { + // First value: multiply directly + chained_value = chained_value + .checked_mul(scaled_value) + .ok_or(SwigError::OraclePriceChainEmpty)?; + chained_exp = chained_exp + .checked_add(scaled_exp) + .ok_or(SwigError::OraclePriceChainEmpty)?; + } else { + // Subsequent values: divide to convert to target currency + // We need to handle division with proper scaling + let target_exp = chained_exp + .checked_sub(scaled_exp) + .ok_or(SwigError::OraclePriceChainEmpty)?; + + // Scale up the numerator if needed for precision + let mut scaled_chained = chained_value; + let mut working_exp = chained_exp; + while scaled_chained < scaled_value && working_exp < u16::MAX as u64 { + scaled_chained = scaled_chained + .checked_mul(10) + .ok_or(SwigError::OraclePriceChainEmpty)?; + working_exp = working_exp + .checked_add(1) + .ok_or(SwigError::OraclePriceChainEmpty)?; + } + + chained_value = scaled_chained + .checked_div(scaled_value) + .ok_or(SwigError::OraclePriceChainEmpty)?; + chained_exp = working_exp + .checked_sub(scaled_exp) + .ok_or(SwigError::OraclePriceChainEmpty)?; + } + + // Scale down if the value is too large to fit in u64 + while chained_value > u64::MAX as u128 && chained_exp > 0 { + chained_value /= 10; + chained_exp = chained_exp + .checked_sub(1) + .ok_or(SwigError::OraclePriceChainEmpty)?; + } + } + + let final_value = if chained_value <= u64::MAX as u128 { + chained_value as u64 + } else { + return Err(SwigError::OracleValueOverflow); + }; + + // Ensure the exponent is within reasonable bounds to prevent overflow in pow operations + let (final_value, final_exp) = if chained_exp > 18 { + // If exponent is too large, scale down the value and reduce exponent + let scale_factor = chained_exp - 18; + let scaled_value = final_value / 10_u64.pow(scale_factor as u32); + (scaled_value, (chained_exp - scale_factor) as u8) + } else { + (final_value, chained_exp as u8) + }; + + Ok((final_value, final_exp)) +} diff --git a/program/tests/common/mod.rs b/program/tests/common/mod.rs index f24f7042..98dd3f01 100644 --- a/program/tests/common/mod.rs +++ b/program/tests/common/mod.rs @@ -1,11 +1,15 @@ use alloy_signer_local::{LocalSigner, PrivateKeySigner}; -use anyhow::Result; +use anyhow::{Ok, Result}; use litesvm::{types::TransactionMetadata, LiteSVM}; use litesvm_token::{spl_token, CreateAssociatedTokenAccount, CreateMint, MintTo}; +use oracle_mapping_state::{ + error::MappingProgramError, scope_mapping_registry, DataLen, MintMapping, ScopeMappingRegistry, +}; use solana_sdk::{ compute_budget::ComputeBudgetInstruction, instruction::Instruction, message::{v0, VersionedMessage}, + program_pack::Pack, pubkey::Pubkey, signature::Keypair, signer::Signer, @@ -16,12 +20,24 @@ use swig_interface::{ CreateSubAccountInstruction, SubAccountSignInstruction, ToggleSubAccountInstruction, WithdrawFromSubAccountInstruction, }; + use swig_state::{ - action::{all::All, manage_authority::ManageAuthority, sub_account::SubAccount}, + action::{ + all::All, manage_authority::ManageAuthority, oracle_limits::OracleTokenLimit, + oracle_recurring_limit::OracleRecurringLimit, program_scope::ProgramScope, + sol_limit::SolLimit, sol_recurring_limit::SolRecurringLimit, sub_account::SubAccount, + }, authority::{ - ed25519::CreateEd25519SessionAuthority, secp256k1::CreateSecp256k1SessionAuthority, - secp256r1::CreateSecp256r1SessionAuthority, AuthorityType, + ed25519::{CreateEd25519SessionAuthority, ED25519Authority, Ed25519SessionAuthority}, + secp256k1::{ + CreateSecp256k1SessionAuthority, Secp256k1Authority, Secp256k1SessionAuthority, + }, + secp256r1::{ + CreateSecp256r1SessionAuthority, Secp256r1Authority, Secp256r1SessionAuthority, + }, + AuthorityType, }, + role::Role, swig::{sub_account_seeds, swig_account_seeds, swig_wallet_address_seeds, SwigWithRoles}, IntoBytes, Transmutable, }; @@ -474,6 +490,120 @@ pub fn load_program(svm: &mut LiteSVM) -> anyhow::Result<()> { .map_err(|_| anyhow::anyhow!("Failed to load program")) } +pub fn load_sample_pyth_accounts(svm: &mut LiteSVM) -> anyhow::Result<()> { + use base64; + use solana_program::pubkey::Pubkey; + use solana_sdk::account::Account; + use std::str::FromStr; + + let pubkey = Pubkey::from_str("7UVimffxr9ow1uXYxsr4LHAcV58mLzhmwaeKvJ1pjLiE").unwrap(); + let owner = Pubkey::from_str("rec5EKMGg6MxZYaMdyBfgwp4d5rB9T1VQH5pJv5LtFJ").unwrap(); + + let mut data = Account { + lamports: 1825020, + data: base64::decode("IvEjY51+9M1gMUcENA3t3zcf1CRyFI8kjp0abRpesqw6zYt/1dayQwHvDYtv2izrpB2hXUCV0do5Kg0vjtDGx7wPTPrIwoC1bbLod+YDAAAA6QJ4AAAAAAD4////lZpJaAAAAACVmkloAAAAAMC2EeIDAAAALL2AAAAAAADcSaEUAAAAAAA=").unwrap(), + owner, + executable: false, + rent_epoch: 18446744073709551615, + }; + + svm.set_account(pubkey, data); + + Ok(()) +} + +pub fn load_sample_scope_data(svm: &mut LiteSVM, payer: &Keypair) -> anyhow::Result<(Pubkey)> { + use base64; + use solana_program::pubkey::Pubkey; + use solana_sdk::account::Account; + use std::str::FromStr; + + let pubkey = Pubkey::from_str("3NJYftD5sjVfxSnUdZ1wVML8f3aC6mp1CXCL6L7TnU8C").unwrap(); + let owner = Pubkey::from_str("HFn8GnPADiny6XqUoWE8uRPPxb29ikn4yTuPa9MF2fWJ").unwrap(); + + use solana_client::rpc_client::RpcClient; + + let client = RpcClient::new("https://api.mainnet-beta.solana.com".to_string()); + let mut scope_account = client.get_account(&pubkey).unwrap(); + + let mut data = Account { + lamports: 200_700_000, + data: scope_account.data, + owner, + executable: false, + rent_epoch: 18446744073709551615, + }; + + svm.set_account(pubkey, data).unwrap(); + + let mapping_pubkey = Pubkey::from_str("FbeuRDWwLvZWEU3HNtaLoYKagw9rH1NvmjpRMpjMwhDw").unwrap(); + let owner_pubkey = Pubkey::from_str("9WM51wrB9xpRzFgYJHocYNnx4DF6G6ee2eB44ZGoZ8vg").unwrap(); + + let mint = setup_mint(svm, &payer).unwrap(); + + let devnet_client = RpcClient::new("https://api.devnet.solana.com".to_string()); + let scope_mapping_registry_acc = devnet_client.get_account(&mapping_pubkey).unwrap(); + + let mut scope_mapping_data = scope_mapping_registry_acc.data.clone(); + let mut scope_mapping_registry = ScopeMappingRegistry::from_bytes( + scope_mapping_data[..ScopeMappingRegistry::LEN] + .try_into() + .unwrap(), + ) + .unwrap(); + + // Create new mint mapping + let new_mint_mapping = MintMapping::new( + mint.to_bytes(), + Some([0, u16::MAX, u16::MAX]), + None, + None, + 9, + ); + + let mapping_mint_data = new_mint_mapping.to_bytes(); + let mapping = &mapping_mint_data[..new_mint_mapping.serialized_size() as usize]; + + let insertion_offset = + ScopeMappingRegistry::LEN + scope_mapping_registry.last_mapping_offset as usize; + + scope_mapping_data.resize(insertion_offset + mapping.len(), 0); + + scope_mapping_data[insertion_offset..insertion_offset + mapping.len()].copy_from_slice(mapping); + + scope_mapping_registry.total_mappings += 1; + scope_mapping_registry.last_mapping_offset += mapping.len() as u16; + + scope_mapping_data[..ScopeMappingRegistry::LEN] + .copy_from_slice(&scope_mapping_registry.to_bytes()); + + let data = Account { + lamports: scope_mapping_registry_acc.lamports + 10000000, + data: scope_mapping_data, + owner: owner_pubkey, + executable: false, + rent_epoch: 18446744073709551615, + }; + + svm.set_account(mapping_pubkey, data).unwrap(); + + // sync litesvm slot to mainnet slot + let slot = client.get_slot().unwrap(); + svm.warp_to_slot(slot); + + Ok(mint) +} + +pub fn advance_slot(context: &mut SwigTestContext, slots: u64) -> u64 { + use solana_client::rpc_client::RpcClient; + + let client = RpcClient::new("https://api.mainnet-beta.solana.com".to_string()); + let slot = client.get_slot().unwrap(); + let new_slot = slot + slots; + context.svm.warp_to_slot(new_slot); + new_slot +} + pub fn setup_mint(svm: &mut LiteSVM, payer: &Keypair) -> anyhow::Result { let mint = CreateMint::new(svm, payer) .decimals(9) @@ -483,6 +613,51 @@ pub fn setup_mint(svm: &mut LiteSVM, payer: &Keypair) -> anyhow::Result Ok(mint) } +pub fn setup_oracle_mint(context: &mut SwigTestContext) -> anyhow::Result { + load_sample_pyth_accounts(&mut context.svm).unwrap(); + + // Setup token accounts + let mint_key_bytes = [ + 193, 17, 76, 51, 120, 6, 8, 131, 149, 6, 187, 31, 102, 121, 14, 198, 202, 133, 249, 221, + 22, 60, 55, 46, 12, 43, 226, 195, 167, 208, 193, 78, 247, 169, 151, 255, 215, 241, 92, 175, + 239, 134, 208, 37, 97, 234, 209, 161, 53, 165, 40, 34, 193, 65, 166, 81, 164, 72, 62, 60, + 149, 224, 228, 83, + ]; + let mint_kp = Keypair::from_bytes(&mint_key_bytes).unwrap(); + let mint_pubkey = mint_kp.pubkey(); + use solana_program::system_instruction::create_account; + use solana_sdk::transaction::Transaction; + use spl_token::instruction::initialize_mint2; + use spl_token::state::Mint; + + let mint_size = Mint::LEN; + + let ix1 = create_account( + &context.default_payer.pubkey(), + &mint_kp.pubkey(), + context.svm.minimum_balance_for_rent_exemption(mint_size), + mint_size as u64, + &spl_token::ID, + ); + let ix2 = initialize_mint2( + &spl_token::ID, + &mint_kp.pubkey(), + &context.default_payer.pubkey(), + None, + 9, + ) + .unwrap(); + let block_hash = context.svm.latest_blockhash(); + let tx = Transaction::new_signed_with_payer( + &[ix1, ix2], + Some(&context.default_payer.pubkey()), + &[&context.default_payer, &mint_kp], + block_hash, + ); + let tx_sig = context.svm.send_transaction(tx).unwrap(); + Ok(mint_pubkey) +} + pub fn mint_to( svm: &mut LiteSVM, mint: &Pubkey, @@ -858,3 +1033,178 @@ fn test_compressed_key_generation() { println!("✓ Compressed key generation test passed"); } + +pub fn display_swig(swig_pubkey: Pubkey, data: &[u8], lamports: u64) -> Result<()> { + let swig_with_roles = SwigWithRoles::from_bytes(data) + .map_err(|e| anyhow::anyhow!("Failed to deserialize swig {:?}", e))?; + + println!("╔══════════════════════════════════════════════════════════════════"); + println!("║ SWIG WALLET DETAILS"); + println!("╠══════════════════════════════════════════════════════════════════"); + println!("║ Account Address: {}", swig_pubkey); + println!("║ Total Roles: {}", swig_with_roles.state.role_counter); + println!("║ Balance: {} SOL", lamports as f64 / 1_000_000_000.0); + + println!("╠══════════════════════════════════════════════════════════════════"); + println!("║ ROLES & PERMISSIONS"); + println!("╠══════════════════════════════════════════════════════════════════"); + + for i in 0..swig_with_roles.state.role_counter { + let role = swig_with_roles + .get_role(i) + .map_err(|e| anyhow::anyhow!("Failed to get role {:?}", e))?; + + if let Some(role) = role { + println!("║"); + println!("║ Role ID: {}", i); + println!( + "║ ├─ Type: {}", + if role.authority.session_based() { + "Session-based Authority" + } else { + "Permanent Authority" + } + ); + println!("║ ├─ Authority Type: {:?}", role.authority.authority_type()); + println!( + "║ ├─ Authority: {}", + match role.authority.authority_type() { + AuthorityType::Ed25519 | AuthorityType::Ed25519Session => { + let authority = role.authority.identity().unwrap(); + let authority = bs58::encode(authority).into_string(); + authority + }, + AuthorityType::Secp256k1 | AuthorityType::Secp256k1Session => { + let authority = role.authority.identity().unwrap(); + let authority_hex = hex::encode([&[0x4].as_slice(), authority].concat()); + // get eth address from public key + let mut hasher = solana_sdk::keccak::Hasher::default(); + hasher.hash(authority_hex.as_bytes()); + let hash = hasher.result(); + let address = format!("0x{}", hex::encode(&hash.0[12..32])); + format!( + "{} \n║ │ ├─ odometer: {:?}", + address, + role.authority.signature_odometer() + ) + }, + AuthorityType::Secp256r1 | AuthorityType::Secp256r1Session => { + let authority = role.authority.identity().unwrap(); + let authority_hex = hex::encode(authority); + format!( + "Secp256r1: {} \n║ │ ├─ odometer: {:?}", + authority_hex, + role.authority.signature_odometer() + ) + }, + _ => "Unknown authority type".to_string(), + } + ); + + println!("║ ├─ Permissions:"); + + // Check All permission + if (Role::get_action::(&role, &[]) + .map_err(|_| anyhow::anyhow!("Failed to get action"))?) + .is_some() + { + println!("║ │ ├─ Full Access (All Permissions)"); + } + + // Check Manage Authority permission + if (Role::get_action::(&role, &[]) + .map_err(|_| anyhow::anyhow!("Failed to get action"))?) + .is_some() + { + println!("║ │ ├─ Manage Authority"); + } + + // Check Oracle limit + let actions = Role::get_all_actions_of_type::(&role) + .map_err(|_| anyhow::anyhow!("Failed to get action"))?; + if !actions.is_empty() { + println!("║ │ ├─ Oracle Token Limit:"); + for action in actions { + println!( + "║ │ │ ├─ Oracle Base Asset: {}", + match action.base_asset_type { + 0 => "USD", + 1 => "EUR", + _ => "Unknown", + } + ); + println!( + "║ │ │ ├─ Value Limit: {}", + action.value_limit as f64 + / 10_f64.powf(action.get_base_asset_decimals() as f64) + ); + println!( + "║ │ │ ├─ Passthrough Check Enabled: {}", + action.passthrough_check + ); + } + } + + let actions = Role::get_all_actions_of_type::(&role) + .map_err(|_| anyhow::anyhow!("Failed to get action"))?; + if !actions.is_empty() { + println!("║ │ ├─ Oracle Recurring Limit:"); + for action in actions { + println!( + "║ │ ├─ Oracle Base Asset: {}", + match action.base_asset_type { + 0 => "USD", + 1 => "EUR", + _ => "Unknown", + } + ); + println!( + "║ │ │ ├─ Value Limit: {}", + action.recurring_value_limit as f64 + / 10_f64.powf(action.get_base_asset_decimals() as f64) + ); + println!("║ │ │ ├─ Window: {} slots", action.window); + println!( + "║ │ │ ├─ Current Usage: {}", + action.current_amount as f64 + / 10_f64.powf(action.get_base_asset_decimals() as f64) + ); + println!("║ │ │ └─ Last Reset: Slot {}", action.last_reset); + } + } + + // Check Sol Limit + if let Some(action) = Role::get_action::(&role, &[]) + .map_err(|_| anyhow::anyhow!("Failed to get action"))? + { + println!( + "║ │ ├─ SOL Limit: {} SOL", + action.amount as f64 / 1_000_000_000.0 + ); + } + + // Check Sol Recurring Limit + if let Some(action) = Role::get_action::(&role, &[]) + .map_err(|_| anyhow::anyhow!("Failed to get action"))? + { + println!("║ │ ├─ Recurring SOL Limit:"); + println!( + "║ │ │ ├─ Amount: {} SOL", + action.recurring_amount as f64 / 1_000_000_000.0 + ); + println!("║ │ │ ├─ Window: {} slots", action.window); + println!( + "║ │ │ ├─ Current Usage: {} SOL", + action.current_amount as f64 / 1_000_000_000.0 + ); + println!("║ │ │ └─ Last Reset: Slot {}", action.last_reset); + } + + println!("║ │ "); + } + } + + println!("╚══════════════════════════════════════════════════════════════════"); + + Ok(()) +} diff --git a/program/tests/oracle_limit_tests.rs b/program/tests/oracle_limit_tests.rs new file mode 100644 index 00000000..5a0675d9 --- /dev/null +++ b/program/tests/oracle_limit_tests.rs @@ -0,0 +1,1324 @@ +#![cfg(not(feature = "program_scope_test"))] +// This feature flag ensures these tests are only run when the +// "program_scope_test" feature is not enabled. This allows us to isolate +// and run only program_scope tests or only the regular tests. + +mod common; + +use std::str::FromStr; + +use common::*; +use litesvm::LiteSVM; +use litesvm_token::spl_token; +use solana_program::{pubkey::Pubkey, system_instruction}; +use solana_sdk::{ + instruction::{AccountMeta, Instruction}, + message::{v0, VersionedMessage}, + signature::{Keypair, Signer}, + transaction::{Transaction, VersionedTransaction}, +}; +use swig_interface::{AuthorityConfig, ClientAction}; +use swig_state::{ + action::{ + all::All, + oracle_limits::{BaseAsset, OracleTokenLimit}, + program_all::ProgramAll, + sol_limit::SolLimit, + token_limit::TokenLimit, + Permission, + }, + authority::AuthorityType, + role::Role, + swig::{swig_wallet_address_seeds, SwigWithRoles}, +}; + +/// Test 1: Verify oracle limit permission is added correctly +#[test_log::test] +fn test_oracle_limit_permission_add() { + let mut context = setup_test_context().unwrap(); + let swig_authority = Keypair::new(); + context + .svm + .airdrop(&swig_authority.pubkey(), 10_000_000_000) + .unwrap(); + + // Create a swig wallet + let id = rand::random::<[u8; 32]>(); + let oracle_program = Keypair::new(); + let (swig_key, _) = create_swig_ed25519(&mut context, &swig_authority, id).unwrap(); + + let (swig_wallet_address, wallet_address_bump) = + Pubkey::find_program_address(&swig_wallet_address_seeds(swig_key.as_ref()), &program_id()); + + // Create secondary authority + let secondary_authority = Keypair::new(); + context + .svm + .airdrop(&secondary_authority.pubkey(), 10_000_000_000) + .unwrap(); + + // Add multiple permissions: Oracle Token Limit (200 USD) and SOL Limit (1 SOL) + let oracle_limit = OracleTokenLimit::new( + BaseAsset::USD, + 250_000_000, // 200 USD + false, + ); + + let sol_limit = SolLimit { + amount: 1_000_000_000, // 1 SOL + }; + + // Add authority with multiple permissions + add_authority_with_ed25519_root( + &mut context, + &swig_key, + &swig_authority, + AuthorityConfig { + authority_type: AuthorityType::Ed25519, + authority: secondary_authority.pubkey().as_ref(), + }, + vec![ + ClientAction::OracleTokenLimit(oracle_limit), + ClientAction::SolLimit(sol_limit), + ClientAction::ProgramAll(ProgramAll {}), + ], + ) + .unwrap(); + + // Verify permissions were added correctly + let swig_account = context.svm.get_account(&swig_key).unwrap(); + let swig = SwigWithRoles::from_bytes(&swig_account.data).unwrap(); + let role_id = swig + .lookup_role_id(secondary_authority.pubkey().as_ref()) + .unwrap() + .unwrap(); + let role = swig.get_role(role_id).unwrap().unwrap(); + + // Verify both permissions exist + assert_eq!(role.position.num_actions(), 3, "Should have 3 actions"); + + let oracle_action = role + .get_action::(&[BaseAsset::USD as u8]) + .unwrap() + .unwrap(); + assert_eq!(oracle_action.value_limit, 250_000_000); + assert_eq!(oracle_action.base_asset_type, BaseAsset::USD as u8); + + let sol_action = role.get_action::(&[]).unwrap().unwrap(); + assert_eq!(sol_action.amount, 1_000_000_000); +} + +/// Test 2: Test SOL transfers with oracle limits +#[test_log::test] +fn test_oracle_limit_sol_transfer() { + let mut context = setup_test_context().unwrap(); + load_sample_scope_data(&mut context.svm, &context.default_payer).unwrap(); + + let swig_authority = Keypair::new(); + context + .svm + .airdrop(&swig_authority.pubkey(), 10_000_000_000) + .unwrap(); + + // Create wallet and setup + let id = rand::random::<[u8; 32]>(); + let oracle_program = Keypair::new(); + let (swig_key, _) = create_swig_ed25519(&mut context, &swig_authority, id).unwrap(); + + let (swig_wallet_address, wallet_address_bump) = + Pubkey::find_program_address(&swig_wallet_address_seeds(swig_key.as_ref()), &program_id()); + + let secondary_authority = Keypair::new(); + context + .svm + .airdrop(&secondary_authority.pubkey(), 10_000_000_000) + .unwrap(); + + // Add oracle limit permission (200 USD limit) + let oracle_limit = OracleTokenLimit::new( + BaseAsset::EUR, + 250_000_000, // 200 USD limit + false, + ); + + add_authority_with_ed25519_root( + &mut context, + &swig_key, + &swig_authority, + AuthorityConfig { + authority_type: AuthorityType::Ed25519, + authority: secondary_authority.pubkey().as_ref(), + }, + vec![ + ClientAction::OracleTokenLimit(oracle_limit), + ClientAction::ProgramAll(ProgramAll {}), + ], + ) + .unwrap(); + + // Fund swig wallet + context + .svm + .airdrop(&swig_wallet_address, 20_000_000_000) + .unwrap(); + + let swig_account = context.svm.get_account(&swig_key).unwrap(); + display_swig(swig_key, &swig_account.data, swig_account.lamports).unwrap(); + + // Test 1: Transfer below limit (1 SOL ≈ 150 USD at mock price) + let transfer_ix = system_instruction::transfer( + &swig_wallet_address, + &secondary_authority.pubkey(), + 1_000_000_000, + ); + let mut sign_ix = swig_interface::SignV2Instruction::new_ed25519( + swig_key, + swig_wallet_address, + secondary_authority.pubkey(), + transfer_ix, + 1, + ) + .unwrap(); + + let message = v0::Message::try_compile( + &secondary_authority.pubkey(), + &[sign_ix], + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let tx = VersionedTransaction::try_new(VersionedMessage::V0(message), &[&secondary_authority]) + .unwrap(); + + let result = context.svm.send_transaction(tx); + assert!(result.is_ok(), "Transfer below limit should succeed"); + println!( + "Compute units consumed for below limit transfer: {}", + result.unwrap().compute_units_consumed + ); + + // Test 2: Transfer above limit (2 SOL ≈ 300 Eur at mock price) + let transfer_ix = system_instruction::transfer( + &swig_wallet_address, + &secondary_authority.pubkey(), + 2_000_000_000, + ); + let mut sign_ix = swig_interface::SignV2Instruction::new_ed25519( + swig_key, + swig_wallet_address, + secondary_authority.pubkey(), + transfer_ix, + 1, + ) + .unwrap(); + + let message = v0::Message::try_compile( + &secondary_authority.pubkey(), + &[sign_ix], + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let tx = VersionedTransaction::try_new(VersionedMessage::V0(message), &[&secondary_authority]) + .unwrap(); + + let result = context.svm.send_transaction(tx); + assert!(result.is_err(), "Transfer above limit should fail"); + assert_eq!( + result.unwrap_err().err, + solana_sdk::transaction::TransactionError::InstructionError( + 0, + solana_sdk::instruction::InstructionError::Custom(3033) + ), + "Expected error code 3033" + ); +} + +/// Test 3: Test token transfers with oracle limits +#[test_log::test] +fn test_oracle_limit_token_transfer() { + let mut context = setup_test_context().unwrap(); + let mint = load_sample_scope_data(&mut context.svm, &context.default_payer).unwrap(); + let swig_authority = Keypair::new(); + context + .svm + .airdrop(&swig_authority.pubkey(), 10_000_000_000) + .unwrap(); + + // Create wallet and setup + let id = rand::random::<[u8; 32]>(); + let oracle_program = Keypair::new(); + let (swig_key, _) = create_swig_ed25519(&mut context, &swig_authority, id).unwrap(); + + let (swig_wallet_address, wallet_address_bump) = + Pubkey::find_program_address(&swig_wallet_address_seeds(swig_key.as_ref()), &program_id()); + + let secondary_authority = Keypair::new(); + context + .svm + .airdrop(&secondary_authority.pubkey(), 10_000_000_000) + .unwrap(); + + // Add oracle limit permission (300 USD limit) + let oracle_limit = OracleTokenLimit::new( + BaseAsset::USD, + 300_000_000, // 300 USD with 6 decimals + false, + ); + + add_authority_with_ed25519_root( + &mut context, + &swig_key, + &swig_authority, + AuthorityConfig { + authority_type: AuthorityType::Ed25519, + authority: secondary_authority.pubkey().as_ref(), + }, + vec![ + ClientAction::OracleTokenLimit(oracle_limit), + ClientAction::ProgramAll(ProgramAll {}), + ], + ) + .unwrap(); + + let oracle_mint = mint; + let swig_ata = setup_ata( + &mut context.svm, + &oracle_mint, + &swig_wallet_address, + &context.default_payer, + ) + .unwrap(); + let recipient_ata = setup_ata( + &mut context.svm, + &oracle_mint, + &secondary_authority.pubkey(), + &context.default_payer, + ) + .unwrap(); + + // Fund swig's token account with 10 tokens + mint_to( + &mut context.svm, + &oracle_mint, + &context.default_payer, + &swig_ata, + 100_000_000_000, // 10 tokens with 9 decimals + ) + .unwrap(); + + // Test 1: Transfer below limit (0.5 tokens ≈ 0.75 USD at mock price of 1.5 USD per token) + let transfer_ix = spl_token::instruction::transfer( + &spl_token::id(), + &swig_ata, + &recipient_ata, + &swig_wallet_address, + &[], + 500_000_000, // 0.5 tokens with 9 decimals + ) + .unwrap(); + + let mut sign_ix = swig_interface::SignV2Instruction::new_ed25519( + swig_key, + swig_wallet_address, + secondary_authority.pubkey(), + transfer_ix, + 1, + ) + .unwrap(); + + let message = v0::Message::try_compile( + &secondary_authority.pubkey(), + &[sign_ix], + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let tx = VersionedTransaction::try_new(VersionedMessage::V0(message), &[&secondary_authority]) + .unwrap(); + + let result = context.svm.send_transaction(tx); + println!("result: {:?}", result); + assert!(result.is_ok(), "Transfer below limit should succeed"); + println!( + "Compute units consumed for below limit transfer: {}", + result.unwrap().compute_units_consumed + ); + + // Test 2: Transfer above limit (2.5 tokens ≈ 3.75 USD at mock price) + let transfer_ix = spl_token::instruction::transfer( + &spl_token::id(), + &swig_ata, + &recipient_ata, + &swig_wallet_address, + &[], + 2_500_000_000, // 2.5 tokens with 9 decimals + ) + .unwrap(); + + let mut sign_ix = swig_interface::SignV2Instruction::new_ed25519( + swig_key, + swig_wallet_address, + secondary_authority.pubkey(), + transfer_ix, + 1, + ) + .unwrap(); + + let message = v0::Message::try_compile( + &secondary_authority.pubkey(), + &[sign_ix], + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let tx = VersionedTransaction::try_new(VersionedMessage::V0(message), &[&secondary_authority]) + .unwrap(); + + let result = context.svm.send_transaction(tx); + assert!(result.is_err(), "Transfer above limit should fail"); + assert_eq!( + result.unwrap_err().err, + solana_sdk::transaction::TransactionError::InstructionError( + 0, + solana_sdk::instruction::InstructionError::Custom(3033) + ), + "Expected error code 3033" + ); +} + +/// Test 4: Test SOL transfers with oracle limits and passthrough enabled +#[test_log::test] +fn test_oracle_limit_sol_passthrough() { + let mut context = setup_test_context().unwrap(); + load_sample_scope_data(&mut context.svm, &context.default_payer).unwrap(); + + let swig_authority = Keypair::new(); + context + .svm + .airdrop(&swig_authority.pubkey(), 10_000_000_000) + .unwrap(); + + // Create wallet and setup + let id = rand::random::<[u8; 32]>(); + let oracle_program = Keypair::new(); + let (swig_key, _) = create_swig_ed25519(&mut context, &swig_authority, id).unwrap(); + + let (swig_wallet_address, wallet_address_bump) = + Pubkey::find_program_address(&swig_wallet_address_seeds(swig_key.as_ref()), &program_id()); + + let secondary_authority = Keypair::new(); + context + .svm + .airdrop(&secondary_authority.pubkey(), 10_000_000_000) + .unwrap(); + + // Add oracle limit permission (200 USD limit) with passthrough enabled + let oracle_limit = OracleTokenLimit::new( + BaseAsset::USD, + 250_000_000, // 200 USD limit + true, + ); + + add_authority_with_ed25519_root( + &mut context, + &swig_key, + &swig_authority, + AuthorityConfig { + authority_type: AuthorityType::Ed25519, + authority: secondary_authority.pubkey().as_ref(), + }, + vec![ + ClientAction::OracleTokenLimit(oracle_limit), + ClientAction::SolLimit(SolLimit { + amount: 100_000_000_000, + }), + ClientAction::ProgramAll(ProgramAll {}), + ], + ) + .unwrap(); + + // Fund swig wallet + context + .svm + .airdrop(&swig_wallet_address, 20_000_000_000) + .unwrap(); + + // Test 1: Transfer below limit (1 SOL ≈ 150 USD at mock price) + let transfer_ix = system_instruction::transfer( + &swig_wallet_address, + &secondary_authority.pubkey(), + 1_000_000_000, + ); + let mut sign_ix = swig_interface::SignV2Instruction::new_ed25519( + swig_key, + swig_wallet_address, + secondary_authority.pubkey(), + transfer_ix, + 1, + ) + .unwrap(); + + let message = v0::Message::try_compile( + &secondary_authority.pubkey(), + &[sign_ix], + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let tx = VersionedTransaction::try_new(VersionedMessage::V0(message), &[&secondary_authority]) + .unwrap(); + + let result = context.svm.send_transaction(tx); + println!("result: {:?}", result); + assert!(result.is_ok(), "Transfer below limit should succeed"); + println!( + "Compute units consumed for below limit transfer: {}", + result.unwrap().compute_units_consumed + ); + + // Test 2: Transfer above limit (2 SOL ≈ 300 USD at mock price) + let transfer_ix = system_instruction::transfer( + &swig_wallet_address, + &secondary_authority.pubkey(), + 2_000_000_000, + ); + let mut sign_ix = swig_interface::SignV2Instruction::new_ed25519( + swig_key, + swig_wallet_address, + secondary_authority.pubkey(), + transfer_ix, + 1, + ) + .unwrap(); + + let message = v0::Message::try_compile( + &secondary_authority.pubkey(), + &[sign_ix], + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let tx = VersionedTransaction::try_new(VersionedMessage::V0(message), &[&secondary_authority]) + .unwrap(); + + let result = context.svm.send_transaction(tx); + assert!(result.is_err(), "Transfer above limit should fail"); + assert_eq!( + result.unwrap_err().err, + solana_sdk::transaction::TransactionError::InstructionError( + 0, + solana_sdk::instruction::InstructionError::Custom(3033) + ), + "Expected error code 3033" + ); +} + +/// Test 5: Test token transfers with oracle limits and passthrough enabled +#[test_log::test] +fn test_oracle_limit_token_passthrough() { + let mut context = setup_test_context().unwrap(); + let mint = load_sample_scope_data(&mut context.svm, &context.default_payer).unwrap(); + let swig_authority = Keypair::new(); + context + .svm + .airdrop(&swig_authority.pubkey(), 10_000_000_000) + .unwrap(); + + // Create wallet and setup + let id = rand::random::<[u8; 32]>(); + let (swig_key, _) = create_swig_ed25519(&mut context, &swig_authority, id).unwrap(); + + let (swig_wallet_address, wallet_address_bump) = + Pubkey::find_program_address(&swig_wallet_address_seeds(swig_key.as_ref()), &program_id()); + + let secondary_authority = Keypair::new(); + context + .svm + .airdrop(&secondary_authority.pubkey(), 10_000_000_000) + .unwrap(); + + // Add oracle limit permission (300 USD limit) with passthrough enabled + let oracle_limit = OracleTokenLimit::new( + BaseAsset::USD, + 300_000_000, // 300 USD with 6 decimals + true, + ); + + let oracle_mint = mint; + + // Setup token accounts + let swig_ata = setup_ata( + &mut context.svm, + &oracle_mint, + &swig_wallet_address, + &context.default_payer, + ) + .unwrap(); + let recipient_ata = setup_ata( + &mut context.svm, + &oracle_mint, + &secondary_authority.pubkey(), + &context.default_payer, + ) + .unwrap(); + + let mint_bytes = oracle_mint.to_bytes(); + + add_authority_with_ed25519_root( + &mut context, + &swig_key, + &swig_authority, + AuthorityConfig { + authority_type: AuthorityType::Ed25519, + authority: secondary_authority.pubkey().as_ref(), + }, + vec![ + ClientAction::OracleTokenLimit(oracle_limit), + ClientAction::TokenLimit(TokenLimit { + token_mint: mint_bytes, + current_amount: 600_000_000, + }), + ClientAction::ProgramAll(ProgramAll {}), + ], + ) + .unwrap(); + + // Fund swig's token account with 10 tokens + mint_to( + &mut context.svm, + &oracle_mint, + &context.default_payer, + &swig_ata, + 10_000_000_000, // 10 tokens with 9 decimals + ) + .unwrap(); + + // Test 1: Transfer below limit (0.5 tokens ≈ 0.75 USD at mock price of 1.5 USD per token) + let transfer_ix = spl_token::instruction::transfer( + &spl_token::id(), + &swig_ata, + &recipient_ata, + &swig_wallet_address, + &[], + 500_000_000, // 0.5 tokens with 9 decimals + ) + .unwrap(); + + let mut sign_ix = swig_interface::SignV2Instruction::new_ed25519( + swig_key, + swig_wallet_address, + secondary_authority.pubkey(), + transfer_ix, + 1, + ) + .unwrap(); + + let message = v0::Message::try_compile( + &secondary_authority.pubkey(), + &[sign_ix], + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let tx = VersionedTransaction::try_new(VersionedMessage::V0(message), &[&secondary_authority]) + .unwrap(); + + let result = context.svm.send_transaction(tx); + assert!(result.is_ok(), "Transfer below limit should succeed"); + println!( + "Compute units consumed for below limit transfer: {}", + result.unwrap().compute_units_consumed + ); + + // Test 2: Transfer above limit (2.5 tokens ≈ 3.75 USD at mock price) + let transfer_ix = spl_token::instruction::transfer( + &spl_token::id(), + &swig_ata, + &recipient_ata, + &swig_wallet_address, + &[], + 2_500_000_000, // 2.5 tokens with 9 decimals + ) + .unwrap(); + + let mut sign_ix = swig_interface::SignV2Instruction::new_ed25519( + swig_key, + swig_wallet_address, + secondary_authority.pubkey(), + transfer_ix, + 1, + ) + .unwrap(); + + let message = v0::Message::try_compile( + &secondary_authority.pubkey(), + &[sign_ix], + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let tx = VersionedTransaction::try_new(VersionedMessage::V0(message), &[&secondary_authority]) + .unwrap(); + + let result = context.svm.send_transaction(tx); + assert!(result.is_err(), "Transfer above limit should fail"); + assert_eq!( + result.unwrap_err().err, + solana_sdk::transaction::TransactionError::InstructionError( + 0, + solana_sdk::instruction::InstructionError::Custom(3033) + ), + "Expected error code 3033" + ); +} + +#[test_log::test] +fn test_oracle_stale_price() { + let mut context = setup_test_context().unwrap(); + load_sample_scope_data(&mut context.svm, &context.default_payer).unwrap(); + + let swig_authority = Keypair::new(); + context + .svm + .airdrop(&swig_authority.pubkey(), 10_000_000_000) + .unwrap(); + + // Create wallet and setup + let id = rand::random::<[u8; 32]>(); + let oracle_program = Keypair::new(); + let (swig_key, _) = create_swig_ed25519(&mut context, &swig_authority, id).unwrap(); + + let (swig_wallet_address, wallet_address_bump) = + Pubkey::find_program_address(&swig_wallet_address_seeds(swig_key.as_ref()), &program_id()); + + let secondary_authority = Keypair::new(); + context + .svm + .airdrop(&secondary_authority.pubkey(), 10_000_000_000) + .unwrap(); + + // Add oracle limit permission (200 USD limit) + let oracle_limit = OracleTokenLimit::new( + BaseAsset::USD, + 250_000_000, // 200 USD limit + false, + ); + + add_authority_with_ed25519_root( + &mut context, + &swig_key, + &swig_authority, + AuthorityConfig { + authority_type: AuthorityType::Ed25519, + authority: secondary_authority.pubkey().as_ref(), + }, + vec![ + ClientAction::OracleTokenLimit(oracle_limit), + ClientAction::ProgramAll(ProgramAll {}), + ], + ) + .unwrap(); + + // Fund swig wallet + context + .svm + .airdrop(&swig_wallet_address, 20_000_000_000) + .unwrap(); + + // Test 1: Transfer with stale price + advance_slot(&mut context, 250); + + let transfer_ix = system_instruction::transfer( + &swig_wallet_address, + &secondary_authority.pubkey(), + 1_000_000_000, + ); + let mut sign_ix = swig_interface::SignV2Instruction::new_ed25519( + swig_key, + swig_wallet_address, + secondary_authority.pubkey(), + transfer_ix, + 1, + ) + .unwrap(); + + let message = v0::Message::try_compile( + &secondary_authority.pubkey(), + &[sign_ix], + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let tx = VersionedTransaction::try_new(VersionedMessage::V0(message), &[&secondary_authority]) + .unwrap(); + + let result = context.svm.send_transaction(tx); + assert!(result.is_err(), "Transfer with stale price should fail"); + assert_eq!( + result.unwrap_err().err, + solana_sdk::transaction::TransactionError::InstructionError( + 0, + solana_sdk::instruction::InstructionError::Custom(52) + ), + "Expected error code 63" + ); +} + +/// This test compares the baseline performance of: +/// 1. A regular SOL transfer (outside of swig) +/// 2. A SOL transfer using swig without oracle +/// 3. A SOL transfer using swig with oracle limit +/// It measures and compares compute units consumption and accounts used +#[test_log::test] +fn test_oracle_sol_transfer_performance_comparison() { + let mut context = setup_test_context().unwrap(); + + // Setup oracle data + load_sample_scope_data(&mut context.svm, &context.default_payer).unwrap(); + + // Setup payers and recipients + let swig_authority = Keypair::new(); + let secondary_authority = Keypair::new(); + let regular_sender = Keypair::new(); + let recipient = Keypair::new(); + + // Airdrop to participants + context + .svm + .airdrop(&swig_authority.pubkey(), 10_000_000_000) + .unwrap(); + context + .svm + .airdrop(&secondary_authority.pubkey(), 10_000_000_000) + .unwrap(); + context + .svm + .airdrop(®ular_sender.pubkey(), 10_000_000_000) + .unwrap(); + context + .svm + .airdrop(&recipient.pubkey(), 10_000_000_000) + .unwrap(); + + // Setup swig account + let id = rand::random::<[u8; 32]>(); + let (swig, _) = create_swig_ed25519(&mut context, &swig_authority, id).unwrap(); + + let (swig_wallet_address, wallet_address_bump) = + Pubkey::find_program_address(&swig_wallet_address_seeds(swig.as_ref()), &program_id()); + + // Fund swig wallet with SOL + context + .svm + .airdrop(&swig_wallet_address, 20_000_000_000) + .unwrap(); + + // Add secondary authority with oracle limit permission (1000 USD limit) + let oracle_limit = OracleTokenLimit::new( + BaseAsset::USD, + 1_000_000_000, // 1000 USD with 6 decimals + false, + ); + + add_authority_with_ed25519_root( + &mut context, + &swig, + &swig_authority, + AuthorityConfig { + authority_type: AuthorityType::Ed25519, + authority: secondary_authority.pubkey().as_ref(), + }, + vec![ + ClientAction::OracleTokenLimit(oracle_limit), + ClientAction::ProgramAll(ProgramAll {}), + ], + ) + .unwrap(); + + // Measure regular SOL transfer performance + let transfer_amount = 1_000_000_000; // 1 SOL + + let regular_transfer_ix = system_instruction::transfer( + ®ular_sender.pubkey(), + &recipient.pubkey(), + transfer_amount, + ); + + let regular_transfer_message = v0::Message::try_compile( + ®ular_sender.pubkey(), + &[regular_transfer_ix], + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let regular_tx_accounts = regular_transfer_message.account_keys.len(); + + let regular_transfer_tx = VersionedTransaction::try_new( + VersionedMessage::V0(regular_transfer_message), + &[regular_sender], + ) + .unwrap(); + + let regular_transfer_result = context.svm.send_transaction(regular_transfer_tx).unwrap(); + let regular_transfer_cu = regular_transfer_result.compute_units_consumed; + + println!("Regular SOL transfer CU: {}", regular_transfer_cu); + println!("Regular SOL transfer accounts: {}", regular_tx_accounts); + + // Measure swig SOL transfer performance (without oracle) + let swig_transfer_ix = + system_instruction::transfer(&swig_wallet_address, &recipient.pubkey(), transfer_amount); + + let sign_ix = swig_interface::SignV2Instruction::new_ed25519( + swig, + swig_wallet_address, + swig_authority.pubkey(), + swig_transfer_ix, + 0, // authority role id + ) + .unwrap(); + + let swig_transfer_message = v0::Message::try_compile( + &swig_authority.pubkey(), + &[sign_ix], + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let swig_tx_accounts = swig_transfer_message.account_keys.len(); + + let swig_transfer_tx = VersionedTransaction::try_new( + VersionedMessage::V0(swig_transfer_message), + &[swig_authority], + ) + .unwrap(); + + let swig_transfer_result = context.svm.send_transaction(swig_transfer_tx).unwrap(); + let swig_transfer_cu = swig_transfer_result.compute_units_consumed; + println!("Swig SOL transfer CU: {}", swig_transfer_cu); + println!("Swig SOL transfer accounts: {}", swig_tx_accounts); + + // Measure swig SOL transfer performance (with oracle) + let swig_oracle_transfer_ix = + system_instruction::transfer(&swig_wallet_address, &recipient.pubkey(), transfer_amount); + + let mut swig_oracle_sign_ix = swig_interface::SignV2Instruction::new_ed25519( + swig, + swig_wallet_address, + secondary_authority.pubkey(), + swig_oracle_transfer_ix, + 1, // secondary authority role id + ) + .unwrap(); + + let swig_oracle_transfer_message = v0::Message::try_compile( + &secondary_authority.pubkey(), + &[swig_oracle_sign_ix], + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let swig_oracle_tx_accounts = swig_oracle_transfer_message.account_keys.len(); + + let swig_oracle_transfer_tx = VersionedTransaction::try_new( + VersionedMessage::V0(swig_oracle_transfer_message), + &[secondary_authority], + ) + .unwrap(); + + let swig_oracle_transfer_result = context + .svm + .send_transaction(swig_oracle_transfer_tx) + .unwrap(); + println!( + "swig_oracle_transfer_result: {:?}", + swig_oracle_transfer_result + ); + let swig_oracle_transfer_cu = swig_oracle_transfer_result.compute_units_consumed; + println!("Swig oracle SOL transfer CU: {}", swig_oracle_transfer_cu); + println!( + "Swig oracle SOL transfer accounts: {}", + swig_oracle_tx_accounts + ); + + // Compare results + let swig_cu_difference = swig_transfer_cu as i64 - regular_transfer_cu as i64; + let swig_account_difference = swig_tx_accounts as i64 - regular_tx_accounts as i64; + let oracle_cu_difference = swig_oracle_transfer_cu as i64 - regular_transfer_cu as i64; + let oracle_account_difference = swig_oracle_tx_accounts as i64 - regular_tx_accounts as i64; + let oracle_overhead = swig_oracle_transfer_cu as i64 - swig_transfer_cu as i64; + + println!("\n=== SOL Transfer Performance Comparison ==="); + println!("Regular SOL transfer:"); + println!( + " CU: {} | Accounts: {}", + regular_transfer_cu, regular_tx_accounts + ); + + println!("\nSwig SOL transfer (without oracle):"); + println!( + " CU: {} | Accounts: {}", + swig_transfer_cu, swig_tx_accounts + ); + println!( + " Overhead: {} CU ({:.2}%) | {} accounts", + swig_cu_difference, + (swig_cu_difference as f64 / regular_transfer_cu as f64) * 100.0, + swig_account_difference + ); + + println!("\nSwig SOL transfer (with oracle):"); + println!( + " CU: {} | Accounts: {}", + swig_oracle_transfer_cu, swig_oracle_tx_accounts + ); + println!( + " Total overhead: {} CU ({:.2}%) | {} accounts", + oracle_cu_difference, + (oracle_cu_difference as f64 / regular_transfer_cu as f64) * 100.0, + oracle_account_difference + ); + println!( + " Oracle overhead: {} CU ({:.2}%) | 2 accounts", + oracle_overhead, + (oracle_overhead as f64 / regular_transfer_cu as f64) * 100.0 + ); + + // Assertions for performance limits + // Swig overhead should be reasonable + assert!( + swig_transfer_cu - regular_transfer_cu <= 3777, + "Swig overhead too high" + ); + + // Oracle overhead should be reasonable (additional oracle processing) + assert!( + swig_oracle_transfer_cu - swig_transfer_cu <= 12000, + "Oracle overhead too high" + ); + + // Total oracle overhead should be reasonable + assert!( + swig_oracle_transfer_cu - regular_transfer_cu <= 12000, + "Total oracle overhead too high" + ); +} + +/// This test compares the baseline performance of: +/// 1. A regular token transfer (outside of swig) +/// 2. A token transfer using swig with oracle limit +/// It measures and compares compute units consumption and accounts used +#[test_log::test] +fn test_oracle_token_transfer_performance_comparison() { + let mut context = setup_test_context().unwrap(); + + // Setup oracle data + let oracle_mint = load_sample_scope_data(&mut context.svm, &context.default_payer).unwrap(); + + // Setup payers and recipients + let swig_authority = Keypair::new(); + let secondary_authority = Keypair::new(); + let regular_sender = Keypair::new(); + let recipient = Keypair::new(); + + // Airdrop to participants + context + .svm + .airdrop(&swig_authority.pubkey(), 10_000_000_000) + .unwrap(); + context + .svm + .airdrop(&secondary_authority.pubkey(), 10_000_000_000) + .unwrap(); + context + .svm + .airdrop(®ular_sender.pubkey(), 10_000_000_000) + .unwrap(); + context + .svm + .airdrop(&recipient.pubkey(), 10_000_000_000) + .unwrap(); + + // Setup swig account + let id = rand::random::<[u8; 32]>(); + let (swig, _) = create_swig_ed25519(&mut context, &swig_authority, id).unwrap(); + + let (swig_wallet_address, _) = + Pubkey::find_program_address(&swig_wallet_address_seeds(swig.as_ref()), &program_id()); + + // Add secondary authority with oracle limit permission (1000 USD limit) + let oracle_limit = OracleTokenLimit::new( + BaseAsset::USD, + 1_000_000_000, // 1000 USD with 6 decimals + false, + ); + + add_authority_with_ed25519_root( + &mut context, + &swig, + &swig_authority, + AuthorityConfig { + authority_type: AuthorityType::Ed25519, + authority: secondary_authority.pubkey().as_ref(), + }, + vec![ + ClientAction::OracleTokenLimit(oracle_limit), + ClientAction::ProgramAll(ProgramAll {}), + ], + ) + .unwrap(); + + // Setup token accounts + let swig_ata = setup_ata( + &mut context.svm, + &oracle_mint, + &swig_wallet_address, + &context.default_payer, + ) + .unwrap(); + + let regular_sender_ata = setup_ata( + &mut context.svm, + &oracle_mint, + ®ular_sender.pubkey(), + &context.default_payer, + ) + .unwrap(); + + let recipient_ata = setup_ata( + &mut context.svm, + &oracle_mint, + &recipient.pubkey(), + &context.default_payer, + ) + .unwrap(); + + // Mint tokens to both sending accounts + let initial_token_amount = 1000; + mint_to( + &mut context.svm, + &oracle_mint, + &context.default_payer, + &swig_ata, + initial_token_amount, + ) + .unwrap(); + + mint_to( + &mut context.svm, + &oracle_mint, + &context.default_payer, + ®ular_sender_ata, + initial_token_amount, + ) + .unwrap(); + + // Measure regular token transfer performance + let transfer_amount = 100; + let token_program_id = spl_token::ID; + + let regular_transfer_ix = spl_token::instruction::transfer( + &token_program_id, + ®ular_sender_ata, + &recipient_ata, + ®ular_sender.pubkey(), + &[], + transfer_amount, + ) + .unwrap(); + + let regular_transfer_message = v0::Message::try_compile( + ®ular_sender.pubkey(), + &[regular_transfer_ix], + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let regular_tx_accounts = regular_transfer_message.account_keys.len(); + + let regular_transfer_tx = VersionedTransaction::try_new( + VersionedMessage::V0(regular_transfer_message), + &[regular_sender], + ) + .unwrap(); + + let regular_transfer_result = context.svm.send_transaction(regular_transfer_tx).unwrap(); + let regular_transfer_cu = regular_transfer_result.compute_units_consumed; + + println!("Regular token transfer CU: {}", regular_transfer_cu); + println!("Regular token transfer accounts: {}", regular_tx_accounts); + + // Measure swig token transfer performance (without oracle) + let swig_transfer_ix = spl_token::instruction::transfer( + &token_program_id, + &swig_ata, + &recipient_ata, + &swig_wallet_address, + &[], + transfer_amount, + ) + .unwrap(); + + let sign_ix = swig_interface::SignV2Instruction::new_ed25519( + swig, + swig_wallet_address, + swig_authority.pubkey(), + swig_transfer_ix, + 0, // authority role id + ) + .unwrap(); + + let swig_transfer_message = v0::Message::try_compile( + &swig_authority.pubkey(), + &[sign_ix], + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let swig_tx_accounts = swig_transfer_message.account_keys.len(); + + let swig_transfer_tx = VersionedTransaction::try_new( + VersionedMessage::V0(swig_transfer_message), + &[swig_authority], + ) + .unwrap(); + + let swig_account = context.svm.get_account(&swig).unwrap(); + display_swig(swig, &swig_account.data, swig_account.lamports).unwrap(); + + let swig_transfer_result = context.svm.send_transaction(swig_transfer_tx).unwrap(); + + let swig_transfer_cu = swig_transfer_result.compute_units_consumed; + println!( + "Swig token transfer CU (without oracle): {}", + swig_transfer_cu + ); + println!("Swig token transfer accounts: {}", swig_tx_accounts); + + // Measure swig token transfer performance (with oracle) + let swig_oracle_transfer_ix = spl_token::instruction::transfer( + &token_program_id, + &swig_ata, + &recipient_ata, + &swig_wallet_address, + &[], + transfer_amount, + ) + .unwrap(); + + let mut swig_oracle_sign_ix = swig_interface::SignV2Instruction::new_ed25519( + swig, + swig_wallet_address, + secondary_authority.pubkey(), + swig_oracle_transfer_ix, + 1, // secondary authority role id + ) + .unwrap(); + + let swig_oracle_transfer_message = v0::Message::try_compile( + &secondary_authority.pubkey(), + &[swig_oracle_sign_ix], + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let swig_oracle_tx_accounts = swig_oracle_transfer_message.account_keys.len(); + + let swig_oracle_transfer_tx = VersionedTransaction::try_new( + VersionedMessage::V0(swig_oracle_transfer_message), + &[secondary_authority], + ) + .unwrap(); + + let swig_oracle_transfer_result = context + .svm + .send_transaction(swig_oracle_transfer_tx) + .unwrap(); + let swig_oracle_transfer_cu = swig_oracle_transfer_result.compute_units_consumed; + println!( + "Swig oracle token transfer CU (with oracle): {}", + swig_oracle_transfer_cu + ); + println!( + "Swig oracle token transfer accounts: {}", + swig_oracle_tx_accounts + ); + + // Compare results + let swig_cu_difference = swig_transfer_cu as i64 - regular_transfer_cu as i64; + let swig_account_difference = swig_tx_accounts as i64 - regular_tx_accounts as i64; + let oracle_cu_difference = swig_oracle_transfer_cu as i64 - regular_transfer_cu as i64; + let oracle_account_difference = swig_oracle_tx_accounts as i64 - regular_tx_accounts as i64; + let oracle_overhead = swig_oracle_transfer_cu as i64 - swig_transfer_cu as i64; + + println!("\n=== Performance Comparison ==="); + println!("Regular token transfer:"); + println!( + " CU: {} | Accounts: {}", + regular_transfer_cu, regular_tx_accounts + ); + + println!("\nSwig token transfer (without oracle):"); + println!( + " CU: {} | Accounts: {}", + swig_transfer_cu, swig_tx_accounts + ); + println!( + " Overhead: {} CU ({:.2}%) | {} accounts", + swig_cu_difference, + (swig_cu_difference as f64 / regular_transfer_cu as f64) * 100.0, + swig_account_difference + ); + + println!("\nSwig token transfer (with oracle):"); + println!( + " CU: {} | Accounts: {}", + swig_oracle_transfer_cu, swig_oracle_tx_accounts + ); + println!( + " Total overhead: {} CU ({:.2}%) | {} accounts", + oracle_cu_difference, + (oracle_cu_difference as f64 / regular_transfer_cu as f64) * 100.0, + oracle_account_difference + ); + println!( + " Oracle overhead: {} CU ({:.2}%) | 2 accounts", + oracle_overhead, + (oracle_overhead as f64 / regular_transfer_cu as f64) * 100.0 + ); + + // Assertions for performance limits + // Swig overhead should be reasonable + assert!( + swig_transfer_cu - regular_transfer_cu <= 8000, + "Swig overhead too high" + ); + + // Oracle overhead should be reasonable (additional oracle processing) + assert!( + swig_oracle_transfer_cu - swig_transfer_cu <= 8000, + "Oracle overhead too high" + ); + + // Total oracle overhead should be reasonable + assert!( + swig_oracle_transfer_cu - regular_transfer_cu <= 8777, + "Total oracle overhead too high" + ); +} diff --git a/program/tests/oracle_recurring_limit_tests.rs b/program/tests/oracle_recurring_limit_tests.rs new file mode 100644 index 00000000..e21f94f0 --- /dev/null +++ b/program/tests/oracle_recurring_limit_tests.rs @@ -0,0 +1,718 @@ +#![cfg(not(feature = "program_scope_test"))] +// This feature flag ensures these tests are only run when the +// "program_scope_test" feature is not enabled. This allows us to isolate +// and run only program_scope tests or only the regular tests. + +mod common; + +use std::str::FromStr; + +use common::*; +use litesvm::LiteSVM; +use litesvm_token::spl_token; +use solana_program::{pubkey::Pubkey, system_instruction}; +use solana_sdk::{ + instruction::{AccountMeta, Instruction}, + message::{v0, VersionedMessage}, + signature::{Keypair, Signer}, + transaction::{Transaction, VersionedTransaction}, +}; +use swig_interface::{AuthorityConfig, ClientAction}; +use swig_state::{ + action::{ + all::All, + oracle_limits::{BaseAsset, OracleTokenLimit}, + oracle_recurring_limit::OracleRecurringLimit, + program_all::ProgramAll, + sol_limit::SolLimit, + sol_recurring_limit::SolRecurringLimit, + token_limit::TokenLimit, + token_recurring_limit::TokenRecurringLimit, + Permission, + }, + authority::AuthorityType, + role::Role, + swig::{swig_wallet_address_seeds, SwigWithRoles}, +}; + +/// Test 1: Verify oracle recurring limit permission is added correctly +#[test_log::test] +fn test_oracle_recurring_limit_permission_add() { + let mut context = setup_test_context().unwrap(); + let swig_authority = Keypair::new(); + context + .svm + .airdrop(&swig_authority.pubkey(), 10_000_000_000) + .unwrap(); + + // Create a swig wallet + let id = rand::random::<[u8; 32]>(); + let oracle_program = Keypair::new(); + let (swig_key, _) = create_swig_ed25519(&mut context, &swig_authority, id).unwrap(); + + // Create secondary authority + let secondary_authority = Keypair::new(); + context + .svm + .airdrop(&secondary_authority.pubkey(), 10_000_000_000) + .unwrap(); + + // Add multiple permissions: Oracle Recurring Limit (200 USD per window) and SOL Limit (1 SOL) + let oracle_recurring_limit = OracleRecurringLimit::new( + BaseAsset::USD, + 250_000_000, // 200 USD + 1000, // 1000 slots window + false, + ); + + let sol_limit = SolLimit { + amount: 1_000_000_000, // 1 SOL + }; + + // Add authority with multiple permissions + add_authority_with_ed25519_root( + &mut context, + &swig_key, + &swig_authority, + AuthorityConfig { + authority_type: AuthorityType::Ed25519, + authority: secondary_authority.pubkey().as_ref(), + }, + vec![ + ClientAction::OracleRecurringLimit(oracle_recurring_limit), + ClientAction::SolLimit(sol_limit), + ClientAction::ProgramAll(ProgramAll {}), + ], + ) + .unwrap(); + + // Verify permissions were added correctly + let swig_account = context.svm.get_account(&swig_key).unwrap(); + let swig = SwigWithRoles::from_bytes(&swig_account.data).unwrap(); + let role_id = swig + .lookup_role_id(secondary_authority.pubkey().as_ref()) + .unwrap() + .unwrap(); + let role = swig.get_role(role_id).unwrap().unwrap(); + + // Verify both permissions exist + assert_eq!(role.position.num_actions(), 3, "Should have 3 actions"); + + let oracle_recurring_action = role + .get_action::(&[BaseAsset::USD as u8]) + .unwrap() + .unwrap(); + + assert_eq!( + oracle_recurring_action.recurring_value_limit, 250_000_000, + "Oracle recurring limit should be 200 USD" + ); + assert_eq!( + oracle_recurring_action.window, 1000, + "Window should be 1000 slots" + ); + assert_eq!( + oracle_recurring_action.last_reset, 0, + "Last reset should be 0 initially" + ); + assert_eq!( + oracle_recurring_action.current_amount, 250_000_000, + "Current amount should equal recurring limit initially" + ); +} + +/// Test 2: Verify oracle recurring limit for SOL transfers +#[test_log::test] +fn test_oracle_recurring_limit_sol_transfer() { + let mut context = setup_test_context().unwrap(); + let swig_authority = Keypair::new(); + context + .svm + .airdrop(&swig_authority.pubkey(), 10_000_000_000) + .unwrap(); + + // Create a swig wallet + let id = rand::random::<[u8; 32]>(); + let (swig_key, _) = create_swig_ed25519(&mut context, &swig_authority, id).unwrap(); + + let (swig_wallet_address, _) = + Pubkey::find_program_address(&swig_wallet_address_seeds(swig_key.as_ref()), &program_id()); + + // Create secondary authority + let secondary_authority = Keypair::new(); + context + .svm + .airdrop(&secondary_authority.pubkey(), 10_000_000_000) + .unwrap(); + + // Add oracle recurring limit permission (100 USD per window) + let oracle_recurring_limit = OracleRecurringLimit::new( + BaseAsset::USD, + 100_000_000, // 100 USD + 1000, // 1000 slots window + false, + ); + + // Add authority with oracle recurring limit permission + add_authority_with_ed25519_root( + &mut context, + &swig_key, + &swig_authority, + AuthorityConfig { + authority_type: AuthorityType::Ed25519, + authority: secondary_authority.pubkey().as_ref(), + }, + vec![ + ClientAction::OracleRecurringLimit(oracle_recurring_limit), + ClientAction::ProgramAll(ProgramAll {}), + ], + ) + .unwrap(); + + // Load oracle accounts + let mint = load_sample_scope_data(&mut context.svm, &context.default_payer).unwrap(); + + // Fund swig wallet + context + .svm + .airdrop(&swig_wallet_address, 20_000_000_000) + .unwrap(); + + // Test SOL transfer within limit (0.1 SOL ≈ 15 USD at mock price) + let transfer_ix = system_instruction::transfer( + &swig_wallet_address, + &secondary_authority.pubkey(), + 100_000_000, // 0.1 SOL + ); + + let mut sign_ix = swig_interface::SignV2Instruction::new_ed25519( + swig_key, + swig_wallet_address, + secondary_authority.pubkey(), + transfer_ix, + 1, + ) + .unwrap(); + + let message = v0::Message::try_compile( + &secondary_authority.pubkey(), + &[sign_ix], + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let tx = VersionedTransaction::try_new(VersionedMessage::V0(message), &[&secondary_authority]) + .unwrap(); + + // This should succeed as it's within the oracle recurring limit + let result = context.svm.send_transaction(tx); + println!("result: {:?}", result); + assert!( + result.is_ok(), + "SOL transfer should succeed within oracle recurring limit" + ); + + // Test SOL transfer that exceeds the limit (1 SOL ≈ 150 USD at mock price) + let transfer_ix2 = system_instruction::transfer( + &swig_wallet_address, + &secondary_authority.pubkey(), + 1_000_000_000, // 1 SOL + ); + + let mut sign_ix2 = swig_interface::SignV2Instruction::new_ed25519( + swig_key, + swig_wallet_address, + secondary_authority.pubkey(), + transfer_ix2, + 1, + ) + .unwrap(); + + sign_ix2.accounts.extend(vec![ + AccountMeta::new_readonly( + Pubkey::from_str("FbeuRDWwLvZWEU3HNtaLoYKagw9rH1NvmjpRMpjMwhDw").unwrap(), + false, + ), + AccountMeta::new_readonly( + Pubkey::from_str("3NJYftD5sjVfxSnUdZ1wVML8f3aC6mp1CXCL6L7TnU8C").unwrap(), + false, + ), + ]); + + let message2 = v0::Message::try_compile( + &secondary_authority.pubkey(), + &[sign_ix2], + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let tx2 = + VersionedTransaction::try_new(VersionedMessage::V0(message2), &[&secondary_authority]) + .unwrap(); + + // This should fail as it exceeds the oracle recurring limit + let result2 = context.svm.send_transaction(tx2); + assert!( + result2.is_err(), + "SOL transfer should fail when exceeding oracle recurring limit" + ); +} + +/// Test 3: Verify oracle recurring limit for token transfers +#[test_log::test] +fn test_oracle_recurring_limit_token_transfer() { + let mut context = setup_test_context().unwrap(); + let swig_authority = Keypair::new(); + context + .svm + .airdrop(&swig_authority.pubkey(), 10_000_000_000) + .unwrap(); + + // Create a swig wallet + let id = rand::random::<[u8; 32]>(); + let (swig_key, _) = create_swig_ed25519(&mut context, &swig_authority, id).unwrap(); + let (swig_wallet_address, _) = + Pubkey::find_program_address(&swig_wallet_address_seeds(swig_key.as_ref()), &program_id()); + + // Create secondary authority + let secondary_authority = Keypair::new(); + context + .svm + .airdrop(&secondary_authority.pubkey(), 10_000_000_000) + .unwrap(); + + // Add oracle recurring limit permission (50 USD per window) + let oracle_recurring_limit = OracleRecurringLimit::new( + BaseAsset::USD, + 50_000_000, // 50 USD + 100, // 100 slots window + false, + ); + + // Add authority with oracle recurring limit permission + add_authority_with_ed25519_root( + &mut context, + &swig_key, + &swig_authority, + AuthorityConfig { + authority_type: AuthorityType::Ed25519, + authority: secondary_authority.pubkey().as_ref(), + }, + vec![ + ClientAction::OracleRecurringLimit(oracle_recurring_limit), + ClientAction::ProgramAll(ProgramAll {}), + ], + ) + .unwrap(); + + // Load oracle accounts + let mint = load_sample_scope_data(&mut context.svm, &context.default_payer).unwrap(); + + // Setup token mint and accounts + let swig_ata = setup_ata( + &mut context.svm, + &mint, + &swig_wallet_address, + &context.default_payer, + ) + .unwrap(); + let recipient_ata = setup_ata( + &mut context.svm, + &mint, + &secondary_authority.pubkey(), + &context.default_payer, + ) + .unwrap(); + + // Mint tokens to swig account + mint_to( + &mut context.svm, + &mint, + &context.default_payer, + &swig_ata, + 1_000_000_000, // 1000 tokens + ) + .unwrap(); + + // Test token transfer within limit (100 tokens ≈ 150 USD at mock price of 1.5 USD per token) + let transfer_ix = spl_token::instruction::transfer( + &spl_token::id(), + &swig_ata, + &recipient_ata, + &swig_wallet_address, + &[], + 100_000_000, // 100 tokens + ) + .unwrap(); + + let mut sign_ix = swig_interface::SignV2Instruction::new_ed25519( + swig_key, + swig_wallet_address, + secondary_authority.pubkey(), + transfer_ix, + 1, + ) + .unwrap(); + + let message = v0::Message::try_compile( + &secondary_authority.pubkey(), + &[sign_ix], + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let tx = VersionedTransaction::try_new(VersionedMessage::V0(message), &[&secondary_authority]) + .unwrap(); + + // This should succeed as it's within the oracle recurring limit + let result = context.svm.send_transaction(tx); + println!("result: {:?}", result); + assert!( + result.is_ok(), + "Token transfer should succeed within oracle recurring limit" + ); + + // Move slot forward + + // Test token transfer that exceeds the limit (1000 tokens ≈ 1500 USD at mock price) + let transfer_ix2 = spl_token::instruction::transfer( + &spl_token::id(), + &swig_ata, + &recipient_ata, + &swig_wallet_address, + &[], + 1_000_000_000, // 1000 tokens + ) + .unwrap(); + + let mut sign_ix2 = swig_interface::SignV2Instruction::new_ed25519( + swig_key, + swig_wallet_address, + secondary_authority.pubkey(), + transfer_ix2, + 1, + ) + .unwrap(); + + sign_ix2.accounts.extend(vec![ + AccountMeta::new_readonly( + Pubkey::from_str("FbeuRDWwLvZWEU3HNtaLoYKagw9rH1NvmjpRMpjMwhDw").unwrap(), + false, + ), + AccountMeta::new_readonly( + Pubkey::from_str("3NJYftD5sjVfxSnUdZ1wVML8f3aC6mp1CXCL6L7TnU8C").unwrap(), + false, + ), + ]); + + let message2 = v0::Message::try_compile( + &secondary_authority.pubkey(), + &[sign_ix2], + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let tx2 = + VersionedTransaction::try_new(VersionedMessage::V0(message2), &[&secondary_authority]) + .unwrap(); + + // This should fail as it exceeds the oracle recurring limit + let result2 = context.svm.send_transaction(tx2); + assert!( + result2.is_err(), + "Token transfer should fail when exceeding oracle recurring limit" + ); +} + +/// Test 4: Verify oracle recurring limit window reset functionality +#[test_log::test] +fn test_oracle_recurring_limit_window_reset() { + let mut context = setup_test_context().unwrap(); + let swig_authority = Keypair::new(); + context + .svm + .airdrop(&swig_authority.pubkey(), 10_000_000_000) + .unwrap(); + + // Create a swig wallet + let id = rand::random::<[u8; 32]>(); + let (swig_key, _) = create_swig_ed25519(&mut context, &swig_authority, id).unwrap(); + let (swig_wallet_address, _) = + Pubkey::find_program_address(&swig_wallet_address_seeds(swig_key.as_ref()), &program_id()); + + // Create secondary authority + let secondary_authority = Keypair::new(); + context + .svm + .airdrop(&secondary_authority.pubkey(), 10_000_000_000) + .unwrap(); + + // Add oracle recurring limit permission (100 USD per window, 100 slots window) + let oracle_recurring_limit = OracleRecurringLimit::new( + BaseAsset::USD, + 180_000_000, // 200 USD + 3, // 3 slots window (smaller to avoid stale price) + false, + ); + + // Add authority with oracle recurring limit permission + add_authority_with_ed25519_root( + &mut context, + &swig_key, + &swig_authority, + AuthorityConfig { + authority_type: AuthorityType::Ed25519, + authority: secondary_authority.pubkey().as_ref(), + }, + vec![ + ClientAction::OracleRecurringLimit(oracle_recurring_limit), + ClientAction::ProgramAll(ProgramAll {}), + ], + ) + .unwrap(); + + // Load oracle accounts + let mint = load_sample_scope_data(&mut context.svm, &context.default_payer).unwrap(); + + // Fund swig wallet + context + .svm + .airdrop(&swig_wallet_address, 20_000_000_000) + .unwrap(); + + // Use up most of the limit + let transfer_ix = system_instruction::transfer( + &swig_wallet_address, + &secondary_authority.pubkey(), + 500_000_000, // 0.5 SOL (should use up most of 100 USD limit) + ); + + let mut sign_ix = swig_interface::SignV2Instruction::new_ed25519( + swig_key, + swig_wallet_address, + secondary_authority.pubkey(), + transfer_ix, + 1, + ) + .unwrap(); + + let message = v0::Message::try_compile( + &secondary_authority.pubkey(), + &[sign_ix], + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let tx = VersionedTransaction::try_new(VersionedMessage::V0(message), &[&secondary_authority]) + .unwrap(); + + display_swig( + swig_key, + context.svm.get_account(&swig_key).unwrap().data.as_ref(), + context.svm.get_account(&swig_key).unwrap().lamports, + ); + let result = context.svm.send_transaction(tx); + println!("result: {:?}", result); + assert!(result.is_ok(), "First SOL transfer should succeed"); + + // Try to transfer more than remaining limit (should fail) + let transfer_ix2 = system_instruction::transfer( + &swig_wallet_address, + &secondary_authority.pubkey(), + 500_000_001, // 0.5 SOL (should exceed remaining limit) + ); + + let mut sign_ix2 = swig_interface::SignV2Instruction::new_ed25519( + swig_key, + swig_wallet_address, + secondary_authority.pubkey(), + transfer_ix2, + 1, + ) + .unwrap(); + + sign_ix2.accounts.extend(vec![ + AccountMeta::new_readonly( + Pubkey::from_str("FbeuRDWwLvZWEU3HNtaLoYKagw9rH1NvmjpRMpjMwhDw").unwrap(), + false, + ), + AccountMeta::new_readonly( + Pubkey::from_str("3NJYftD5sjVfxSnUdZ1wVML8f3aC6mp1CXCL6L7TnU8C").unwrap(), + false, + ), + ]); + + let message2 = v0::Message::try_compile( + &secondary_authority.pubkey(), + &[sign_ix2], + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let tx2 = + VersionedTransaction::try_new(VersionedMessage::V0(message2), &[&secondary_authority]) + .unwrap(); + + let result2 = context.svm.send_transaction(tx2); + println!("result2: {:?}", result2); + assert!(result2.is_err(), "Second transfer should fail due to limit"); + + // Try the same transfer again (should succeed after window reset) + let transfer_ix3 = system_instruction::transfer( + &swig_wallet_address, + &secondary_authority.pubkey(), + 51_000_000, // 0.05 SOL (should succeed after window reset) + ); + + let mut sign_ix3 = swig_interface::SignV2Instruction::new_ed25519( + swig_key, + swig_wallet_address, + secondary_authority.pubkey(), + transfer_ix3, + 1, + ) + .unwrap(); + + sign_ix3.accounts.extend(vec![ + AccountMeta::new_readonly( + Pubkey::from_str("FbeuRDWwLvZWEU3HNtaLoYKagw9rH1NvmjpRMpjMwhDw").unwrap(), + false, + ), + AccountMeta::new_readonly( + Pubkey::from_str("3NJYftD5sjVfxSnUdZ1wVML8f3aC6mp1CXCL6L7TnU8C").unwrap(), + false, + ), + ]); + + println!("slot: {:?}", advance_slot(&mut context, 5)); + + let message3 = v0::Message::try_compile( + &secondary_authority.pubkey(), + &[sign_ix3], + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let tx3 = + VersionedTransaction::try_new(VersionedMessage::V0(message3), &[&secondary_authority]) + .unwrap(); + + let result3 = context.svm.send_transaction(tx3); + println!("result3: {:?}", result3); + assert!( + result3.is_ok(), + "Transfer should succeed after window reset" + ); +} + +/// Test 5: Verify oracle recurring limit with passthrough check +#[test_log::test] +fn test_oracle_recurring_limit_passthrough() { + let mut context = setup_test_context().unwrap(); + let swig_authority = Keypair::new(); + context + .svm + .airdrop(&swig_authority.pubkey(), 10_000_000_000) + .unwrap(); + + // Create a swig wallet + let id = rand::random::<[u8; 32]>(); + let (swig_key, _) = create_swig_ed25519(&mut context, &swig_authority, id).unwrap(); + + let (swig_wallet_address, _) = + Pubkey::find_program_address(&swig_wallet_address_seeds(swig_key.as_ref()), &program_id()); + + // Create secondary authority + let secondary_authority = Keypair::new(); + context + .svm + .airdrop(&secondary_authority.pubkey(), 10_000_000_000) + .unwrap(); + + // Add oracle recurring limit permission with passthrough enabled + let oracle_recurring_limit = OracleRecurringLimit::new( + BaseAsset::USD, + 100_000_000, // 100 USD + 1000, // 1000 slots window + true, // passthrough enabled + ); + + // Add SOL limit as well + let sol_limit = SolLimit { + amount: 500_000_000, // 0.5 SOL + }; + + // Add authority with both permissions + add_authority_with_ed25519_root( + &mut context, + &swig_key, + &swig_authority, + AuthorityConfig { + authority_type: AuthorityType::Ed25519, + authority: secondary_authority.pubkey().as_ref(), + }, + vec![ + ClientAction::OracleRecurringLimit(oracle_recurring_limit), + ClientAction::SolLimit(sol_limit), + ClientAction::ProgramAll(ProgramAll {}), + ], + ) + .unwrap(); + + // Load oracle accounts + let mint = load_sample_scope_data(&mut context.svm, &context.default_payer).unwrap(); + + // Fund swig wallet + context + .svm + .airdrop(&swig_wallet_address, 20_000_000_000) + .unwrap(); + + // Test transfer that passes oracle recurring limit but should be caught by SOL limit + let transfer_ix = system_instruction::transfer( + &swig_wallet_address, + &secondary_authority.pubkey(), + 600_000_000, // 0.6 SOL (within oracle limit but exceeds SOL limit) + ); + + let mut sign_ix = swig_interface::SignV2Instruction::new_ed25519( + swig_key, + swig_wallet_address, + secondary_authority.pubkey(), + transfer_ix, + 1, + ) + .unwrap(); + + let message = v0::Message::try_compile( + &secondary_authority.pubkey(), + &[sign_ix], + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let tx = VersionedTransaction::try_new(VersionedMessage::V0(message), &[&secondary_authority]) + .unwrap(); + + // This should fail due to SOL limit even though it passes oracle recurring limit + let result = context.svm.send_transaction(tx); + assert!( + result.is_err(), + "Transfer should fail due to SOL limit even with passthrough" + ); + assert_eq!( + result.unwrap_err().err, + solana_sdk::transaction::TransactionError::InstructionError( + 0, + solana_sdk::instruction::InstructionError::Custom(3033) + ), + ); +} diff --git a/program/tests/program_scope_test.rs b/program/tests/program_scope_test.rs index 3e47eac2..3422ba51 100644 --- a/program/tests/program_scope_test.rs +++ b/program/tests/program_scope_test.rs @@ -236,7 +236,7 @@ fn test_token_transfer_with_program_scope() { "Account difference (swig - regular): {} accounts", account_difference ); - assert!(swig_transfer_cu - regular_transfer_cu <= 5800); + assert!(swig_transfer_cu - regular_transfer_cu <= 5823); } /// Helper function to perform token transfers through the swig diff --git a/program/tests/sign_performance_test.rs b/program/tests/sign_performance_test.rs index fd7326c1..c02a78c2 100644 --- a/program/tests/sign_performance_test.rs +++ b/program/tests/sign_performance_test.rs @@ -191,7 +191,7 @@ fn test_token_transfer_performance_comparison() { ); // 3744 is the max difference in CU between the two transactions lets lower // this as far as possible but never increase it - assert!(swig_transfer_cu - regular_transfer_cu <= 3851); + assert!(swig_transfer_cu - regular_transfer_cu <= 3876); } #[test_log::test] @@ -305,5 +305,5 @@ fn test_sol_transfer_performance_comparison() { // Set a reasonable limit for the CU difference to avoid regressions // Similar to the token transfer test assertion - assert!(swig_transfer_cu - regular_transfer_cu <= 2196); + assert!(swig_transfer_cu - regular_transfer_cu <= 2215); } diff --git a/program/tests/sign_performance_v2_test.rs b/program/tests/sign_performance_v2_test.rs index 887d066c..97765649 100644 --- a/program/tests/sign_performance_v2_test.rs +++ b/program/tests/sign_performance_v2_test.rs @@ -190,7 +190,7 @@ fn test_token_transfer_performance_comparison_v2() { "Account difference (swig - regular): {} accounts", account_difference ); - assert!(swig_transfer_cu - regular_transfer_cu <= 3798); + assert!(swig_transfer_cu - regular_transfer_cu <= 3800); } #[test_log::test] @@ -310,5 +310,5 @@ fn test_sol_transfer_performance_comparison_v2() { account_difference ); - assert!(swig_transfer_cu - regular_transfer_cu <= 3253); + assert!(swig_transfer_cu - regular_transfer_cu <= 3256); } diff --git a/program/tests/sol_destination_limit.rs b/program/tests/sol_destination_limit.rs index 5cdca096..93fdb312 100644 --- a/program/tests/sol_destination_limit.rs +++ b/program/tests/sol_destination_limit.rs @@ -398,7 +398,7 @@ fn test_multiple_destination_limits() { context.svm.warp_to_slot(100); // Test transfer to recipient1 within limit - let transfer_amount1 = 200_000_000u64; // 0.2 SOL - within recipient1's limit + let transfer_amount1 = 250_000_000u64; // 0.2 SOL - within recipient1's limit let inner_ix1 = system_instruction::transfer(&swig, &recipient1.pubkey(), transfer_amount1); let sol_transfer_ix1 = SignInstruction::new_ed25519( diff --git a/program/tests/sol_destination_limit_v2.rs b/program/tests/sol_destination_limit_v2.rs index 79760208..4b95c346 100644 --- a/program/tests/sol_destination_limit_v2.rs +++ b/program/tests/sol_destination_limit_v2.rs @@ -420,7 +420,7 @@ fn test_multiple_destination_limits_v2() { context.svm.warp_to_slot(100); // Test transfer to recipient1 within limit - let transfer_amount1 = 200_000_000u64; // 0.2 SOL - within recipient1's limit + let transfer_amount1 = 250_000_000u64; // 0.2 SOL - within recipient1's limit let inner_ix1 = system_instruction::transfer(&swig_wallet_address, &recipient1.pubkey(), transfer_amount1); diff --git a/program/tests/sol_recurring_destination_limit.rs b/program/tests/sol_recurring_destination_limit.rs index 5781ffd1..9c744509 100644 --- a/program/tests/sol_recurring_destination_limit.rs +++ b/program/tests/sol_recurring_destination_limit.rs @@ -442,7 +442,7 @@ fn test_multiple_sol_recurring_destination_limits() { context.svm.warp_to_slot(100); // Test transfer to recipient1 within limit - let transfer_amount1 = 200_000_000u64; // 0.2 SOL - within recipient1's limit + let transfer_amount1 = 250_000_000u64; // 0.2 SOL - within recipient1's limit let inner_ix1 = system_instruction::transfer(&swig, &recipient1.pubkey(), transfer_amount1); let sol_transfer_ix1 = SignInstruction::new_ed25519( diff --git a/program/tests/sol_recurring_destination_limit_v2.rs b/program/tests/sol_recurring_destination_limit_v2.rs index 56ef4cdd..80115ef4 100644 --- a/program/tests/sol_recurring_destination_limit_v2.rs +++ b/program/tests/sol_recurring_destination_limit_v2.rs @@ -468,7 +468,7 @@ fn test_multiple_sol_recurring_destination_limits_v2() { context.svm.warp_to_slot(100); // Test transfer to recipient1 within limit - let transfer_amount1 = 200_000_000u64; // 0.2 SOL - within recipient1's limit + let transfer_amount1 = 250_000_000u64; // 0.2 SOL - within recipient1's limit let inner_ix1 = system_instruction::transfer(&swig_wallet_address, &recipient1.pubkey(), transfer_amount1); diff --git a/rust-sdk/Cargo.toml b/rust-sdk/Cargo.toml index 1b112a0f..b18da665 100644 --- a/rust-sdk/Cargo.toml +++ b/rust-sdk/Cargo.toml @@ -9,6 +9,7 @@ documentation.workspace = true [dependencies] swig-interface = { path = "../interface" } swig-state = { path = "../state" } +oracle-mapping-state = { git = "https://github.com/anagrambuild/scope-oracle-mapper" } solana-program = "2.0" solana-sdk = "2.0" diff --git a/rust-sdk/src/tests/wallet/helper_tests.rs b/rust-sdk/src/tests/wallet/helper_tests.rs index 0c37c633..141b8e79 100644 --- a/rust-sdk/src/tests/wallet/helper_tests.rs +++ b/rust-sdk/src/tests/wallet/helper_tests.rs @@ -124,7 +124,7 @@ fn should_get_sub_account() { Some(&sub_account_authority), ) .unwrap(); - + swig_wallet.switch_payer(&main_authority).unwrap(); // Create a sub account swig_wallet.create_sub_account().unwrap(); diff --git a/rust-sdk/src/tests/wallet/mod.rs b/rust-sdk/src/tests/wallet/mod.rs index 822e129f..b09f6a87 100644 --- a/rust-sdk/src/tests/wallet/mod.rs +++ b/rust-sdk/src/tests/wallet/mod.rs @@ -2,6 +2,7 @@ pub mod authority_tests; pub mod creation_tests; pub mod destination_tests; pub mod helper_tests; +pub mod oracle_tests; pub mod program_all_tests; pub mod program_scope_test; pub mod secp256r1_test; @@ -107,3 +108,84 @@ fn convert_wallet_to_v1(wallet: &mut SwigWallet) { .set_account(swig_key, account) .expect("Failed to update account"); } + +use crate::tests::common::{mint_to, setup_ata, setup_mint}; +use oracle_mapping_state::{DataLen, MintMapping, ScopeMappingRegistry}; +use solana_client::rpc_client::RpcClient; +use solana_sdk::account::Account; +use std::str::FromStr; + +pub fn load_sample_scope_data(svm: &mut LiteSVM, payer: &Keypair) -> anyhow::Result<(Pubkey)> { + let pubkey = Pubkey::from_str("3NJYftD5sjVfxSnUdZ1wVML8f3aC6mp1CXCL6L7TnU8C").unwrap(); + let owner = Pubkey::from_str("HFn8GnPADiny6XqUoWE8uRPPxb29ikn4yTuPa9MF2fWJ").unwrap(); + + let client = RpcClient::new("https://api.mainnet-beta.solana.com".to_string()); + let mut scope_account = client.get_account(&pubkey).unwrap(); + + let mut data = Account { + lamports: 200_700_000, + data: scope_account.data, + owner, + executable: false, + rent_epoch: 18446744073709551615, + }; + + svm.set_account(pubkey, data).unwrap(); + + let mapping_pubkey = Pubkey::from_str("FbeuRDWwLvZWEU3HNtaLoYKagw9rH1NvmjpRMpjMwhDw").unwrap(); + let owner_pubkey = Pubkey::from_str("9WM51wrB9xpRzFgYJHocYNnx4DF6G6ee2eB44ZGoZ8vg").unwrap(); + + let mint = setup_mint(svm, &payer).unwrap(); + + let devnet_client = RpcClient::new("https://api.devnet.solana.com".to_string()); + let scope_mapping_registry_acc = devnet_client.get_account(&mapping_pubkey).unwrap(); + + let mut scope_mapping_data = scope_mapping_registry_acc.data.clone(); + let mut scope_mapping_registry = ScopeMappingRegistry::from_bytes( + scope_mapping_data[..ScopeMappingRegistry::LEN] + .try_into() + .unwrap(), + ) + .unwrap(); + + // Create new mint mapping + let new_mint_mapping = MintMapping::new( + mint.to_bytes(), + Some([0, u16::MAX, u16::MAX]), + None, + None, + 9, + ); + + let mapping_mint_data = new_mint_mapping.to_bytes(); + let mapping = &mapping_mint_data[..new_mint_mapping.serialized_size() as usize]; + + let insertion_offset = + ScopeMappingRegistry::LEN + scope_mapping_registry.last_mapping_offset as usize; + + scope_mapping_data.resize(insertion_offset + mapping.len(), 0); + + scope_mapping_data[insertion_offset..insertion_offset + mapping.len()].copy_from_slice(mapping); + + scope_mapping_registry.total_mappings += 1; + scope_mapping_registry.last_mapping_offset += mapping.len() as u16; + + scope_mapping_data[..ScopeMappingRegistry::LEN] + .copy_from_slice(&scope_mapping_registry.to_bytes()); + + let data = Account { + lamports: scope_mapping_registry_acc.lamports + 10000000, + data: scope_mapping_data, + owner: owner_pubkey, + executable: false, + rent_epoch: 18446744073709551615, + }; + + svm.set_account(mapping_pubkey, data).unwrap(); + + // sync litesvm slot to mainnet slot + let slot = client.get_slot().unwrap(); + svm.warp_to_slot(slot); + + Ok(mint) +} diff --git a/rust-sdk/src/tests/wallet/oracle_tests.rs b/rust-sdk/src/tests/wallet/oracle_tests.rs new file mode 100644 index 00000000..cde074d4 --- /dev/null +++ b/rust-sdk/src/tests/wallet/oracle_tests.rs @@ -0,0 +1,232 @@ +use solana_sdk::{signature::Keypair, signer::Signer}; + +use super::*; +use crate::types::Permission::{self, ProgramAll}; +use solana_program::{pubkey::Pubkey, system_instruction, system_program}; + +#[test_log::test] +fn should_add_oracle_limit_permission() { + let (litesvm, main_authority) = setup_test_environment(); + let mut wallet = create_test_wallet(litesvm, &main_authority); + + // Add secondary authority with OracleLimit (USD, 250_000_000 = 250 USD, passthrough=false) + let secondary = Keypair::new(); + wallet + .add_authority( + AuthorityType::Ed25519, + &secondary.pubkey().to_bytes(), + vec![Permission::OracleLimit { + base_asset_type: 0, // USD + value_limit: 250_000_000, + passthrough_check: false, + recurring: None, + }], + ) + .unwrap(); + + // Verify authority added and oracle permission present + assert_eq!(wallet.get_role_count().unwrap(), 2); + let role_id = wallet.get_role_id(&secondary.pubkey().to_bytes()).unwrap(); + let perms = wallet.get_role_permissions(role_id).unwrap(); + assert!(perms.iter().any(|p| matches!( + p, + Permission::OracleLimit { base_asset_type, value_limit, passthrough_check, recurring } + if *base_asset_type == 0 && *value_limit == 250_000_000 && !*passthrough_check + ))); +} + +#[test_log::test] +fn should_add_oracle_recurring_limit_permission() { + let (litesvm, main_authority) = setup_test_environment(); + let mut wallet = create_test_wallet(litesvm, &main_authority); + + // Add secondary authority with OracleTokenLimit (EUR, 300_000_000 per window, window=1000) + let secondary = Keypair::new(); + wallet + .add_authority( + AuthorityType::Ed25519, + &secondary.pubkey().to_bytes(), + vec![Permission::OracleLimit { + base_asset_type: 1, // EUR + value_limit: 300_000_000, + passthrough_check: true, + recurring: Some(RecurringConfig::new(1000)), + }], + ) + .unwrap(); + + // Verify authority added and oracle recurring permission present + assert_eq!(wallet.get_role_count().unwrap(), 2); + let role_id = wallet.get_role_id(&secondary.pubkey().to_bytes()).unwrap(); + let perms = wallet.get_role_permissions(role_id).unwrap(); + println!("perms: {:?}", perms); + assert!(perms.iter().any(|p| matches!( + p, + Permission::OracleLimit { base_asset_type, value_limit, passthrough_check, recurring } + if *base_asset_type == 1 && *value_limit == 300_000_000 && *passthrough_check && recurring.is_some() + ))); +} + +#[test_log::test] +fn should_send_with_oracle_limit_permission() { + let (mut litesvm, main_authority) = setup_test_environment(); + load_sample_scope_data(&mut litesvm, &main_authority).unwrap(); + + let mut swig_wallet = create_test_wallet_v2(litesvm, &main_authority); + + // Fund the swig wallet PDA + let swig_wallet_address = swig_wallet.get_swig_wallet_address().unwrap(); + swig_wallet + .litesvm() + .airdrop(&swig_wallet_address, 5_000_000_000) + .unwrap(); + + // Prepare a transfer from wallet PDA to recipient + let secondary_authority = Keypair::new(); + swig_wallet + .litesvm() + .airdrop(&secondary_authority.pubkey(), 10_000_000_000) + .unwrap(); + + swig_wallet + .add_authority( + AuthorityType::Ed25519, + &secondary_authority.pubkey().to_bytes(), + vec![ + Permission::OracleLimit { + base_asset_type: 0, + value_limit: 250_000_000, + passthrough_check: false, + recurring: None, + }, + Permission::ProgramAll, + ], + ) + .unwrap(); + + swig_wallet + .switch_authority( + 1, + Box::new(Ed25519ClientRole::new(secondary_authority.pubkey())), + Some(&secondary_authority), + ) + .unwrap(); + + let transfer_ix = system_instruction::transfer( + &swig_wallet_address, + &secondary_authority.pubkey(), + 1_000_000_000, + ); + + swig_wallet.display_swig().unwrap(); + + let sig = swig_wallet.sign_v2(vec![transfer_ix], None).unwrap(); + + swig_wallet.display_swig().unwrap(); + + // test if the oracle limit fails if exceeded + let transfer_ix = system_instruction::transfer( + &swig_wallet_address, + &secondary_authority.pubkey(), + 1_000_000_000, + ); + + let sig = swig_wallet.sign_v2(vec![transfer_ix], None); + assert!(sig.is_err()); + + swig_wallet.display_swig().unwrap(); +} + +#[test_log::test] +fn should_send_with_oracle_recurring_limit_permission() { + let (mut litesvm, main_authority) = setup_test_environment(); + load_sample_scope_data(&mut litesvm, &main_authority).unwrap(); + + let mut swig_wallet = create_test_wallet_v2(litesvm, &main_authority); + + // Fund the swig wallet PDA + let swig_wallet_address = swig_wallet.get_swig_wallet_address().unwrap(); + swig_wallet + .litesvm() + .airdrop(&swig_wallet_address, 5_000_000_000) + .unwrap(); + + // Prepare a transfer from wallet PDA to recipient + let secondary_authority = Keypair::new(); + swig_wallet + .litesvm() + .airdrop(&secondary_authority.pubkey(), 10_000_000_000) + .unwrap(); + + swig_wallet + .add_authority( + AuthorityType::Ed25519, + &secondary_authority.pubkey().to_bytes(), + vec![ + Permission::OracleLimit { + base_asset_type: 0, + value_limit: 250_000_000, + recurring: Some(RecurringConfig::new(5)), + passthrough_check: false, + }, + Permission::ProgramAll, + ], + ) + .unwrap(); + + swig_wallet + .switch_authority( + 1, + Box::new(Ed25519ClientRole::new(secondary_authority.pubkey())), + Some(&secondary_authority), + ) + .unwrap(); + + let first_passing_transfer_ix = system_instruction::transfer( + &swig_wallet_address, + &secondary_authority.pubkey(), + 1_000_000_000, + ); + + swig_wallet.display_swig().unwrap(); + + let sig = swig_wallet + .sign_v2(vec![first_passing_transfer_ix], None) + .unwrap(); + + swig_wallet.display_swig().unwrap(); + + // test if the oracle limit fails if exceeded + let second_failing_transfer_ix = system_instruction::transfer( + &swig_wallet_address, + &secondary_authority.pubkey(), + 1_000_000_000, + ); + + let sig = swig_wallet.sign_v2(vec![second_failing_transfer_ix.clone()], None); + assert!(sig.is_err()); + + advance_slot(&mut swig_wallet.litesvm(), 10); + + let third_passing_transfer_ix = system_instruction::transfer( + &swig_wallet_address, + &secondary_authority.pubkey(), + 500_000_001, + ); + + let sig = swig_wallet + .sign_v2(vec![third_passing_transfer_ix], None) + .unwrap(); + + swig_wallet.display_swig().unwrap(); +} + +pub fn advance_slot(svm: &mut LiteSVM, slots: u64) -> u64 { + use solana_client::rpc_client::RpcClient; + + let client = RpcClient::new("https://api.mainnet-beta.solana.com".to_string()); + let slot = client.get_slot().unwrap(); + let new_slot = slot + slots; + svm.warp_to_slot(new_slot); + new_slot +} diff --git a/rust-sdk/src/tests/wallet/program_scope_test.rs b/rust-sdk/src/tests/wallet/program_scope_test.rs index 05d113ab..daf09792 100644 --- a/rust-sdk/src/tests/wallet/program_scope_test.rs +++ b/rust-sdk/src/tests/wallet/program_scope_test.rs @@ -73,6 +73,7 @@ fn should_token_transfer_with_program_scope() { Some(&new_authority), ) .unwrap(); + swig_wallet.switch_payer(&main_authority).unwrap(); // Mint initial tokens to swig wallet let initial_token_amount = 2000; @@ -168,7 +169,7 @@ fn should_token_transfer_with_recurring_limit_program_scope() { Some(&new_authority), ) .unwrap(); - + swig_wallet.switch_payer(&main_authority).unwrap(); // Mint initial tokens to swig wallet let initial_token_amount = 2000; mint_to( diff --git a/rust-sdk/src/tests/wallet/sign_v2_tests.rs b/rust-sdk/src/tests/wallet/sign_v2_tests.rs index f899714b..18982abb 100644 --- a/rust-sdk/src/tests/wallet/sign_v2_tests.rs +++ b/rust-sdk/src/tests/wallet/sign_v2_tests.rs @@ -319,6 +319,10 @@ fn should_sign_v2_token_recurring_limit_enforced() { Some(&second_authority), ) .unwrap(); + swig_wallet + .litesvm() + .airdrop(&second_authority.pubkey(), 1_000_000_000) + .unwrap(); // First transfer within limit let amount1 = 300u64; diff --git a/rust-sdk/src/tests/wallet/sub_accounts_test.rs b/rust-sdk/src/tests/wallet/sub_accounts_test.rs index 586c8443..5ecc0788 100644 --- a/rust-sdk/src/tests/wallet/sub_accounts_test.rs +++ b/rust-sdk/src/tests/wallet/sub_accounts_test.rs @@ -39,6 +39,7 @@ fn test_sub_account_creation_and_setup() { Some(&secondary_authority), ) .unwrap(); + swig_wallet.switch_payer(&main_authority).unwrap(); let signature = swig_wallet.create_sub_account().unwrap(); println!("Sub-account created with signature: {:?}", signature); @@ -78,6 +79,7 @@ fn test_sub_account_sol_operations() { Some(&secondary_authority), ) .unwrap(); + swig_wallet.switch_payer(&main_authority).unwrap(); // Create sub-account let signature = swig_wallet.create_sub_account().unwrap(); @@ -196,6 +198,7 @@ fn test_sub_account_token_operations() { Some(&secondary_authority), ) .unwrap(); + swig_wallet.switch_payer(&main_authority).unwrap(); let signature = swig_wallet.create_sub_account().unwrap(); let role_id_bytes = swig_wallet.get_current_role_id().unwrap().to_le_bytes(); @@ -280,6 +283,7 @@ fn test_sub_account_toggle_operations() { Some(&secondary_authority), ) .unwrap(); + swig_wallet.switch_payer(&main_authority).unwrap(); let signature = swig_wallet.create_sub_account().unwrap(); let role_id_bytes = swig_wallet.get_current_role_id().unwrap().to_le_bytes(); @@ -334,6 +338,7 @@ fn test_secondary_authority_operations() { Some(&secondary_authority), ) .unwrap(); + swig_wallet.switch_payer(&main_authority).unwrap(); let signature = swig_wallet.create_sub_account().unwrap(); let role_id_bytes = swig_wallet.get_current_role_id().unwrap().to_le_bytes(); diff --git a/rust-sdk/src/types.rs b/rust-sdk/src/types.rs index e3e080c8..03eaa346 100644 --- a/rust-sdk/src/types.rs +++ b/rust-sdk/src/types.rs @@ -5,6 +5,8 @@ use swig_state::{ all::All, all_but_manage_authority::AllButManageAuthority, manage_authority::ManageAuthority, + oracle_limits::OracleTokenLimit as OracleTokenLimitAction, + oracle_recurring_limit::OracleRecurringLimit as OracleRecurringLimitAction, program::Program, program_all::ProgramAll, program_curated::ProgramCurated, @@ -164,6 +166,20 @@ pub enum Permission { /// This grants access to all wallet operations but excludes the ability /// to add, remove, or modify authorities/subaccounts. AllButManageAuthority, + + /// Permission to enforce value-based limits using oracle pricing + OracleLimit { + /// Base asset denomination for the limit (e.g., 0=USD, 1=EUR) + base_asset_type: u8, + /// Remaining value limit in base asset units (e.g., 6 decimals for USD) + value_limit: u64, + /// If true, continue checking other actions even if oracle check passes + passthrough_check: bool, + /// Optional recurring configuration. If provided, the amount becomes a + /// recurring limit that resets after the specified window + /// period. If None, amount is treated as a fixed limit. + recurring: Option, + }, } impl Permission { @@ -333,6 +349,38 @@ impl Permission { AllButManageAuthority {}, )); }, + Permission::OracleLimit { + base_asset_type, + value_limit, + passthrough_check, + recurring, + } => match recurring { + Some(config) => { + actions.push(ClientAction::OracleRecurringLimit( + OracleRecurringLimitAction::new( + match base_asset_type { + 0 => swig_state::action::oracle_limits::BaseAsset::USD, + 1 => swig_state::action::oracle_limits::BaseAsset::EUR, + _ => swig_state::action::oracle_limits::BaseAsset::USD, + }, + value_limit, + config.window, + passthrough_check, + ), + )); + }, + None => { + actions.push(ClientAction::OracleTokenLimit(OracleTokenLimitAction::new( + match base_asset_type { + 0 => swig_state::action::oracle_limits::BaseAsset::USD, + 1 => swig_state::action::oracle_limits::BaseAsset::EUR, + _ => swig_state::action::oracle_limits::BaseAsset::USD, + }, + value_limit, + passthrough_check, + ))); + }, + }, } } actions @@ -573,6 +621,36 @@ impl Permission { permissions.push(Permission::AllButManageAuthority); } + // Check for OracleTokenLimit permissions + let oracle_limits = + swig_state::role::Role::get_all_actions_of_type::(role) + .map_err(|_| SwigError::InvalidSwigData)?; + for action in oracle_limits { + permissions.push(Permission::OracleLimit { + base_asset_type: action.base_asset_type, + value_limit: action.value_limit, + passthrough_check: action.passthrough_check, + recurring: None, + }); + } + + // Check for OracleRecurringLimit permissions + let oracle_recurring_limits = + swig_state::role::Role::get_all_actions_of_type::(role) + .map_err(|_| SwigError::InvalidSwigData)?; + for action in oracle_recurring_limits { + permissions.push(Permission::OracleLimit { + base_asset_type: action.base_asset_type, + value_limit: action.recurring_value_limit, + passthrough_check: action.passthrough_check != 0, + recurring: Some(RecurringConfig { + window: action.window, + last_reset: action.last_reset, + current_amount: action.current_amount, + }), + }); + } + Ok(permissions) } @@ -649,6 +727,13 @@ impl Permission { }, Permission::StakeAll => 12, Permission::AllButManageAuthority => 15, + Permission::OracleLimit { recurring, .. } => { + if recurring.is_some() { + 21 // OracleRecurringLimit + } else { + 20 // OracleTokenLimit + } + }, } } } diff --git a/rust-sdk/src/wallet.rs b/rust-sdk/src/wallet.rs index aa55d883..6c478f29 100644 --- a/rust-sdk/src/wallet.rs +++ b/rust-sdk/src/wallet.rs @@ -27,7 +27,9 @@ use spl_token::ID as TOKEN_PROGRAM_ID; use swig_interface::{swig, swig_key}; use swig_state::{ action::{ - all::All, manage_authority::ManageAuthority, program_scope::ProgramScope, + all::All, manage_authority::ManageAuthority, oracle_limits::OracleTokenLimit, + oracle_recurring_limit::OracleRecurringLimit, program::Program, program_all::ProgramAll, + program_curated::ProgramCurated, program_scope::ProgramScope, sol_destination_limit::SolDestinationLimit, sol_limit::SolLimit, sol_recurring_destination_limit::SolRecurringDestinationLimit, sol_recurring_limit::SolRecurringLimit, sub_account::SubAccount, @@ -751,6 +753,12 @@ impl<'c> SwigWallet<'c> { #[cfg(all(feature = "rust_sdk_test", test))] let swig_account = self.litesvm.get_account(&swig_pubkey).unwrap(); + let swig_wallet_address = self.get_swig_wallet_address()?; + #[cfg(not(all(feature = "rust_sdk_test", test)))] + let swig_wallet_data = self.rpc_client.get_account(&swig_wallet_address)?; + #[cfg(all(feature = "rust_sdk_test", test))] + let swig_wallet_data = self.litesvm.get_account(&swig_wallet_address).unwrap(); + #[cfg(not(all(feature = "rust_sdk_test", test)))] let swig_data = self.rpc_client.get_account_data(&swig_pubkey)?; #[cfg(all(feature = "rust_sdk_test", test))] @@ -758,7 +766,7 @@ impl<'c> SwigWallet<'c> { #[cfg(not(all(feature = "rust_sdk_test", test)))] let token_accounts = self.rpc_client.get_token_accounts_by_owner( - &swig_pubkey, + &swig_wallet_address, TokenAccountsFilter::ProgramId(TOKEN_PROGRAM_ID), )?; #[cfg(all(feature = "rust_sdk_test", test))] @@ -766,7 +774,7 @@ impl<'c> SwigWallet<'c> { #[cfg(not(all(feature = "rust_sdk_test", test)))] let token_accounts_22 = self.rpc_client.get_token_accounts_by_owner( - &swig_pubkey, + &swig_wallet_address, TokenAccountsFilter::ProgramId(TOKEN_22_PROGRAM_ID), )?; #[cfg(all(feature = "rust_sdk_test", test))] @@ -778,11 +786,12 @@ impl<'c> SwigWallet<'c> { println!("╔══════════════════════════════════════════════════════════════════"); println!("║ SWIG WALLET DETAILS"); println!("╠══════════════════════════════════════════════════════════════════"); - println!("║ Account Address: {}", swig_pubkey); + println!("║ Swig Account Address: {}", swig_wallet_address); + println!("║ Config Address: {}", swig_pubkey); println!("║ Total Roles: {}", swig_with_roles.state.role_counter); println!( "║ Balance: {} SOL", - swig_account.lamports() as f64 / 1_000_000_000.0 + swig_wallet_data.lamports() as f64 / 1_000_000_000.0 ); if !token_accounts.is_empty() || !token_accounts_22.is_empty() { println!("║ Token Balances:"); @@ -871,6 +880,27 @@ impl<'c> SwigWallet<'c> { } ); + println!("║ ├─ Program Permissions:"); + let program_all_permission = Role::get_action::(&role, &[]) + .map_err(|_| SwigError::AuthorityNotFound)?; + if program_all_permission.is_some() { + println!("║ │ ├─ Full Access (All Permissions)"); + } + let program_curated_permission = Role::get_action::(&role, &[]) + .map_err(|_| SwigError::AuthorityNotFound)?; + if program_curated_permission.is_some() { + println!("║ │ ├─ Curated Programs"); + } + let program_permissions = Role::get_all_actions_of_type::(&role) + .map_err(|_| SwigError::AuthorityNotFound)?; + for (index, action) in program_permissions.iter().enumerate() { + println!( + "║ │ ├─ Program {}: {}", + index + 1, + Pubkey::from(action.program_id) + ); + } + println!("║ ├─ Permissions:"); // Check All permission @@ -1087,6 +1117,64 @@ impl<'c> SwigWallet<'c> { } } + // Check Oracle Limits + let oracle_limits = Role::get_all_actions_of_type::(&role) + .map_err(|_| SwigError::AuthorityNotFound)?; + for (index, action) in oracle_limits.iter().enumerate() { + println!("║ │ ├─ Oracle Limits"); + print!( + "║ │ │ ├─ Amount: {} ", + action.value_limit as f64 + / 10_f64.powf(action.get_base_asset_decimals() as f64) + ); + println!( + "{}", + match action.base_asset_type { + 0 => "USD", + 1 => "EUR", + _ => "Unknown", + } + ); + println!("║ │ │ └─ Passthrough Check: {}", action.passthrough_check); + } + + let oracle_recurring_limits = + Role::get_all_actions_of_type::(&role) + .map_err(|_| SwigError::AuthorityNotFound)?; + for (index, action) in oracle_recurring_limits.iter().enumerate() { + println!("║ │ ├─ Oracle Recurring Limits"); + print!( + "║ │ │ ├─ Current Usage: {} ", + action.current_amount as f64 + / 10_f64.powf(action.get_base_asset_decimals() as f64) + ); + println!( + "{}", + match action.base_asset_type { + 0 => "USD", + 1 => "EUR", + _ => "Unknown", + } + ); + print!( + "║ │ │ ├─ Max Usage for the cycle: {} ", + action.recurring_value_limit as f64 + / 10_f64.powf(action.get_base_asset_decimals() as f64) + ); + println!( + "{}", + match action.base_asset_type { + 0 => "USD", + 1 => "EUR", + _ => "Unknown", + } + ); + println!("║ │ │ ├─ Window: {} slots", action.window); + + println!("║ │ │ └─ Last Reset: Slot {}", action.last_reset); + println!("║ │ │ └─ Passthrough Check: {}", action.passthrough_check); + } + println!("║ │ "); } } @@ -1205,6 +1293,10 @@ impl<'c> SwigWallet<'c> { self.authority_keypair = authority_kp; + if authority_kp.is_some() { + self.switch_payer(authority_kp.unwrap())?; + } + // Update the stored role data for the new authority self.refresh_permissions()?; diff --git a/state/Cargo.toml b/state/Cargo.toml index e817a8c6..f721f729 100644 --- a/state/Cargo.toml +++ b/state/Cargo.toml @@ -12,6 +12,7 @@ pinocchio-pubkey = { version = "0.3" } swig-assertions = { path = "../assertions" } no-padding = { path = "../no-padding" } libsecp256k1 = { version = "0.7.2", default-features = false } +hex = "0.4.3" [target.'cfg(not(feature = "static_syscalls"))'.dependencies] murmur3 = "0.5.2" diff --git a/state/src/action/mod.rs b/state/src/action/mod.rs index b6715f6b..eb3a0137 100644 --- a/state/src/action/mod.rs +++ b/state/src/action/mod.rs @@ -8,6 +8,8 @@ pub mod all; pub mod all_but_manage_authority; pub mod manage_authority; +pub mod oracle_limits; +pub mod oracle_recurring_limit; pub mod program; pub mod program_all; pub mod program_curated; @@ -24,10 +26,13 @@ pub mod token_destination_limit; pub mod token_limit; pub mod token_recurring_destination_limit; pub mod token_recurring_limit; +use crate::{IntoBytes, SwigStateError, Transmutable, TransmutableMut}; use all::All; use all_but_manage_authority::AllButManageAuthority; use manage_authority::ManageAuthority; use no_padding::NoPadding; +use oracle_limits::OracleTokenLimit; +use oracle_recurring_limit::OracleRecurringLimit; use pinocchio::program_error::ProgramError; use program::Program; use program_all::ProgramAll; @@ -46,8 +51,6 @@ use token_limit::TokenLimit; use token_recurring_destination_limit::TokenRecurringDestinationLimit; use token_recurring_limit::TokenRecurringLimit; -use crate::{IntoBytes, SwigStateError, Transmutable, TransmutableMut}; - /// Represents an action in the Swig wallet system. /// /// Actions define what operations can be performed and under what conditions. @@ -163,6 +166,10 @@ pub enum Permission { /// Permission to perform recurring token operations with limits to specific /// destinations TokenRecurringDestinationLimit = 19, + /// Permission to perform token operations with oracle-based limits + OracleTokenLimit = 20, + /// Permission to perform token operations with recurring oracle-based limits + OracleRecurringLimit = 21, } impl TryFrom for Permission { @@ -172,7 +179,7 @@ impl TryFrom for Permission { fn try_from(value: u16) -> Result { match value { // SAFETY: `value` is guaranteed to be in the range of the enum variants. - 0..=19 => Ok(unsafe { core::mem::transmute::(value) }), + 0..=21 => Ok(unsafe { core::mem::transmute::(value) }), _ => Err(SwigStateError::PermissionLoadError.into()), } } @@ -240,6 +247,8 @@ impl ActionLoader { Permission::TokenRecurringDestinationLimit => { TokenRecurringDestinationLimit::valid_layout(data) }, + Permission::OracleTokenLimit => OracleTokenLimit::valid_layout(data), + Permission::OracleRecurringLimit => OracleRecurringLimit::valid_layout(data), _ => Ok(false), } } diff --git a/state/src/action/oracle_limits.rs b/state/src/action/oracle_limits.rs new file mode 100644 index 00000000..47fa9e65 --- /dev/null +++ b/state/src/action/oracle_limits.rs @@ -0,0 +1,206 @@ +/// Oracle-based token limit action type. +/// +/// This module defines the OracleTokenLimit action type which enforces value-based limits on +/// token operations within the Swig wallet system. It uses oracle price feeds to convert token +/// amounts to a base asset value (e.g. USDC) for limit enforcement. +/// +/// The system supports: +/// - Different base assets (e.g. USDC, EURC) for value denomination +/// - Oracle price feed integration for real-time value conversion +/// - Configurable value limits per base asset +/// - Decimal precision handling for different token types +/// +/// The limits are enforced by: +/// 1. Converting token amounts to base asset value using oracle prices +/// 2. Tracking cumulative usage against the configured limit +/// 3. Preventing operations that would exceed the limit +/// 4. Supporting both token and native SOL operations +use super::{Actionable, Permission}; +use crate::{IntoBytes, SwigAuthenticateError, Transmutable, TransmutableMut}; +use no_padding::NoPadding; +use pinocchio::program_error::ProgramError; +use pinocchio_pubkey::pubkey; + +pub const ORACLE_MAPPING_ACCOUNT: [u8; 32] = + pubkey!("FbeuRDWwLvZWEU3HNtaLoYKagw9rH1NvmjpRMpjMwhDw"); +pub const SCOPE_ACCOUNT: [u8; 32] = pubkey!("3NJYftD5sjVfxSnUdZ1wVML8f3aC6mp1CXCL6L7TnU8C"); +pub const SOL_MINT: [u8; 32] = pubkey!("So11111111111111111111111111111111111111112"); + +/// Represents the base asset type for value denomination. +/// +/// This enum defines the supported base assets that can be used to denominate +/// token value limits. Each base asset has a specific decimal precision that +/// is used in value calculations. +#[repr(u8)] +pub enum BaseAsset { + /// USDC stablecoin with 6 decimal places precision + USD = 0, + /// EURC stablecoin with 6 decimal places precision + EUR = 1, +} + +impl TryFrom for BaseAsset { + type Error = ProgramError; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(BaseAsset::USD), + 1 => Ok(BaseAsset::EUR), + _ => Err(SwigAuthenticateError::InvalidDataPayload.into()), + } + } +} + +impl BaseAsset { + pub fn get_scope_index(&self) -> Option { + match self { + BaseAsset::USD => None, + BaseAsset::EUR => Some(173), + } + } +} + +/// Represents a limit on token operations based on oracle base asset value. +/// +/// This struct tracks and enforces a maximum value of tokens that can be +/// used in operations, denominated in a base asset (e.g. USDC). The limit is enforced +/// by converting token amounts to the base asset value using oracle price feeds. +/// +/// # Fields +/// * `value_limit` - The current remaining amount that can be used (in base asset) +/// * `base_asset_type` - The base asset type used to denominate the limit (e.g. USDC) +/// * `passthrough_check` - Flag to check remaining actions after oracle limit check +/// * `_padding` - Padding bytes to ensure proper struct alignment +#[repr(C, align(8))] +#[derive(Debug, NoPadding)] +pub struct OracleTokenLimit { + /// The current remaining amount that can be used (in base asset) + pub value_limit: u64, + /// The base asset type used to denominate the limit (e.g. USDC) + pub base_asset_type: u8, + /// The passthrough flag, it will check the remaining actions + /// and not just stop with oracle limit check + pub passthrough_check: bool, + /// Padding bytes to ensure proper struct alignment + _padding: [u8; 6], +} + +impl OracleTokenLimit { + /// Creates a new OracleTokenLimit with the specified parameters. + /// + /// # Arguments + /// * `base_asset` - The base asset to denominate the limit in (e.g. USDC) + /// * `value_limit` - The maximum value allowed in base asset + /// * `passthrough_check` - Whether to check remaining actions after oracle limit check + /// + /// # Returns + /// A new OracleTokenLimit instance configured with the specified parameters + pub fn new(base_asset: BaseAsset, value_limit: u64, passthrough_check: bool) -> Self { + Self { + base_asset_type: base_asset as u8, + value_limit, + passthrough_check, + _padding: [0; 6], + } + } + + /// Gets the decimal places for the configured base asset type. + /// + /// # Returns + /// The number of decimal places for the base asset (e.g. 6 for USDC) + pub fn get_base_asset_decimals(&self) -> u8 { + match BaseAsset::try_from(self.base_asset_type).unwrap() { + BaseAsset::USD => 6, + BaseAsset::EUR => 6, + } + } + + /// Processes a token operation by checking the oracle price and value limit. + /// + /// This method: + /// 1. Converts the token amount to oracle decimal precision + /// 2. Multiplies by the oracle price to get the value + /// 3. Adjusts for price exponent + /// 4. Converts to base asset decimal precision + /// 5. Checks against and updates the remaining limit + /// + /// # Arguments + /// * `amount` - The amount of tokens to be used (in token decimals) + /// * `oracle_price` - The current oracle price for the token + /// * `_confidence` - The confidence interval for the oracle price (unused) + /// * `exponent` - The exponent for price calculation + /// * `token_decimals` - The number of decimal places for the token + /// + /// # Returns + /// * `Ok(())` - If the operation is within limits + /// * `Err(ProgramError)` - If the operation would exceed the limit or encounters an error + pub fn run_for_token(&mut self, price: u64) -> Result<(), ProgramError> { + // Check if operation would exceed limit + if price > self.value_limit { + return Err(SwigAuthenticateError::PermissionDeniedOracleLimitReached.into()); + } + + // Safe to subtract since we verified value <= value_limit + self.value_limit = self + .value_limit + .checked_sub(price) + .ok_or(SwigAuthenticateError::PermissionDeniedInsufficientBalance)?; + + Ok(()) + } + + /// Processes a Solana operation by checking the oracle price and value limit. + /// + /// This method handles native SOL operations by: + /// 1. Checking for potential multiplication overflow + /// 2. Converting SOL amount to base asset value using oracle price + /// 3. Adjusting for price exponent + /// 4. Checking against and updating the remaining limit + /// + /// # Arguments + /// * `amount` - The amount of SOL lamports to be used + /// * `oracle_price` - The current oracle price for SOL + /// * `_confidence` - The confidence interval for the oracle price (unused) + /// * `exponent` - The exponent for price calculation + /// + /// # Returns + /// * `Ok(())` - If the operation is within limits + /// * `Err(ProgramError)` - If the operation would exceed the limit or encounters an error + pub fn run_for_sol(&mut self, price: u64) -> Result<(), ProgramError> { + // Check if we have enough limit + if price > self.value_limit { + return Err(SwigAuthenticateError::PermissionDeniedOracleLimitReached.into()); + } + + // Safe to subtract since we verified value <= value_limit + self.value_limit -= price; + Ok(()) + } +} + +impl Transmutable for OracleTokenLimit { + /// Size of the OracleTokenLimit struct in bytes + const LEN: usize = core::mem::size_of::(); +} + +impl TransmutableMut for OracleTokenLimit {} + +impl IntoBytes for OracleTokenLimit { + /// Converts the OracleTokenLimit struct into a byte slice. + /// + /// # Returns + /// * `Ok(&[u8])` - A byte slice representing the struct + /// * `Err(ProgramError)` - If the conversion fails + fn into_bytes(&self) -> Result<&[u8], ProgramError> { + let bytes = + unsafe { core::slice::from_raw_parts(self as *const Self as *const u8, Self::LEN) }; + Ok(bytes) + } +} + +impl<'a> Actionable<'a> for OracleTokenLimit { + /// This action represents the OracleTokenLimit permission type + const TYPE: Permission = Permission::OracleTokenLimit; + /// Multiple oracle token limits can exist per role (one per base asset) + const REPEATABLE: bool = false; +} diff --git a/state/src/action/oracle_recurring_limit.rs b/state/src/action/oracle_recurring_limit.rs new file mode 100644 index 00000000..6b4dd270 --- /dev/null +++ b/state/src/action/oracle_recurring_limit.rs @@ -0,0 +1,233 @@ +/// Oracle-based recurring token limit action type. +/// +/// This module defines the OracleRecurringLimit action type which enforces recurring +/// value-based limits on token operations within the Swig wallet system. It uses oracle +/// price feeds to convert token amounts to a base asset value (e.g. USDC) for limit +/// enforcement and resets the limit after a specified time window. +/// +/// The system supports: +/// - Different base assets (e.g. USDC, EURC) for value denomination +/// - Oracle price feed integration for real-time value conversion +/// - Configurable recurring value limits per base asset +/// - Time-based window resets for recurring limits +/// - Decimal precision handling for different token types +/// +/// The limits are enforced by: +/// 1. Converting token amounts to base asset value using oracle prices +/// 2. Tracking cumulative usage against the configured recurring limit +/// 3. Resetting the limit after the time window expires +/// 4. Preventing operations that would exceed the current limit +/// 5. Supporting both token and native SOL operations +use super::{Actionable, Permission}; +use crate::{IntoBytes, SwigAuthenticateError, Transmutable, TransmutableMut}; +use no_padding::NoPadding; +use pinocchio::msg; +use pinocchio::program_error::ProgramError; + +/// Represents a recurring limit on token operations based on oracle base asset value. +/// +/// This struct tracks and enforces a maximum value of tokens that can be +/// used in operations within a specified time window, denominated in a base asset +/// (e.g. USDC). The limit is enforced by converting token amounts to the base asset +/// value using oracle price feeds and resets after the time window expires. +/// +/// # Fields +/// * `recurring_value_limit` - The value limit that resets each window (in base asset) +/// * `base_asset_type` - The base asset type used to denominate the limit (e.g. USDC) +/// * `window` - The time window in slots after which the limit resets +/// * `last_reset` - The last slot when the limit was reset +/// * `current_amount` - The current remaining amount that can be used (in base asset) +/// * `passthrough_check` - Flag to check remaining actions after oracle limit check +/// * `_padding` - Padding bytes to ensure proper struct alignment +#[repr(C, align(8))] +#[derive(Debug, NoPadding)] +pub struct OracleRecurringLimit { + /// The value limit that resets each window (in base asset) + pub recurring_value_limit: u64, + /// The time window in slots after which the limit resets + pub window: u64, + /// The last slot when the limit was reset + pub last_reset: u64, + /// The current remaining amount that can be used (in base asset) + pub current_amount: u64, + /// The base asset type used to denominate the limit (e.g. USDC) + pub base_asset_type: u8, + /// The passthrough flag, it will check the remaining actions + /// and not just stop with oracle limit check + pub passthrough_check: u8, + /// Padding bytes to ensure proper struct alignment + _padding: [u8; 6], +} + +impl OracleRecurringLimit { + /// Creates a new OracleRecurringLimit with the specified parameters. + /// + /// # Arguments + /// * `base_asset` - The base asset to denominate the limit in (e.g. USDC) + /// * `recurring_value_limit` - The maximum value allowed in base asset that resets each window + /// * `window` - The time window in slots after which the limit resets + /// * `passthrough_check` - Whether to check remaining actions after oracle limit check + /// + /// # Returns + /// A new OracleRecurringLimit instance configured with the specified parameters + pub fn new( + base_asset: super::oracle_limits::BaseAsset, + recurring_value_limit: u64, + window: u64, + passthrough_check: bool, + ) -> Self { + Self { + recurring_value_limit, + window, + last_reset: 0, + current_amount: recurring_value_limit, + base_asset_type: base_asset as u8, + passthrough_check: passthrough_check as u8, + _padding: [0; 6], + } + } + + /// Gets the decimal places for the configured base asset type. + /// + /// # Returns + /// The number of decimal places for the base asset (e.g. 6 for USDC) + pub fn get_base_asset_decimals(&self) -> u8 { + match super::oracle_limits::BaseAsset::try_from(self.base_asset_type).unwrap() { + super::oracle_limits::BaseAsset::USD => 6, + super::oracle_limits::BaseAsset::EUR => 6, + } + } + + /// Processes a token operation by checking the oracle price and recurring value limit. + /// + /// This method: + /// 1. Checks if the time window has expired and resets the limit if needed + /// 2. Converts the token amount to oracle decimal precision + /// 3. Multiplies by the oracle price to get the value + /// 4. Adjusts for price exponent + /// 5. Converts to base asset decimal precision + /// 6. Checks against and updates the remaining limit + /// + /// # Arguments + /// * `price` - The value in base asset for the token operation + /// * `current_slot` - The current slot number for window calculation + /// + /// # Returns + /// * `Ok(())` - If the operation is within limits + /// * `Err(ProgramError)` - If the operation would exceed the limit or encounters an error + pub fn run_for_token(&mut self, price: u64, current_slot: u64) -> Result<(), ProgramError> { + // Check if time window has expired and reset if needed + if current_slot - self.last_reset > self.window { + self.current_amount = self.recurring_value_limit; + self.last_reset = current_slot; + } + + // Check if operation would exceed limit + if price > self.current_amount { + msg!("Operation denied: Would exceed recurring value limit"); + return Err(SwigAuthenticateError::PermissionDeniedOracleLimitReached.into()); + } + + // Safe to subtract since we verified value <= current_amount + self.current_amount = self + .current_amount + .checked_sub(price) + .ok_or(SwigAuthenticateError::PermissionDeniedInsufficientBalance)?; + + Ok(()) + } + + /// Processes a Solana operation by checking the oracle price and recurring value limit. + /// + /// This method handles native SOL operations by: + /// 1. Checking if the time window has expired and resets the limit if needed + /// 2. Checking for potential multiplication overflow + /// 3. Converting SOL amount to base asset value using oracle price + /// 4. Adjusting for price exponent + /// 5. Checking against and updating the remaining limit + /// + /// # Arguments + /// * `price` - The value in base asset for the SOL operation + /// * `current_slot` - The current slot number for window calculation + /// + /// # Returns + /// * `Ok(())` - If the operation is within limits + /// * `Err(ProgramError)` - If the operation would exceed the limit or encounters an error + pub fn run_for_sol(&mut self, price: u64, current_slot: u64) -> Result<(), ProgramError> { + // Check if time window has expired and reset if needed + if current_slot - self.last_reset > self.window { + self.current_amount = self.recurring_value_limit; + self.last_reset = current_slot; + } + + // Check if we have enough limit + if price > self.current_amount { + return Err(SwigAuthenticateError::PermissionDeniedOracleLimitReached.into()); + } + + // Safe to subtract since we verified value <= current_amount + self.current_amount -= price; + Ok(()) + } +} + +impl Transmutable for OracleRecurringLimit { + /// Size of the OracleRecurringLimit struct in bytes + const LEN: usize = core::mem::size_of::(); +} + +impl TransmutableMut for OracleRecurringLimit {} + +impl IntoBytes for OracleRecurringLimit { + /// Converts the OracleRecurringLimit struct into a byte slice. + /// + /// # Returns + /// * `Ok(&[u8])` - A byte slice representing the struct + /// * `Err(ProgramError)` - If the conversion fails + fn into_bytes(&self) -> Result<&[u8], ProgramError> { + let bytes = + unsafe { core::slice::from_raw_parts(self as *const Self as *const u8, Self::LEN) }; + Ok(bytes) + } +} + +impl<'a> Actionable<'a> for OracleRecurringLimit { + /// This action represents the OracleRecurringLimit permission type + const TYPE: Permission = Permission::OracleRecurringLimit; + /// Multiple oracle recurring limits can exist per role (one per base asset) + const REPEATABLE: bool = false; + + /// Checks if this token limit matches the provided base asset type. + /// + /// # Arguments + /// * `data` - The base asset type to check against (first byte) + /// + /// # Returns + /// `true` if the base asset type matches, `false` otherwise + fn match_data(&self, data: &[u8]) -> bool { + !data.is_empty() + } + + /// Validates the layout of the action data. + /// + /// # Arguments + /// * `data` - The action data to validate + /// + /// # Returns + /// * `Ok(true)` - If the layout is valid + /// * `Err(ProgramError)` - If validation fails + fn valid_layout(data: &'a [u8]) -> Result { + if data.len() != Self::LEN { + return Ok(false); + } + + // Check that current_amount equals recurring_value_limit initially + let current_amount_bytes = &data[24..32]; + let recurring_value_limit_bytes = &data[0..8]; + + // Check that last_reset is 0 initially + let last_reset_bytes = &data[16..24]; + + Ok(current_amount_bytes == recurring_value_limit_bytes && last_reset_bytes == &[0u8; 8]) + } +} diff --git a/state/src/lib.rs b/state/src/lib.rs index 447faa7e..36024350 100644 --- a/state/src/lib.rs +++ b/state/src/lib.rs @@ -108,6 +108,12 @@ pub enum SwigStateError { PermissionLoadError, /// Adding an authority requires at least one action InvalidAuthorityMustHaveAtLeastOneAction, + /// Oracle not available for mint + InvalidOracleTokenMint, + /// Feed Id non hex char + FeedIdNonHexCharacter, + /// Feed id must be 32 bytes + FeedIdMustBe32Bytes, } /// Error types related to authentication operations. @@ -178,6 +184,8 @@ pub enum SwigAuthenticateError { PermissionDeniedTokenDestinationLimitExceeded, /// Token destination recurring limit exceeded PermissionDeniedRecurringTokenDestinationLimitExceeded, + /// Missing oracle account + PermissionDeniedOracleLimitReached, } impl From for ProgramError {