diff --git a/Cargo.lock b/Cargo.lock index a79bac78..1b330976 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1600,6 +1600,7 @@ dependencies = [ "jiminy-entrypoint", "jiminy-log", "jiminy-pda", + "jiminy-sysvar-instructions", "jiminy-sysvar-rent", "mollusk-svm", "proptest", @@ -1608,6 +1609,7 @@ dependencies = [ "sanctum-system-jiminy", "solana-account", "solana-instruction", + "solana-instructions-sysvar", "solana-pubkey", ] @@ -1901,6 +1903,7 @@ dependencies = [ "inf1-svc-spl-core", "inf1-svc-wsol-core", "jiminy-program-error", + "jiminy-sysvar-instructions", "jiminy-sysvar-rent", "lazy_static", "mollusk-svm", @@ -1914,9 +1917,11 @@ dependencies = [ "solana-account", "solana-account-decoder-client-types", "solana-instruction", + "solana-instructions-sysvar", "solana-logger", "solana-program-error", "solana-pubkey", + "solana-sdk", "solido-legacy-core", ] @@ -1987,7 +1992,7 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiminy-account" version = "0.1.0" -source = "git+https://github.com/igneous-labs/jiminy.git?branch=master#92fc4e4236a47e68d5ed34a1b1b2a15220424c5f" +source = "git+https://github.com/igneous-labs/jiminy.git?branch=master#74ac27a8c991e27603b0a84b45f57b1e66105c91" dependencies = [ "jiminy-program-error", ] @@ -1995,7 +2000,7 @@ dependencies = [ [[package]] name = "jiminy-cpi" version = "0.1.0" -source = "git+https://github.com/igneous-labs/jiminy.git?branch=master#92fc4e4236a47e68d5ed34a1b1b2a15220424c5f" +source = "git+https://github.com/igneous-labs/jiminy.git?branch=master#74ac27a8c991e27603b0a84b45f57b1e66105c91" dependencies = [ "jiminy-account", "jiminy-pda", @@ -2006,7 +2011,7 @@ dependencies = [ [[package]] name = "jiminy-entrypoint" version = "0.1.0" -source = "git+https://github.com/igneous-labs/jiminy.git?branch=master#92fc4e4236a47e68d5ed34a1b1b2a15220424c5f" +source = "git+https://github.com/igneous-labs/jiminy.git?branch=master#74ac27a8c991e27603b0a84b45f57b1e66105c91" dependencies = [ "jiminy-account", "jiminy-syscall", @@ -2015,7 +2020,7 @@ dependencies = [ [[package]] name = "jiminy-log" version = "0.1.0" -source = "git+https://github.com/igneous-labs/jiminy.git?branch=master#92fc4e4236a47e68d5ed34a1b1b2a15220424c5f" +source = "git+https://github.com/igneous-labs/jiminy.git?branch=master#74ac27a8c991e27603b0a84b45f57b1e66105c91" dependencies = [ "jiminy-syscall", ] @@ -2023,7 +2028,7 @@ dependencies = [ [[package]] name = "jiminy-pda" version = "0.1.0" -source = "git+https://github.com/igneous-labs/jiminy.git?branch=master#92fc4e4236a47e68d5ed34a1b1b2a15220424c5f" +source = "git+https://github.com/igneous-labs/jiminy.git?branch=master#74ac27a8c991e27603b0a84b45f57b1e66105c91" dependencies = [ "jiminy-syscall", ] @@ -2031,12 +2036,12 @@ dependencies = [ [[package]] name = "jiminy-program-error" version = "0.1.0" -source = "git+https://github.com/igneous-labs/jiminy.git?branch=master#92fc4e4236a47e68d5ed34a1b1b2a15220424c5f" +source = "git+https://github.com/igneous-labs/jiminy.git?branch=master#74ac27a8c991e27603b0a84b45f57b1e66105c91" [[package]] name = "jiminy-return-data" version = "0.1.0" -source = "git+https://github.com/igneous-labs/jiminy.git?branch=master#92fc4e4236a47e68d5ed34a1b1b2a15220424c5f" +source = "git+https://github.com/igneous-labs/jiminy.git?branch=master#74ac27a8c991e27603b0a84b45f57b1e66105c91" dependencies = [ "jiminy-syscall", ] @@ -2044,22 +2049,32 @@ dependencies = [ [[package]] name = "jiminy-syscall" version = "0.1.0" -source = "git+https://github.com/igneous-labs/jiminy.git?branch=master#92fc4e4236a47e68d5ed34a1b1b2a15220424c5f" +source = "git+https://github.com/igneous-labs/jiminy.git?branch=master#74ac27a8c991e27603b0a84b45f57b1e66105c91" [[package]] name = "jiminy-sysvar" version = "0.1.0" -source = "git+https://github.com/igneous-labs/jiminy.git?branch=master#92fc4e4236a47e68d5ed34a1b1b2a15220424c5f" +source = "git+https://github.com/igneous-labs/jiminy.git?branch=master#74ac27a8c991e27603b0a84b45f57b1e66105c91" dependencies = [ "const-crypto", "jiminy-program-error", "jiminy-syscall", ] +[[package]] +name = "jiminy-sysvar-instructions" +version = "0.1.0" +source = "git+https://github.com/igneous-labs/jiminy.git?branch=master#74ac27a8c991e27603b0a84b45f57b1e66105c91" +dependencies = [ + "const-crypto", + "jiminy-account", + "jiminy-sysvar", +] + [[package]] name = "jiminy-sysvar-rent" version = "0.1.0" -source = "git+https://github.com/igneous-labs/jiminy.git?branch=master#92fc4e4236a47e68d5ed34a1b1b2a15220424c5f" +source = "git+https://github.com/igneous-labs/jiminy.git?branch=master#74ac27a8c991e27603b0a84b45f57b1e66105c91" dependencies = [ "const-crypto", "jiminy-sysvar", diff --git a/Cargo.toml b/Cargo.toml index e2f2350b..61f16b89 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,6 +68,7 @@ mollusk-svm-programs-token = { version = "^0.5", default-features = false } solana-account = { version = "^2.2.1", default-features = false } # vers constraint based on solana-sdk subdep solana-account-decoder-client-types = { version = "^2", default-features = false } solana-instruction = { version = "^2.3.0", default-features = false } # vers constraint based on solana-sdk subdep +solana-instructions-sysvar = { version = "^2.2.1", default-features = false } solana-logger = { version = "^2.3", default-features = false } solana-program-error = { version = "^2.2", default-features = false } solana-pubkey = { version = "^2.2.1", default-features = false } # vers constraint based on solana-sdk subdep @@ -87,6 +88,7 @@ jiminy-log = { git = "https://github.com/igneous-labs/jiminy.git", branch = "mas jiminy-pda = { git = "https://github.com/igneous-labs/jiminy.git", branch = "master", default-features = false } jiminy-program-error = { git = "https://github.com/igneous-labs/jiminy.git", branch = "master", default-features = false } jiminy-return-data = { git = "https://github.com/igneous-labs/jiminy.git", branch = "master", default-features = false } +jiminy-sysvar-instructions = { git = "https://github.com/igneous-labs/jiminy.git", branch = "master", default-features = false } jiminy-sysvar-rent = { git = "https://github.com/igneous-labs/jiminy.git", branch = "master", default-features = false } sanctum-ata-jiminy = { git = "https://github.com/igneous-labs/sanctum-ata-sdk.git", branch = "master", default-features = false } sanctum-spl-token-core = { git = "https://github.com/igneous-labs/sanctum-spl-token-sdk.git", branch = "master", default-features = false } diff --git a/controller/core/src/accounts/mod.rs b/controller/core/src/accounts/mod.rs index 65c43efa..3b8c9621 100644 --- a/controller/core/src/accounts/mod.rs +++ b/controller/core/src/accounts/mod.rs @@ -2,3 +2,4 @@ pub mod disable_pool_authority_list; pub mod lst_state_list; pub mod packed_list; pub mod pool_state; +pub mod rebalance_record; diff --git a/controller/core/src/accounts/rebalance_record.rs b/controller/core/src/accounts/rebalance_record.rs new file mode 100644 index 00000000..fad6506d --- /dev/null +++ b/controller/core/src/accounts/rebalance_record.rs @@ -0,0 +1,58 @@ +use core::mem::size_of; + +use crate::internal_utils::{impl_cast_from_acc_data, impl_cast_to_acc_data}; + +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct RebalanceRecord { + pub old_total_sol_value: u64, + pub inp_lst_index: u32, + pub padding: [u8; 4], +} +impl_cast_from_acc_data!(RebalanceRecord); +impl_cast_to_acc_data!(RebalanceRecord); + +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct RebalanceRecordPacked { + old_total_sol_value: [u8; 8], + inp_lst_index: [u8; 4], + padding: [u8; 4], +} +impl_cast_from_acc_data!(RebalanceRecordPacked, packed); +impl_cast_to_acc_data!(RebalanceRecordPacked, packed); + +impl RebalanceRecordPacked { + #[inline] + pub const fn into_rebalance_record(self) -> RebalanceRecord { + let Self { + old_total_sol_value, + inp_lst_index, + padding, + } = self; + RebalanceRecord { + old_total_sol_value: u64::from_le_bytes(old_total_sol_value), + inp_lst_index: u32::from_le_bytes(inp_lst_index), + padding, + } + } + + /// # Safety + /// - `self` must be pointing to mem that has same align as `RebalanceRecord`. + /// This is true onchain for a RebalanceRecord account since account data + /// is always 8-byte aligned onchain. + #[inline] + pub const unsafe fn as_rebalance_record(&self) -> &RebalanceRecord { + &*(self as *const Self).cast() + } +} + +impl From for RebalanceRecord { + #[inline] + fn from(value: RebalanceRecordPacked) -> Self { + value.into_rebalance_record() + } +} + +const _ASSERT_PACKED_UNPACKED_SIZES_EQ: () = + assert!(size_of::() == size_of::()); diff --git a/controller/core/src/instructions/rebalance/start.rs b/controller/core/src/instructions/rebalance/start.rs index 91cb298f..bcc56115 100644 --- a/controller/core/src/instructions/rebalance/start.rs +++ b/controller/core/src/instructions/rebalance/start.rs @@ -1,6 +1,6 @@ use generic_array_struct::generic_array_struct; -use crate::instructions::internal_utils::caba; +use crate::instructions::internal_utils::{caba, csba}; // Accounts @@ -104,4 +104,25 @@ impl StartRebalanceIxData { pub const fn as_buf(&self) -> &[u8; START_REBALANCE_IX_DATA_LEN] { &self.0 } + + #[inline] + pub const fn parse_no_discm( + data: &[u8; START_REBALANCE_IX_DATA_LEN - 1], + ) -> StartRebalanceIxArgs { + let (out_lst_value_calc_accs, rest) = csba::<33, 1, 32>(data); + let (out_lst_index, rest) = csba::<32, 4, 28>(rest); + let (inp_lst_index, rest) = csba::<28, 4, 24>(rest); + let (amount, rest) = csba::<24, 8, 16>(rest); + let (min_starting_out_lst, rest) = csba::<16, 8, 8>(rest); + let (max_starting_inp_lst, _) = csba::<8, 8, 0>(rest); + + StartRebalanceIxArgs { + out_lst_value_calc_accs: out_lst_value_calc_accs[0], + out_lst_index: u32::from_le_bytes(*out_lst_index), + inp_lst_index: u32::from_le_bytes(*inp_lst_index), + amount: u64::from_le_bytes(*amount), + min_starting_out_lst: u64::from_le_bytes(*min_starting_out_lst), + max_starting_inp_lst: u64::from_le_bytes(*max_starting_inp_lst), + } + } } diff --git a/controller/jiminy/src/account_utils.rs b/controller/jiminy/src/account_utils.rs index e1952c67..bc02c399 100644 --- a/controller/jiminy/src/account_utils.rs +++ b/controller/jiminy/src/account_utils.rs @@ -4,6 +4,7 @@ use inf1_ctl_core::{ lst_state_list::{LstStateList, LstStateListMut}, packed_list::{PackedList, PackedListMut}, pool_state::PoolState, + rebalance_record::RebalanceRecord, }, err::Inf1CtlErr, typedefs::lst_state::LstState, @@ -105,6 +106,25 @@ pub fn disable_pool_auth_list_checked_mut( } } +const _REBALANCE_RECORD_ALIGN_CHECK: () = + assert!(core::mem::align_of::() <= _ACC_DATA_ALIGN); + +#[inline] +pub fn rebalance_record_checked(acc: &Account) -> Result<&RebalanceRecord, Inf1CtlCustomProgErr> { + // safety: account data is 8-byte aligned + unsafe { RebalanceRecord::of_acc_data(acc.data()) } + .ok_or(Inf1CtlCustomProgErr(Inf1CtlErr::InvalidRebalanceRecordData)) +} + +#[inline] +pub fn rebalance_record_checked_mut( + acc: &mut Account, +) -> Result<&mut RebalanceRecord, Inf1CtlCustomProgErr> { + // safety: account data is 8-byte aligned + unsafe { RebalanceRecord::of_acc_data_mut(acc.data_mut()) } + .ok_or(Inf1CtlCustomProgErr(Inf1CtlErr::InvalidRebalanceRecordData)) +} + // TODO: refactor to use this fn everywhere #[inline] pub fn lst_state_list_get( diff --git a/controller/jiminy/src/cpi.rs b/controller/jiminy/src/cpi.rs index f93f30ac..61948504 100644 --- a/controller/jiminy/src/cpi.rs +++ b/controller/jiminy/src/cpi.rs @@ -2,6 +2,7 @@ use core::ops::RangeInclusive; use inf1_ctl_core::instructions::{ admin::set_sol_value_calculator::SetSolValueCalculatorIxPreAccs, liquidity::{add::AddLiquidityIxPreAccs, remove::RemoveLiquidityIxPreAccs}, + rebalance::{end::EndRebalanceIxPreAccs, start::StartRebalanceIxPreAccs}, swap::IxPreAccs as SwapIxPreAccs, sync_sol_value::SyncSolValueIxPreAccs, }; @@ -31,6 +32,15 @@ pub type RemoveLiquidityPreAccountHandles<'account> = pub type SetSolValueCalculatorIxPreAccountHandles<'account> = SetSolValueCalculatorIxPreAccs>; +/// `S: AsRef<[AccountHandle]>` +/// -> use [`IxAccountHandles::seq`] with [`jiminy_cpi::Cpi::invoke_fwd`] +pub type StartRebalanceIxPreAccountHandles<'account> = + StartRebalanceIxPreAccs>; + +/// `S: AsRef<[AccountHandle]>` +/// -> use [`IxAccountHandles::seq`] with [`jiminy_cpi::Cpi::invoke_fwd`] +pub type EndRebalanceIxPreAccountHandles<'account> = EndRebalanceIxPreAccs>; + pub type SwapIxPreAccountHandles<'account> = SwapIxPreAccs>; // TODO: make invoke() helpers for client programs diff --git a/controller/jiminy/src/pda_onchain.rs b/controller/jiminy/src/pda_onchain.rs index 78e198c3..7b21fc9b 100644 --- a/controller/jiminy/src/pda_onchain.rs +++ b/controller/jiminy/src/pda_onchain.rs @@ -1,11 +1,12 @@ use inf1_ctl_core::{ keys::{ ATOKEN_ID, DISABLE_POOL_AUTHORITY_LIST_BUMP, LST_STATE_LIST_BUMP, POOL_STATE_BUMP, - PROTOCOL_FEE_BUMP, + PROTOCOL_FEE_BUMP, REBALANCE_RECORD_BUMP, }, pda::{ pool_reserves_ata_seeds, protocol_fee_accumulator_ata_seeds, DISABLE_POOL_AUTHORITY_LIST_SEED, LST_STATE_LIST_SEED, POOL_STATE_SEED, PROTOCOL_FEE_SEED, + REBALANCE_RECORD_SEED, }, }; use jiminy_pda::{ @@ -31,6 +32,11 @@ const_1seed_signer!( DISABLE_POOL_AUTHORITY_LIST_SEED, DISABLE_POOL_AUTHORITY_LIST_BUMP ); +const_1seed_signer!( + REBALANCE_RECORD_SIGNER, + REBALANCE_RECORD_SEED, + REBALANCE_RECORD_BUMP +); #[inline] pub fn create_raw_pool_reserves_addr( diff --git a/controller/program/Cargo.toml b/controller/program/Cargo.toml index c6f4ce43..740a79a5 100644 --- a/controller/program/Cargo.toml +++ b/controller/program/Cargo.toml @@ -22,6 +22,7 @@ jiminy-cpi = { workspace = true } jiminy-entrypoint = { workspace = true, features = ["allocator", "panic"] } jiminy-log = { workspace = true } jiminy-pda = { workspace = true } +jiminy-sysvar-instructions = { workspace = true } jiminy-sysvar-rent = { workspace = true } sanctum-ata-jiminy = { workspace = true } sanctum-spl-token-jiminy = { workspace = true } @@ -40,4 +41,5 @@ mollusk-svm = { workspace = true } proptest = { workspace = true, features = ["std"] } solana-account = { workspace = true } solana-instruction = { workspace = true } +solana-instructions-sysvar = { workspace = true } solana-pubkey = { workspace = true } diff --git a/controller/program/src/instructions/mod.rs b/controller/program/src/instructions/mod.rs index 1b24a4b6..85cf1e1b 100644 --- a/controller/program/src/instructions/mod.rs +++ b/controller/program/src/instructions/mod.rs @@ -2,5 +2,6 @@ pub mod admin; pub mod disable_pool; pub mod liquidity; pub mod protocol_fee; +pub mod rebalance; pub mod swap; pub mod sync_sol_value; diff --git a/controller/program/src/instructions/rebalance/end.rs b/controller/program/src/instructions/rebalance/end.rs new file mode 100644 index 00000000..7075327e --- /dev/null +++ b/controller/program/src/instructions/rebalance/end.rs @@ -0,0 +1,148 @@ +use inf1_ctl_jiminy::{ + account_utils::{ + lst_state_list_checked, pool_state_checked, pool_state_checked_mut, + rebalance_record_checked, + }, + cpi::EndRebalanceIxPreAccountHandles, + err::Inf1CtlErr, + instructions::{ + rebalance::end::{ + EndRebalanceIxPreAccs, NewEndRebalanceIxPreAccsBuilder, END_REBALANCE_IX_PRE_IS_SIGNER, + }, + sync_sol_value::NewSyncSolValueIxPreAccsBuilder, + }, + keys::{LST_STATE_LIST_ID, POOL_STATE_ID, REBALANCE_RECORD_ID}, + pda_onchain::create_raw_pool_reserves_addr, + program_err::Inf1CtlCustomProgErr, + typedefs::u8bool::U8BoolMut, +}; +use jiminy_cpi::{ + account::{Abr, AccountHandle}, + program_error::{ProgramError, NOT_ENOUGH_ACCOUNT_KEYS}, +}; + +use inf1_core::instructions::{ + rebalance::end::EndRebalanceIxAccs, sync_sol_value::SyncSolValueIxAccs, +}; + +use crate::{ + svc::lst_sync_sol_val_unchecked, + verify::{verify_is_rebalancing, verify_pks, verify_signers}, + Cpi, +}; + +pub type EndRebalanceIxAccounts<'a, 'acc> = EndRebalanceIxAccs< + AccountHandle<'acc>, + EndRebalanceIxPreAccountHandles<'acc>, + &'a [AccountHandle<'acc>], +>; + +fn end_rebalance_accs_checked<'a, 'acc>( + abr: &Abr, + accounts: &'a [AccountHandle<'acc>], +) -> Result, ProgramError> { + let (ix_prefix, suf) = accounts + .split_first_chunk() + .ok_or(NOT_ENOUGH_ACCOUNT_KEYS)?; + let ix_prefix = EndRebalanceIxPreAccs(*ix_prefix); + + let pool = pool_state_checked(abr.get(*ix_prefix.pool_state()))?; + let list = lst_state_list_checked(abr.get(*ix_prefix.lst_state_list()))?; + + verify_is_rebalancing(pool)?; + + let rr = rebalance_record_checked(abr.get(*ix_prefix.rebalance_record()))?; + + let inp_lst_idx = rr.inp_lst_index as usize; + let inp_lst_state = list + .0 + .get(inp_lst_idx) + .ok_or(Inf1CtlCustomProgErr(Inf1CtlErr::InvalidLstIndex))?; + + let inp_lst_mint_acc = abr.get(*ix_prefix.inp_lst_mint()); + let inp_token_prog = inp_lst_mint_acc.owner(); + let expected_inp_reserves = create_raw_pool_reserves_addr( + inp_token_prog, + &inp_lst_state.mint, + &inp_lst_state.pool_reserves_bump, + ) + .ok_or(Inf1CtlCustomProgErr(Inf1CtlErr::InvalidReserves))?; + + let expected_pks = NewEndRebalanceIxPreAccsBuilder::start() + .with_rebalance_auth(&pool.rebalance_authority) + .with_pool_state(&POOL_STATE_ID) + .with_lst_state_list(&LST_STATE_LIST_ID) + .with_rebalance_record(&REBALANCE_RECORD_ID) + .with_inp_lst_mint(&inp_lst_state.mint) + .with_inp_pool_reserves(&expected_inp_reserves) + .build(); + verify_pks(abr, &ix_prefix.0, &expected_pks.0)?; + + verify_signers(abr, &ix_prefix.0, &END_REBALANCE_IX_PRE_IS_SIGNER.0)?; + + let (inp_calc_prog, inp_calc) = suf.split_first().ok_or(NOT_ENOUGH_ACCOUNT_KEYS)?; + + verify_pks( + abr, + &[*inp_calc_prog], + &[&inp_lst_state.sol_value_calculator], + )?; + + Ok(EndRebalanceIxAccounts { + ix_prefix, + inp_calc_prog: *inp_calc_prog, + inp_calc, + }) +} + +#[inline] +pub fn process_end_rebalance( + abr: &mut Abr, + accounts: &[AccountHandle], + cpi: &mut Cpi, +) -> Result<(), ProgramError> { + let EndRebalanceIxAccounts { + ix_prefix, + inp_calc_prog, + inp_calc, + } = end_rebalance_accs_checked(abr, accounts)?; + + let pool_acc = abr.get_mut(*ix_prefix.pool_state()); + let pool = pool_state_checked_mut(pool_acc)?; + U8BoolMut(&mut pool.is_rebalancing).set_false(); + + let (old_total_sol_value, inp_lst_idx) = { + let rr = rebalance_record_checked(abr.get(*ix_prefix.rebalance_record()))?; + + (rr.old_total_sol_value, rr.inp_lst_index as usize) + }; + + lst_sync_sol_val_unchecked( + abr, + cpi, + SyncSolValueIxAccs { + ix_prefix: NewSyncSolValueIxPreAccsBuilder::start() + .with_lst_mint(*ix_prefix.inp_lst_mint()) + .with_pool_state(*ix_prefix.pool_state()) + .with_lst_state_list(*ix_prefix.lst_state_list()) + .with_pool_reserves(*ix_prefix.inp_pool_reserves()) + .build(), + calc_prog: inp_calc_prog, + calc: inp_calc, + }, + inp_lst_idx, + )?; + + let new_total_sol_value = { + let pool = pool_state_checked(abr.get(*ix_prefix.pool_state()))?; + pool.total_sol_value + }; + + if new_total_sol_value < old_total_sol_value { + return Err(Inf1CtlCustomProgErr(Inf1CtlErr::PoolWouldLoseSolValue).into()); + } + + abr.close(*ix_prefix.rebalance_record(), *ix_prefix.pool_state())?; + + Ok(()) +} diff --git a/controller/program/src/instructions/rebalance/mod.rs b/controller/program/src/instructions/rebalance/mod.rs new file mode 100644 index 00000000..cf88740d --- /dev/null +++ b/controller/program/src/instructions/rebalance/mod.rs @@ -0,0 +1,2 @@ +pub mod end; +pub mod start; diff --git a/controller/program/src/instructions/rebalance/start.rs b/controller/program/src/instructions/rebalance/start.rs new file mode 100644 index 00000000..2b871e79 --- /dev/null +++ b/controller/program/src/instructions/rebalance/start.rs @@ -0,0 +1,340 @@ +use inf1_ctl_jiminy::{ + account_utils::{ + lst_state_list_checked, pool_state_checked, pool_state_checked_mut, + rebalance_record_checked_mut, + }, + accounts::rebalance_record::RebalanceRecord, + cpi::StartRebalanceIxPreAccountHandles, + err::Inf1CtlErr, + instructions::{ + rebalance::{ + end::{END_REBALANCE_IX_DISCM, END_REBALANCE_IX_PRE_ACCS_IDX_INP_LST_MINT}, + start::{ + NewStartRebalanceIxPreAccsBuilder, StartRebalanceIxArgs, StartRebalanceIxPreAccs, + START_REBALANCE_IX_PRE_IS_SIGNER, + }, + }, + sync_sol_value::NewSyncSolValueIxPreAccsBuilder, + }, + keys::{INSTRUCTIONS_SYSVAR_ID, LST_STATE_LIST_ID, POOL_STATE_ID, REBALANCE_RECORD_ID}, + pda_onchain::{create_raw_pool_reserves_addr, POOL_STATE_SIGNER, REBALANCE_RECORD_SIGNER}, + program_err::Inf1CtlCustomProgErr, + typedefs::u8bool::U8BoolMut, + ID, +}; +use jiminy_cpi::{ + account::{Abr, Account, AccountHandle}, + program_error::{ProgramError, INVALID_ACCOUNT_DATA, NOT_ENOUGH_ACCOUNT_KEYS}, +}; +use jiminy_sysvar_instructions::Instructions; + +use inf1_core::instructions::{ + rebalance::start::StartRebalanceIxAccs, sync_sol_value::SyncSolValueIxAccs, +}; + +use sanctum_spl_token_jiminy::{ + instructions::transfer::transfer_checked_ix_account_handle_perms, + sanctum_spl_token_core::{ + instructions::transfer::{NewTransferCheckedIxAccsBuilder, TransferCheckedIxData}, + state::mint::{Mint, RawMint}, + }, +}; + +use core::mem::size_of; + +use sanctum_system_jiminy::{ + instructions::assign::assign_ix_account_handle_perms, + sanctum_system_core::{ + instructions::assign::{AssignIxData, NewAssignIxAccsBuilder}, + ID as SYSTEM_PROGRAM_ID, + }, +}; + +use crate::{ + svc::lst_sync_sol_val_unchecked, + token::get_token_account_amount, + verify::{verify_not_rebalancing_and_not_disabled, verify_pks, verify_signers}, + Cpi, +}; + +pub type StartRebalanceIxAccounts<'a, 'acc> = StartRebalanceIxAccs< + AccountHandle<'acc>, + StartRebalanceIxPreAccountHandles<'acc>, + &'a [AccountHandle<'acc>], + &'a [AccountHandle<'acc>], +>; + +/// Verify that an EndRebalance instruction exists after the current instruction with the expected destination mint +#[inline] +fn verify_end_rebalance_exists( + instructions_acc: &Account, + expected_inp_lst_mint: &[u8; 32], +) -> Result<(), ProgramError> { + let instructions = + Instructions::try_from_account(instructions_acc).ok_or(INVALID_ACCOUNT_DATA)?; + + let next_end_rebalance = instructions + .iter() + .skip(instructions.current_idx() + 1) + .find(|intro_instr| { + intro_instr.program_id() == &ID + && intro_instr.data().first().copied() == Some(END_REBALANCE_IX_DISCM) + }) + .ok_or(Inf1CtlCustomProgErr(Inf1CtlErr::NoSucceedingEndRebalance))?; + + let inp_lst_mint = next_end_rebalance + .accounts() + .get(END_REBALANCE_IX_PRE_ACCS_IDX_INP_LST_MINT) + .ok_or(Inf1CtlCustomProgErr(Inf1CtlErr::NoSucceedingEndRebalance))? + .key(); + + if inp_lst_mint != expected_inp_lst_mint { + return Err(Inf1CtlCustomProgErr(Inf1CtlErr::NoSucceedingEndRebalance).into()); + } + + Ok(()) +} + +fn start_rebalance_accs_checked<'a, 'acc>( + abr: &Abr, + accounts: &'a [AccountHandle<'acc>], + args: &StartRebalanceIxArgs, +) -> Result, ProgramError> { + let (ix_prefix, suf) = accounts + .split_first_chunk() + .ok_or(NOT_ENOUGH_ACCOUNT_KEYS)?; + let ix_prefix = StartRebalanceIxPreAccs(*ix_prefix); + + let pool = pool_state_checked(abr.get(*ix_prefix.pool_state()))?; + let list = lst_state_list_checked(abr.get(*ix_prefix.lst_state_list()))?; + + let out_lst_idx = args.out_lst_index as usize; + let out_lst_state = list + .0 + .get(out_lst_idx) + .ok_or(Inf1CtlCustomProgErr(Inf1CtlErr::InvalidLstIndex))?; + + let inp_lst_idx = args.inp_lst_index as usize; + let inp_lst_state = list + .0 + .get(inp_lst_idx) + .ok_or(Inf1CtlCustomProgErr(Inf1CtlErr::InvalidLstIndex))?; + + if inp_lst_state.is_input_disabled != 0 { + return Err(Inf1CtlCustomProgErr(Inf1CtlErr::LstInputDisabled).into()); + } + + let instructions_acc = abr.get(*ix_prefix.instructions()); + + verify_end_rebalance_exists(instructions_acc, abr.get(*ix_prefix.inp_lst_mint()).key())?; + + let out_lst_mint_acc = abr.get(*ix_prefix.out_lst_mint()); + let out_token_prog = out_lst_mint_acc.owner(); + + let inp_lst_mint_acc = abr.get(*ix_prefix.inp_lst_mint()); + let inp_token_prog = inp_lst_mint_acc.owner(); + + let [out_res, inp_res] = [ + (out_token_prog, &out_lst_state), + (inp_token_prog, &inp_lst_state), + ] + .map(|(token_prog, lst_state)| { + let Some(reserves) = create_raw_pool_reserves_addr( + token_prog, + &lst_state.mint, + &lst_state.pool_reserves_bump, + ) else { + return Err(Inf1CtlCustomProgErr(Inf1CtlErr::InvalidReserves)); + }; + Ok(reserves) + }); + + let expected_out_reserves = out_res?; + let expected_inp_reserves = inp_res?; + + let expected_pks = NewStartRebalanceIxPreAccsBuilder::start() + .with_rebalance_auth(&pool.rebalance_authority) + .with_pool_state(&POOL_STATE_ID) + .with_lst_state_list(&LST_STATE_LIST_ID) + .with_rebalance_record(&REBALANCE_RECORD_ID) + .with_out_lst_mint(&out_lst_state.mint) + .with_inp_lst_mint(&inp_lst_state.mint) + .with_out_pool_reserves(&expected_out_reserves) + .with_inp_pool_reserves(&expected_inp_reserves) + .with_instructions(&INSTRUCTIONS_SYSVAR_ID) + .with_system_program(&SYSTEM_PROGRAM_ID) + .with_out_lst_token_program(out_token_prog) + // Free account - caller can specify any destination for withdrawn tokens + .with_withdraw_to(abr.get(*ix_prefix.withdraw_to()).key()) + .build(); + verify_pks(abr, &ix_prefix.0, &expected_pks.0)?; + + verify_signers(abr, &ix_prefix.0, &START_REBALANCE_IX_PRE_IS_SIGNER.0)?; + + verify_not_rebalancing_and_not_disabled(pool)?; + + let (out_calc_all, inp_calc_all) = suf + .split_at_checked(args.out_lst_value_calc_accs.into()) + .ok_or(NOT_ENOUGH_ACCOUNT_KEYS)?; + + let [Some((out_calc_prog, out_calc)), Some((inp_calc_prog, inp_calc))] = + [out_calc_all, inp_calc_all].map(|arr| arr.split_first()) + else { + return Err(NOT_ENOUGH_ACCOUNT_KEYS.into()); + }; + + verify_pks( + abr, + &[*out_calc_prog, *inp_calc_prog], + &[ + &out_lst_state.sol_value_calculator, + &inp_lst_state.sol_value_calculator, + ], + )?; + + let out_reserves_balance = + get_token_account_amount(abr.get(*ix_prefix.out_pool_reserves()).data())?; + if out_reserves_balance < args.min_starting_out_lst { + return Err(Inf1CtlCustomProgErr(Inf1CtlErr::SlippageToleranceExceeded).into()); + } + + let inp_reserves_balance = + get_token_account_amount(abr.get(*ix_prefix.inp_pool_reserves()).data())?; + if inp_reserves_balance > args.max_starting_inp_lst { + return Err(Inf1CtlCustomProgErr(Inf1CtlErr::SlippageToleranceExceeded).into()); + } + + Ok(StartRebalanceIxAccounts { + ix_prefix, + out_calc_prog: *out_calc_prog, + out_calc, + inp_calc_prog: *inp_calc_prog, + inp_calc, + }) +} + +#[inline] +pub fn process_start_rebalance( + abr: &mut Abr, + accounts: &[AccountHandle], + args: StartRebalanceIxArgs, + cpi: &mut Cpi, +) -> Result<(), ProgramError> { + let StartRebalanceIxAccounts { + ix_prefix, + out_calc_prog, + out_calc, + inp_calc_prog, + inp_calc, + } = start_rebalance_accs_checked(abr, accounts, &args)?; + + let out_lst_idx = args.out_lst_index as usize; + let inp_lst_idx = args.inp_lst_index as usize; + + for (mint, reserves, calc_prog, calc, idx) in [ + ( + *ix_prefix.out_lst_mint(), + *ix_prefix.out_pool_reserves(), + out_calc_prog, + out_calc, + out_lst_idx, + ), + ( + *ix_prefix.inp_lst_mint(), + *ix_prefix.inp_pool_reserves(), + inp_calc_prog, + inp_calc, + inp_lst_idx, + ), + ] { + lst_sync_sol_val_unchecked( + abr, + cpi, + SyncSolValueIxAccs { + ix_prefix: NewSyncSolValueIxPreAccsBuilder::start() + .with_lst_mint(mint) + .with_pool_state(*ix_prefix.pool_state()) + .with_lst_state_list(*ix_prefix.lst_state_list()) + .with_pool_reserves(reserves) + .build(), + calc_prog, + calc, + }, + idx, + )?; + } + + let old_total_sol_value = { + let pool = pool_state_checked(abr.get(*ix_prefix.pool_state()))?; + pool.total_sol_value + }; + + // Transfer out_lst tokens from reserves to withdraw_to account. + let out_lst_mint_data = abr.get(*ix_prefix.out_lst_mint()).data(); + let out_lst_mint = RawMint::of_acc_data(out_lst_mint_data) + .and_then(Mint::try_from_raw) + .ok_or(INVALID_ACCOUNT_DATA)?; + let decimals = out_lst_mint.decimals(); + + let transfer_checked_ix_data = TransferCheckedIxData::new(args.amount, decimals); + let transfer_checked_accs = NewTransferCheckedIxAccsBuilder::start() + .with_src(*ix_prefix.out_pool_reserves()) + .with_mint(*ix_prefix.out_lst_mint()) + .with_dst(*ix_prefix.withdraw_to()) + .with_auth(*ix_prefix.pool_state()) + .build(); + let out_lst_token_program_key = *abr.get(*ix_prefix.out_lst_token_program()).key(); + + cpi.invoke_signed( + abr, + &out_lst_token_program_key, + transfer_checked_ix_data.as_buf(), + transfer_checked_ix_account_handle_perms(transfer_checked_accs), + &[POOL_STATE_SIGNER], + )?; + + lst_sync_sol_val_unchecked( + abr, + cpi, + SyncSolValueIxAccs { + ix_prefix: NewSyncSolValueIxPreAccsBuilder::start() + .with_lst_mint(*ix_prefix.out_lst_mint()) + .with_pool_state(*ix_prefix.pool_state()) + .with_lst_state_list(*ix_prefix.lst_state_list()) + .with_pool_reserves(*ix_prefix.out_pool_reserves()) + .build(), + calc_prog: out_calc_prog, + calc: out_calc, + }, + out_lst_idx, + )?; + + cpi.invoke_signed( + abr, + &SYSTEM_PROGRAM_ID, + AssignIxData::new(&ID).as_buf(), + assign_ix_account_handle_perms( + NewAssignIxAccsBuilder::start() + .with_assign(*ix_prefix.rebalance_record()) + .build(), + ), + &[REBALANCE_RECORD_SIGNER], + )?; + + abr.transfer_direct(*ix_prefix.pool_state(), *ix_prefix.rebalance_record(), 1)?; + + let rebalance_record_space = size_of::(); + abr.get_mut(*ix_prefix.rebalance_record()) + .realloc(rebalance_record_space, false)?; + + let rr = rebalance_record_checked_mut(abr.get_mut(*ix_prefix.rebalance_record()))?; + + rr.inp_lst_index = args.inp_lst_index; + rr.old_total_sol_value = old_total_sol_value; + + let pool_acc = abr.get_mut(*ix_prefix.pool_state()); + let pool = pool_state_checked_mut(pool_acc)?; + U8BoolMut(&mut pool.is_rebalancing).set_true(); + + Ok(()) +} diff --git a/controller/program/src/lib.rs b/controller/program/src/lib.rs index 1fdf42e7..87cca57a 100644 --- a/controller/program/src/lib.rs +++ b/controller/program/src/lib.rs @@ -24,6 +24,10 @@ use inf1_ctl_jiminy::instructions::{ set_protocol_fee_beneficiary::SET_PROTOCOL_FEE_BENEFICIARY_IX_DISCM, withdraw_protocol_fees::WITHDRAW_PROTOCOL_FEES_IX_DISCM, }, + rebalance::{ + end::END_REBALANCE_IX_DISCM, + start::{StartRebalanceIxData, START_REBALANCE_IX_DISCM}, + }, swap::{exact_in::SWAP_EXACT_IN_IX_DISCM, exact_out::SWAP_EXACT_OUT_IX_DISCM, IxData}, sync_sol_value::{SyncSolValueIxData, SYNC_SOL_VALUE_IX_DISCM}, }; @@ -66,6 +70,7 @@ use crate::instructions::{ }, withdraw_protocol_fee::{process_withdraw_protocol_fees, withdraw_protocol_fees_checked}, }, + rebalance::{end::process_end_rebalance, start::process_start_rebalance}, swap::{process_swap_exact_in, process_swap_exact_out}, sync_sol_value::process_sync_sol_value, }; @@ -73,6 +78,7 @@ use crate::instructions::{ mod instructions; mod pricing; mod svc; +mod token; mod utils; mod verify; @@ -231,6 +237,18 @@ fn process_ix( let accs = enable_pool_accs_checked(abr, accounts)?; process_enable_pool(abr, &accs) } + // rebalance + (&START_REBALANCE_IX_DISCM, data) => { + sol_log("StartRebalance"); + let args = StartRebalanceIxData::parse_no_discm( + data.try_into().map_err(|_e| INVALID_INSTRUCTION_DATA)?, + ); + process_start_rebalance(abr, accounts, args, cpi) + } + (&END_REBALANCE_IX_DISCM, _data) => { + sol_log("EndRebalance"); + process_end_rebalance(abr, accounts, cpi) + } _ => Err(INVALID_INSTRUCTION_DATA.into()), } } diff --git a/controller/program/src/svc.rs b/controller/program/src/svc.rs index d07f74e7..7f18341d 100644 --- a/controller/program/src/svc.rs +++ b/controller/program/src/svc.rs @@ -13,14 +13,10 @@ pub use inf1_svc_jiminy::{ }; use jiminy_cpi::{ account::{Abr, AccountHandle}, - program_error::{ProgramError, INVALID_ACCOUNT_DATA}, + program_error::ProgramError, }; -use sanctum_spl_token_jiminy::sanctum_spl_token_core::state::account::{ - RawTokenAccount, TokenAccount, -}; - -use crate::Cpi; +use crate::{token::get_token_account_amount, Cpi}; pub type SyncSolValIxAccounts<'a, 'acc> = SyncSolValueIxAccs< AccountHandle<'acc>, @@ -47,10 +43,7 @@ pub fn lst_sync_sol_val_unchecked<'acc>( let lst_mint = *ix_prefix.lst_mint(); // Sync sol value for input LST - let lst_balance = RawTokenAccount::of_acc_data(abr.get(pool_reserves).data()) - .and_then(TokenAccount::try_from_raw) - .map(|a| a.amount()) - .ok_or(INVALID_ACCOUNT_DATA)?; + let lst_balance = get_token_account_amount(abr.get(pool_reserves).data())?; let cpi_retval = cpi_lst_to_sol( cpi, abr, diff --git a/controller/program/src/token.rs b/controller/program/src/token.rs new file mode 100644 index 00000000..285f9f4c --- /dev/null +++ b/controller/program/src/token.rs @@ -0,0 +1,12 @@ +use jiminy_cpi::program_error::{ProgramError, INVALID_ACCOUNT_DATA}; +use sanctum_spl_token_jiminy::sanctum_spl_token_core::state::account::{ + RawTokenAccount, TokenAccount, +}; + +#[inline] +pub fn get_token_account_amount(token_acc_data: &[u8]) -> Result { + Ok(RawTokenAccount::of_acc_data(token_acc_data) + .and_then(TokenAccount::try_from_raw) + .map(|a| a.amount()) + .ok_or(INVALID_ACCOUNT_DATA)?) +} diff --git a/controller/program/src/verify.rs b/controller/program/src/verify.rs index 2a3bda26..9898a51e 100644 --- a/controller/program/src/verify.rs +++ b/controller/program/src/verify.rs @@ -77,6 +77,14 @@ pub fn verify_not_rebalancing_and_not_disabled(pool: &PoolState) -> Result<(), P Ok(()) } +#[inline] +pub fn verify_is_rebalancing(pool: &PoolState) -> Result<(), ProgramError> { + if !U8Bool(&pool.is_rebalancing).to_bool() { + return Err(Inf1CtlCustomProgErr(Inf1CtlErr::PoolNotRebalancing).into()); + } + Ok(()) +} + #[inline] pub fn verify_signers<'a, 'acc, const LEN: usize>( abr: &Abr, diff --git a/controller/program/tests/tests/mod.rs b/controller/program/tests/tests/mod.rs index 7ffaddfe..9506ef69 100644 --- a/controller/program/tests/tests/mod.rs +++ b/controller/program/tests/tests/mod.rs @@ -2,5 +2,6 @@ mod admin; mod disable_pool; mod liquidity; mod protocol_fee; +mod rebalance; mod swap; mod sync_sol_value; diff --git a/controller/program/tests/tests/rebalance/chain.rs b/controller/program/tests/tests/rebalance/chain.rs new file mode 100644 index 00000000..f5fca9ec --- /dev/null +++ b/controller/program/tests/tests/rebalance/chain.rs @@ -0,0 +1,923 @@ +use crate::{ + common::SVM, + tests::rebalance::test_utils::{ + add_common_accounts, fixture_lst_state_data, jupsol_wsol_builder, rebalance_ixs, + StartRebalanceKeysBuilder, + }, +}; + +use inf1_test_utils::{LstStateData, LstStateListData}; + +use inf1_core::quote::rebalance::{quote_rebalance_exact_out, RebalanceQuoteArgs}; + +use inf1_ctl_jiminy::{ + accounts::{ + pool_state::{PoolState, PoolStatePacked}, + rebalance_record::RebalanceRecord, + }, + err::Inf1CtlErr::{ + NoSucceedingEndRebalance, PoolRebalancing, PoolWouldLoseSolValue, SlippageToleranceExceeded, + }, + instructions::rebalance::end::END_REBALANCE_IX_PRE_ACCS_IDX_INP_LST_MINT, + keys::{INSTRUCTIONS_SYSVAR_ID, POOL_STATE_ID, REBALANCE_RECORD_ID}, + program_err::Inf1CtlCustomProgErr, +}; + +use inf1_svc_ag_core::inf1_svc_lido_core::solido_legacy_core::TOKENKEG_PROGRAM; + +use inf1_svc_ag_core::{ + inf1_svc_spl_core::{calc::SplCalc, sanctum_spl_stake_pool_core::StakePool}, + inf1_svc_wsol_core::calc::WsolCalc, +}; + +use inf1_test_utils::{ + acc_bef_aft, assert_balanced, assert_jiminy_prog_err, fixtures_accounts_opt_cloned, + get_token_account_amount, keys_signer_writable_to_metas, mock_instructions_sysvar, + mock_sys_acc, mock_token_acc, raw_token_acc, silence_mollusk_logs, upsert_account, + KeyedUiAccount, PkAccountTup, +}; + +use jiminy_cpi::program_error::INVALID_ARGUMENT; + +use mollusk_svm::{ + program::keyed_account_for_system_program, + result::{Check, InstructionResult, ProgramResult}, +}; + +use sanctum_spl_token_jiminy::sanctum_spl_token_core::instructions::transfer::{ + NewTransferIxAccsBuilder, TransferIxData, TRANSFER_IX_IS_SIGNER, TRANSFER_IX_IS_WRITABLE, +}; + +use solana_account::Account; +use solana_instruction::Instruction; +use solana_pubkey::Pubkey; + +use proptest::prelude::*; + +struct TestFixture { + pool: PoolState, + lsl: LstStateListData, + out_lsd: LstStateData, + inp_lsd: LstStateData, + builder: StartRebalanceKeysBuilder, + withdraw_to: [u8; 32], + out_idx: u32, + inp_idx: u32, +} + +fn setup_test_fixture() -> TestFixture { + let (pool, mut lsl, mut out_lsd, mut inp_lsd) = fixture_lst_state_data(); + let withdraw_to = Pubkey::new_unique().to_bytes(); + let builder = jupsol_wsol_builder( + pool.rebalance_authority, + out_lsd.lst_state.mint, + inp_lsd.lst_state.mint, + withdraw_to, + ); + out_lsd.lst_state.sol_value_calculator = builder.out_calc_prog; + inp_lsd.lst_state.sol_value_calculator = builder.inp_calc_prog; + let out_idx = lsl.upsert(out_lsd) as u32; + let inp_idx = lsl.upsert(inp_lsd) as u32; + + TestFixture { + pool, + lsl, + out_lsd, + inp_lsd, + builder, + withdraw_to, + out_idx, + inp_idx, + } +} + +struct OwnerAccounts { + owner: [u8; 32], + owner_token_account: [u8; 32], + owner_balance: u64, +} + +fn setup_owner_accounts(balance: u64) -> OwnerAccounts { + OwnerAccounts { + owner: Pubkey::new_unique().to_bytes(), + owner_token_account: Pubkey::new_unique().to_bytes(), + owner_balance: balance, + } +} + +fn standard_reserves(amount: u64) -> (u64, u64) { + (amount * 2, amount * 2) +} + +fn setup_basic_rebalance_test( + fixture: &TestFixture, + amount: u64, + min_starting_out_lst: u64, + max_starting_inp_lst: u64, +) -> (Vec, Vec) { + let (out_reserves, inp_reserves) = standard_reserves(amount); + let instructions = rebalance_ixs( + &fixture.builder, + fixture.out_idx, + fixture.inp_idx, + amount, + min_starting_out_lst, + max_starting_inp_lst, + ); + let owner_accs = setup_owner_accounts(0); + let accounts = setup_rebalance_transaction_accounts( + fixture, + &instructions, + out_reserves, + inp_reserves, + &owner_accs, + ); + (instructions, accounts) +} + +fn create_transfer_ix( + owner: [u8; 32], + owner_token_account: [u8; 32], + inp_pool_reserves: [u8; 32], + amount: u64, +) -> Instruction { + let transfer_ix_data = TransferIxData::new(amount); + let transfer_accs = NewTransferIxAccsBuilder::start() + .with_src(owner_token_account) + .with_dst(inp_pool_reserves) + .with_auth(owner) + .build(); + + Instruction { + program_id: Pubkey::new_from_array(TOKENKEG_PROGRAM), + accounts: keys_signer_writable_to_metas( + transfer_accs.0.iter(), + TRANSFER_IX_IS_SIGNER.0.iter(), + TRANSFER_IX_IS_WRITABLE.0.iter(), + ), + data: transfer_ix_data.as_buf().into(), + } +} + +/// Calculates the input token amount for a JupSOL -> WSOL rebalance +fn calculate_jupsol_wsol_inp_amount( + out_lst_amount: u64, + out_reserves: u64, + inp_reserves: u64, + out_mint: [u8; 32], + inp_mint: [u8; 32], +) -> u64 { + let (_, jupsol_pool_acc) = + KeyedUiAccount::from_test_fixtures_json("jupsol-pool.json").into_keyed_account(); + let jupsol_stakepool = StakePool::borsh_de(jupsol_pool_acc.data.as_slice()).unwrap(); + + let inp_calc = WsolCalc; + let out_calc = SplCalc::new(&jupsol_stakepool, 0); + + let quote = quote_rebalance_exact_out(RebalanceQuoteArgs { + amt: out_lst_amount, + inp_reserves, + out_reserves, + inp_mint, + out_mint, + inp_calc, + out_calc, + }) + .expect("quote should succeed"); + + quote.inp +} + +/// Creates the full account set required for StartRebalance → Token Transfer → EndRebalance transaction +fn setup_rebalance_transaction_accounts( + fixture: &TestFixture, + instructions: &[Instruction], + out_balance: u64, + inp_balance: u64, + owner_accs: &OwnerAccounts, +) -> Vec { + let mut accounts: Vec = + fixtures_accounts_opt_cloned(fixture.builder.keys_owned().seq().copied()).collect(); + + add_common_accounts( + &mut accounts, + &fixture.pool, + &fixture.lsl.lst_state_list, + Some(&fixture.lsl.all_pool_reserves), + fixture.pool.rebalance_authority, + fixture.out_lsd.lst_state.mint, + fixture.inp_lsd.lst_state.mint, + fixture.withdraw_to, + out_balance, + inp_balance, + ); + + upsert_account(&mut accounts, keyed_account_for_system_program()); + + upsert_account( + &mut accounts, + ( + Pubkey::new_from_array(owner_accs.owner), + mock_sys_acc(100_000_000_000), + ), + ); + + upsert_account( + &mut accounts, + ( + Pubkey::new_from_array(owner_accs.owner_token_account), + mock_token_acc(raw_token_acc( + fixture.inp_lsd.lst_state.mint, + owner_accs.owner, + owner_accs.owner_balance, + )), + ), + ); + + upsert_account( + &mut accounts, + ( + Pubkey::new_from_array(INSTRUCTIONS_SYSVAR_ID), + mock_instructions_sysvar(instructions, 0), + ), + ); + + upsert_account( + &mut accounts, + ( + Pubkey::new_from_array(REBALANCE_RECORD_ID), + Account::default(), + ), + ); + + accounts +} + +/// Creates an instruction chain with StartRebalance → Token Transfer → EndRebalance instructions. +fn build_rebalance_instruction_chain( + fixture: &TestFixture, + owner_accs: &OwnerAccounts, + out_lst_amount: u64, + owner_transfer_amount: u64, +) -> Vec { + let mut instructions = rebalance_ixs( + &fixture.builder, + fixture.out_idx, + fixture.inp_idx, + out_lst_amount, + 0, + u64::MAX, + ); + + let transfer_ix = create_transfer_ix( + owner_accs.owner, + owner_accs.owner_token_account, + fixture.inp_lsd.pool_reserves, + owner_transfer_amount, + ); + + // Put transfer ix between start and end + instructions.insert(1, transfer_ix); + + instructions +} + +fn execute_rebalance_transaction( + amount: u64, + out_reserves: Option, + inp_reserves: Option, +) -> (Vec, InstructionResult, u64) { + silence_mollusk_logs(); + + let fixture = setup_test_fixture(); + + let out_reserves = out_reserves.unwrap_or(amount * 2); + let inp_reserves = inp_reserves.unwrap_or(amount * 2); + + let owner_transfer_amount = calculate_jupsol_wsol_inp_amount( + amount, + out_reserves, + inp_reserves, + fixture.out_lsd.lst_state.mint, + fixture.inp_lsd.lst_state.mint, + ); + + let owner_accs = setup_owner_accounts(owner_transfer_amount); + + let instructions = + build_rebalance_instruction_chain(&fixture, &owner_accs, amount, owner_transfer_amount); + + let accounts = setup_rebalance_transaction_accounts( + &fixture, + &instructions, + out_reserves, + inp_reserves, + &owner_accs, + ); + + let accs_bef = accounts.clone(); + + let result = SVM.with(|svm| svm.process_instruction_chain(&instructions, &accounts)); + + // Run StartRebalance ix separately to extract old_total_sol_value + // from RebalanceRecord + let start_result = SVM.with(|svm| svm.process_instruction(&instructions[0], &accs_bef)); + let rr_aft = start_result + .resulting_accounts + .iter() + .find(|(pk, _)| pk.to_bytes() == REBALANCE_RECORD_ID) + .map(|(_, acc)| acc) + .expect("rebalance record after start"); + let rebalance_record = + unsafe { RebalanceRecord::of_acc_data(&rr_aft.data) }.expect("rebalance record"); + + (accs_bef, result, rebalance_record.old_total_sol_value) +} + +/// Validate that the transaction succeeded, +/// the pool state is not rebalancing before or after, +/// the pool did not lose SOL value, +/// the RebalanceRecord is properly closed, +/// and lamports are balanced. +fn assert_rebalance_transaction_success( + accs_bef: &[PkAccountTup], + result: &InstructionResult, + old_total_sol_value: u64, +) { + assert_eq!(result.program_result, ProgramResult::Success); + + let [pool_state_bef, pool_state_aft] = acc_bef_aft( + &Pubkey::new_from_array(POOL_STATE_ID), + accs_bef, + &result.resulting_accounts, + ) + .map(|a| { + PoolStatePacked::of_acc_data(&a.data) + .unwrap() + .into_pool_state() + }); + + assert_eq!(pool_state_bef.is_rebalancing, 0); + assert_eq!(pool_state_aft.is_rebalancing, 0); + assert!(pool_state_aft.total_sol_value >= old_total_sol_value); + + let rr_aft = result + .resulting_accounts + .iter() + .find(|(pk, _)| pk.to_bytes() == REBALANCE_RECORD_ID); + assert_eq!(rr_aft.unwrap().1.lamports, 0); + + assert_balanced(accs_bef, &result.resulting_accounts); +} + +#[test] +fn rebalance_transaction_success() { + let (accs_bef, result, old_total_sol_value) = + execute_rebalance_transaction(100_000, None, None); + + assert_rebalance_transaction_success(&accs_bef, &result, old_total_sol_value); + + // Filter out executable accounts (programs and sysvars) which are not + // relevant for rent-exemption checks - only user accounts matter + let mut result_for_check = result.clone(); + result_for_check + .resulting_accounts + .retain(|(_, acc)| !acc.executable); + + // Assert all non-executable accounts are rent-exempt after transaction + SVM.with(|svm| { + assert!(result_for_check.run_checks(&[Check::all_rent_exempt()], &svm.config, svm)); + }); +} + +#[test] +fn missing_end_rebalance() { + silence_mollusk_logs(); + + let fixture = setup_test_fixture(); + let (out_reserves, inp_reserves) = standard_reserves(100_000); + + let mut instructions = rebalance_ixs( + &fixture.builder, + fixture.out_idx, + fixture.inp_idx, + 100_000, + 0, + u64::MAX, + ); + // Remove EndRebalance + instructions.pop(); + + let owner_accs = setup_owner_accounts(0); + + let accounts = setup_rebalance_transaction_accounts( + &fixture, + &instructions, + out_reserves, + inp_reserves, + &owner_accs, + ); + + let result = SVM.with(|svm| svm.process_instruction(&instructions[0], &accounts)); + + assert_jiminy_prog_err( + &result.program_result, + Inf1CtlCustomProgErr(NoSucceedingEndRebalance), + ); +} + +#[test] +fn wrong_end_mint() { + silence_mollusk_logs(); + + let fixture = setup_test_fixture(); + let (out_reserves, inp_reserves) = standard_reserves(100_000); + + let mut instructions = rebalance_ixs( + &fixture.builder, + fixture.out_idx, + fixture.inp_idx, + 100_000, + 0, + u64::MAX, + ); + + // Change EndRebalance instruction to use wrong inp_lst_mint + if let Some(end_ix) = instructions.get_mut(1) { + if end_ix.accounts.len() > END_REBALANCE_IX_PRE_ACCS_IDX_INP_LST_MINT { + end_ix.accounts[END_REBALANCE_IX_PRE_ACCS_IDX_INP_LST_MINT].pubkey = + Pubkey::new_unique(); + } + } + + let owner_accs = setup_owner_accounts(0); + + let accounts = setup_rebalance_transaction_accounts( + &fixture, + &instructions, + out_reserves, + inp_reserves, + &owner_accs, + ); + + let result = SVM.with(|svm| svm.process_instruction(&instructions[0], &accounts)); + + assert_jiminy_prog_err( + &result.program_result, + Inf1CtlCustomProgErr(NoSucceedingEndRebalance), + ); +} + +#[test] +fn no_transfer() { + silence_mollusk_logs(); + + let fixture = setup_test_fixture(); + let (instructions, accounts) = setup_basic_rebalance_test(&fixture, 100_000, 0, u64::MAX); + + let result = SVM.with(|svm| svm.process_instruction_chain(&instructions, &accounts)); + + assert_jiminy_prog_err( + &result.program_result, + Inf1CtlCustomProgErr(PoolWouldLoseSolValue), + ); +} + +#[test] +fn insufficient_transfer() { + silence_mollusk_logs(); + + let amount = 100_000; + let fixture = setup_test_fixture(); + let (out_reserves, inp_reserves) = standard_reserves(amount); + + let required_amount = calculate_jupsol_wsol_inp_amount( + amount, + out_reserves, + inp_reserves, + fixture.out_lsd.lst_state.mint, + fixture.inp_lsd.lst_state.mint, + ); + + let insufficient_amount = required_amount / 2; + let owner_accs = setup_owner_accounts(insufficient_amount); + + let instructions = + build_rebalance_instruction_chain(&fixture, &owner_accs, amount, insufficient_amount); + + let accounts = setup_rebalance_transaction_accounts( + &fixture, + &instructions, + out_reserves, + inp_reserves, + &owner_accs, + ); + + let result = SVM.with(|svm| svm.process_instruction_chain(&instructions, &accounts)); + + assert_jiminy_prog_err( + &result.program_result, + Inf1CtlCustomProgErr(PoolWouldLoseSolValue), + ); +} + +#[test] +fn slippage_min_out_violated() { + silence_mollusk_logs(); + + let fixture = setup_test_fixture(); + + let (instructions, accounts) = + setup_basic_rebalance_test(&fixture, 100_000, u64::MAX, u64::MAX); + + let result = SVM.with(|svm| svm.process_instruction(&instructions[0], &accounts)); + + assert_jiminy_prog_err( + &result.program_result, + Inf1CtlCustomProgErr(SlippageToleranceExceeded), + ); +} + +#[test] +fn slippage_max_inp_violated() { + silence_mollusk_logs(); + + let fixture = setup_test_fixture(); + + let (instructions, accounts) = setup_basic_rebalance_test(&fixture, 100_000, 0, 1); + + let result = SVM.with(|svm| svm.process_instruction(&instructions[0], &accounts)); + + assert_jiminy_prog_err( + &result.program_result, + Inf1CtlCustomProgErr(SlippageToleranceExceeded), + ); +} + +#[test] +fn multi_instruction_transfer_chain() { + silence_mollusk_logs(); + + let amount = 100_000; + let fixture = setup_test_fixture(); + let (out_reserves, inp_reserves) = standard_reserves(amount); + + let total_transfer = calculate_jupsol_wsol_inp_amount( + amount, + out_reserves, + inp_reserves, + fixture.out_lsd.lst_state.mint, + fixture.inp_lsd.lst_state.mint, + ); + + let owner_accs = setup_owner_accounts(total_transfer); + + let mut instructions = rebalance_ixs( + &fixture.builder, + fixture.out_idx, + fixture.inp_idx, + amount, + 0, + u64::MAX, + ); + + let transfer1 = total_transfer / 3; + let transfer2 = total_transfer / 3; + let transfer3 = total_transfer - transfer1 - transfer2; + + let transfer_ix1 = create_transfer_ix( + owner_accs.owner, + owner_accs.owner_token_account, + fixture.inp_lsd.pool_reserves, + transfer1, + ); + let transfer_ix2 = create_transfer_ix( + owner_accs.owner, + owner_accs.owner_token_account, + fixture.inp_lsd.pool_reserves, + transfer2, + ); + let transfer_ix3 = create_transfer_ix( + owner_accs.owner, + owner_accs.owner_token_account, + fixture.inp_lsd.pool_reserves, + transfer3, + ); + + instructions.insert(1, transfer_ix1); + instructions.insert(2, transfer_ix2); + instructions.insert(3, transfer_ix3); + + let accounts = setup_rebalance_transaction_accounts( + &fixture, + &instructions, + out_reserves, + inp_reserves, + &owner_accs, + ); + + let accs_bef = accounts.clone(); + let result = SVM.with(|svm| svm.process_instruction_chain(&instructions, &accounts)); + + let start_result = SVM.with(|svm| svm.process_instruction(&instructions[0], &accs_bef)); + let rr_aft = start_result + .resulting_accounts + .iter() + .find(|(pk, _)| pk.to_bytes() == REBALANCE_RECORD_ID) + .map(|(_, acc)| acc) + .expect("rebalance record after start"); + let rebalance_record = + unsafe { RebalanceRecord::of_acc_data(&rr_aft.data) }.expect("rebalance record"); + + assert_rebalance_transaction_success(&accs_bef, &result, rebalance_record.old_total_sol_value); +} + +#[test] +fn rebalance_chain_updates_reserves_correctly() { + silence_mollusk_logs(); + + let amount = 100_000; + let fixture = setup_test_fixture(); + let (out_reserves, inp_reserves) = standard_reserves(amount); + + let transfer_amount = calculate_jupsol_wsol_inp_amount( + amount, + out_reserves, + inp_reserves, + fixture.out_lsd.lst_state.mint, + fixture.inp_lsd.lst_state.mint, + ); + + let owner_accs = setup_owner_accounts(transfer_amount); + + let instructions = + build_rebalance_instruction_chain(&fixture, &owner_accs, amount, transfer_amount); + + let accounts = setup_rebalance_transaction_accounts( + &fixture, + &instructions, + out_reserves, + inp_reserves, + &owner_accs, + ); + + let accs_bef = accounts.clone(); + + let result = SVM.with(|svm| svm.process_instruction_chain(&instructions, &accounts)); + + assert_eq!(result.program_result, ProgramResult::Success); + + let [out_reserves_bef, out_reserves_aft] = acc_bef_aft( + &Pubkey::new_from_array(fixture.out_lsd.pool_reserves), + &accs_bef, + &result.resulting_accounts, + ) + .map(|a| get_token_account_amount(&a.data)); + + let [inp_reserves_bef, inp_reserves_aft] = acc_bef_aft( + &Pubkey::new_from_array(fixture.inp_lsd.pool_reserves), + &accs_bef, + &result.resulting_accounts, + ) + .map(|a| get_token_account_amount(&a.data)); + + let [withdraw_to_bef, withdraw_to_aft] = acc_bef_aft( + &Pubkey::new_from_array(fixture.withdraw_to), + &accs_bef, + &result.resulting_accounts, + ) + .map(|a| get_token_account_amount(&a.data)); + + assert_eq!( + out_reserves_aft, + out_reserves_bef - amount, + "out reserves should decrease by withdrawal amount" + ); + assert_eq!( + inp_reserves_aft, + inp_reserves_bef + transfer_amount, + "inp reserves should increase by transfer amount" + ); + assert_eq!( + withdraw_to_aft, + withdraw_to_bef + amount, + "withdraw_to should receive withdrawn LST" + ); + + assert_balanced(&accs_bef, &result.resulting_accounts); +} + +#[test] +fn rebalance_record_lifecycle() { + silence_mollusk_logs(); + + let amount = 100_000; + + let (accs_bef, result, old_total_sol_value) = execute_rebalance_transaction(amount, None, None); + + assert_eq!(result.program_result, ProgramResult::Success); + + let [pool_state_bef, pool_state_aft] = acc_bef_aft( + &Pubkey::new_from_array(POOL_STATE_ID), + &accs_bef, + &result.resulting_accounts, + ) + .map(|a| { + PoolStatePacked::of_acc_data(&a.data) + .expect("pool state") + .into_pool_state() + }); + + assert_eq!(pool_state_bef.is_rebalancing, 0); + + let rr_bef = accs_bef + .iter() + .find(|(pk, _)| pk.to_bytes() == REBALANCE_RECORD_ID); + assert_eq!( + rr_bef.map(|(_, acc)| acc.lamports).unwrap(), + 0, + "rebalance record should not exist initially" + ); + + assert_eq!(pool_state_bef.is_rebalancing, 0); + assert_eq!(pool_state_aft.is_rebalancing, 0); + + assert!(pool_state_aft.total_sol_value >= old_total_sol_value); + + let rr_aft = result + .resulting_accounts + .iter() + .find(|(pk, _)| pk.to_bytes() == REBALANCE_RECORD_ID); + assert_eq!(rr_aft.unwrap().1.lamports, 0); + + // Verify RebalanceRecord creation by executing just StartRebalance + let fixture2 = setup_test_fixture(); + let (start_ixs, start_accounts) = setup_basic_rebalance_test(&fixture2, amount, 0, u64::MAX); + + let start_result = SVM.with(|svm| svm.process_instruction(&start_ixs[0], &start_accounts)); + assert_eq!(start_result.program_result, ProgramResult::Success); + + let [pool_state_bef, pool_state_aft] = acc_bef_aft( + &Pubkey::new_from_array(POOL_STATE_ID), + &start_accounts, + &start_result.resulting_accounts, + ) + .map(|a| { + PoolStatePacked::of_acc_data(&a.data) + .expect("pool state") + .into_pool_state() + }); + + assert_eq!(pool_state_bef.is_rebalancing, 0); + assert_eq!(pool_state_aft.is_rebalancing, 1); + + let rr_aft = start_result + .resulting_accounts + .iter() + .find(|(pk, _)| pk.to_bytes() == REBALANCE_RECORD_ID) + .map(|(_, acc)| acc) + .expect("rebalance record after start"); + + assert!(rr_aft.lamports > 0); + + let rebalance_record = + unsafe { RebalanceRecord::of_acc_data(&rr_aft.data) }.expect("rebalance record"); + + assert_eq!(rebalance_record.inp_lst_index, fixture2.inp_idx); + + assert!(rebalance_record.old_total_sol_value > 0); + + assert_balanced(&accs_bef, &result.resulting_accounts); +} + +#[test] +fn pool_already_rebalancing() { + silence_mollusk_logs(); + + let fixture = setup_test_fixture(); + let owner_accs = setup_owner_accounts(0); + let (out_reserves, inp_reserves) = standard_reserves(100_000); + + let first_instructions = rebalance_ixs( + &fixture.builder, + fixture.out_idx, + fixture.inp_idx, + 100_000, + 0, + u64::MAX, + ); + + let accounts = setup_rebalance_transaction_accounts( + &fixture, + &first_instructions, + out_reserves, + inp_reserves, + &owner_accs, + ); + + // Execute first StartRebalance instruction to set pool.is_rebalancing = 1 + let result = SVM.with(|svm| svm.process_instruction(&first_instructions[0], &accounts)); + assert_eq!(result.program_result, ProgramResult::Success); + + let pool_state_aft = result + .resulting_accounts + .iter() + .find(|(pk, _)| pk.to_bytes() == POOL_STATE_ID) + .map(|(_, acc)| { + PoolStatePacked::of_acc_data(&acc.data) + .expect("pool state") + .into_pool_state() + }) + .expect("pool state"); + assert_eq!(pool_state_aft.is_rebalancing, 1); + + let second_instructions = rebalance_ixs( + &fixture.builder, + fixture.out_idx, + fixture.inp_idx, + 100_000, + 0, + u64::MAX, + ); + + let mut accounts_with_second_ix = result.resulting_accounts.clone(); + upsert_account( + &mut accounts_with_second_ix, + ( + Pubkey::new_from_array(INSTRUCTIONS_SYSVAR_ID), + mock_instructions_sysvar(&second_instructions, 0), + ), + ); + + // Execute another StartRebalance instruction + let result2 = + SVM.with(|svm| svm.process_instruction(&second_instructions[0], &accounts_with_second_ix)); + + assert_jiminy_prog_err( + &result2.program_result, + Inf1CtlCustomProgErr(PoolRebalancing), + ); +} + +#[test] +fn unauthorized_rebalance_authority() { + silence_mollusk_logs(); + + let fixture = setup_test_fixture(); + let owner_accs = setup_owner_accounts(0); + let (out_reserves, inp_reserves) = standard_reserves(100_000); + + let unauthorized_pk = Pubkey::new_unique().to_bytes(); + let unauthorized_builder = jupsol_wsol_builder( + unauthorized_pk, + fixture.out_lsd.lst_state.mint, + fixture.inp_lsd.lst_state.mint, + fixture.withdraw_to, + ); + + let instructions = rebalance_ixs( + &unauthorized_builder, + fixture.out_idx, + fixture.inp_idx, + 100_000, + 0, + u64::MAX, + ); + + let mut accounts = setup_rebalance_transaction_accounts( + &fixture, + &instructions, + out_reserves, + inp_reserves, + &owner_accs, + ); + + upsert_account( + &mut accounts, + ( + Pubkey::new_from_array(unauthorized_pk), + mock_sys_acc(100_000_000_000), + ), + ); + + let result = SVM.with(|svm| svm.process_instruction_chain(&instructions, &accounts)); + + assert_jiminy_prog_err(&result.program_result, INVALID_ARGUMENT); +} + +proptest! { + #[test] + fn rebalance_transaction_various_amounts_any( + amount in 1u64..=1_000_000_000, + out_reserve_multiplier in 2u64..=100, + inp_reserve_multiplier in 2u64..=100, + ) { + let out_reserves = amount.saturating_mul(out_reserve_multiplier); + let inp_reserves = amount.saturating_mul(inp_reserve_multiplier); + + let (accs_bef, result, old_total_sol_value) = + execute_rebalance_transaction(amount, Some(out_reserves), Some(inp_reserves)); + + assert_rebalance_transaction_success(&accs_bef, &result, old_total_sol_value); + } +} diff --git a/controller/program/tests/tests/rebalance/mod.rs b/controller/program/tests/tests/rebalance/mod.rs new file mode 100644 index 00000000..189f791c --- /dev/null +++ b/controller/program/tests/tests/rebalance/mod.rs @@ -0,0 +1,2 @@ +mod chain; +mod test_utils; diff --git a/controller/program/tests/tests/rebalance/test_utils.rs b/controller/program/tests/tests/rebalance/test_utils.rs new file mode 100644 index 00000000..8c27c75d --- /dev/null +++ b/controller/program/tests/tests/rebalance/test_utils.rs @@ -0,0 +1,291 @@ +use std::collections::HashMap; + +use inf1_core::instructions::rebalance::start::StartRebalanceIxAccs; +use inf1_ctl_jiminy::{ + accounts::{ + lst_state_list::LstStatePackedList, + pool_state::{PoolState, PoolStatePacked}, + }, + instructions::rebalance::{ + end::EndRebalanceIxData, + start::{ + NewStartRebalanceIxPreAccsBuilder, StartRebalanceIxData, StartRebalanceIxPreKeysOwned, + }, + }, + keys::{INSTRUCTIONS_SYSVAR_ID, LST_STATE_LIST_ID, POOL_STATE_ID, REBALANCE_RECORD_ID}, + ID, +}; +use inf1_std::instructions::rebalance::{end::EndRebalanceIxAccs, start::StartRebalanceIxArgs}; +use inf1_svc_ag_core::{ + inf1_svc_lido_core::solido_legacy_core::TOKENKEG_PROGRAM, + inf1_svc_wsol_core::instructions::sol_val_calc::WsolCalcAccs, instructions::SvcCalcAccsAg, + SvcAgTy, +}; +use inf1_test_utils::{ + gen_lst_state, keys_signer_writable_to_metas, lst_state_list_account, mock_mint, + mock_token_acc, pool_state_account, raw_mint, raw_token_acc, u8_to_bool, upsert_account, + GenLstStateArgs, LstStateData, LstStateListData, NewLstStateBumpsBuilder, + NewLstStatePksBuilder, PkAccountTup, ALL_FIXTURES, JUPSOL_FIXTURE_LST_IDX, WSOL_MINT, +}; +use sanctum_system_jiminy::sanctum_system_core::ID as SYSTEM_PROGRAM_ID; +use solana_account::Account; +use solana_instruction::Instruction; +use solana_pubkey::Pubkey; + +use crate::common::jupsol_fixtures_svc_suf; + +pub fn fixture_pool_and_lsl() -> (PoolState, Vec) { + let pool_pk = Pubkey::new_from_array(POOL_STATE_ID); + let pool_acc = ALL_FIXTURES + .get(&pool_pk) + .expect("missing pool state fixture"); + let pool = PoolStatePacked::of_acc_data(&pool_acc.data) + .expect("pool packed") + .into_pool_state(); + + let lsl_pk = Pubkey::new_from_array(LST_STATE_LIST_ID); + let lsl_acc = ALL_FIXTURES.get(&lsl_pk).expect("missing lsl fixture"); + + (pool, lsl_acc.data.clone()) +} + +pub type StartRebalanceKeysBuilder = + StartRebalanceIxAccs<[u8; 32], StartRebalanceIxPreKeysOwned, SvcCalcAccsAg, SvcCalcAccsAg>; + +pub fn start_rebalance_ix_pre_keys_owned( + rebalance_auth: [u8; 32], + out_token_program: &[u8; 32], + out_mint: [u8; 32], + inp_mint: [u8; 32], + withdraw_to: [u8; 32], +) -> StartRebalanceIxPreKeysOwned { + let rebalance_record_pda = Pubkey::new_from_array(REBALANCE_RECORD_ID); + + NewStartRebalanceIxPreAccsBuilder::start() + .with_rebalance_auth(rebalance_auth) + .with_pool_state(POOL_STATE_ID) + .with_lst_state_list(LST_STATE_LIST_ID) + .with_rebalance_record(rebalance_record_pda.to_bytes()) + .with_out_lst_mint(out_mint) + .with_inp_lst_mint(inp_mint) + .with_out_pool_reserves( + inf1_test_utils::find_pool_reserves_ata(out_token_program, &out_mint) + .0 + .to_bytes(), + ) + .with_inp_pool_reserves( + inf1_test_utils::find_pool_reserves_ata(out_token_program, &inp_mint) + .0 + .to_bytes(), + ) + .with_withdraw_to(withdraw_to) + .with_instructions(INSTRUCTIONS_SYSVAR_ID) + .with_system_program(SYSTEM_PROGRAM_ID) + .with_out_lst_token_program(*out_token_program) + .build() +} + +pub fn rebalance_ixs( + builder: &StartRebalanceKeysBuilder, + out_lst_index: u32, + inp_lst_index: u32, + amount: u64, + min_starting_out_lst: u64, + max_starting_inp_lst: u64, +) -> Vec { + let start_args = StartRebalanceIxArgs { + out_lst_index, + inp_lst_index, + amount, + min_starting_out_lst, + max_starting_inp_lst, + accs: *builder, + }; + + let start_ix = Instruction { + program_id: Pubkey::new_from_array(ID), + accounts: keys_signer_writable_to_metas( + builder.keys_owned().seq(), + builder.is_signer().seq(), + builder.is_writer().seq(), + ), + data: StartRebalanceIxData::new(start_args.to_full()) + .as_buf() + .into(), + }; + + let end_accs = EndRebalanceIxAccs::from_start(*builder); + let end_ix = Instruction { + program_id: Pubkey::new_from_array(ID), + accounts: keys_signer_writable_to_metas( + end_accs.keys_owned().seq(), + end_accs.is_signer().seq(), + end_accs.is_writer().seq(), + ), + data: EndRebalanceIxData::as_buf().into(), + }; + + vec![start_ix, end_ix] +} + +pub fn jupsol_wsol_builder( + rebalance_auth: [u8; 32], + out_mint: [u8; 32], + inp_mint: [u8; 32], + withdraw_to: [u8; 32], +) -> StartRebalanceKeysBuilder { + let ix_prefix = start_rebalance_ix_pre_keys_owned( + rebalance_auth, + &TOKENKEG_PROGRAM, + out_mint, + inp_mint, + withdraw_to, + ); + + StartRebalanceKeysBuilder { + ix_prefix, + out_calc_prog: *SvcAgTy::SanctumSplMulti(()).svc_program_id(), + out_calc: jupsol_fixtures_svc_suf(), + inp_calc_prog: *SvcAgTy::Wsol(()).svc_program_id(), + inp_calc: SvcCalcAccsAg::Wsol(WsolCalcAccs), + } +} + +pub fn fixture_lst_state_data() -> (PoolState, LstStateListData, LstStateData, LstStateData) { + let (pool, lst_state_bytes) = fixture_pool_and_lsl(); + + let packed_list = LstStatePackedList::of_acc_data(&lst_state_bytes).expect("lst packed"); + let packed_states = &packed_list.0; + + let mut out_state = packed_states[JUPSOL_FIXTURE_LST_IDX].into_lst_state(); + out_state.sol_value_calculator = *SvcAgTy::Wsol(()).svc_program_id(); + + let mut inp_state = packed_states + .iter() + .find(|s| s.into_lst_state().mint == WSOL_MINT.to_bytes()) + .expect("wsol fixture available") + .into_lst_state(); + inp_state.sol_value_calculator = *SvcAgTy::Wsol(()).svc_program_id(); + + let out_lsd = gen_lst_state( + GenLstStateArgs { + is_input_disabled: u8_to_bool(out_state.is_input_disabled), + sol_value: out_state.sol_value, + pks: NewLstStatePksBuilder::start() + .with_mint(out_state.mint) + .with_sol_value_calculator(out_state.sol_value_calculator) + .build(), + bumps: NewLstStateBumpsBuilder::start() + .with_pool_reserves_bump(out_state.pool_reserves_bump) + .with_protocol_fee_accumulator_bump(out_state.protocol_fee_accumulator_bump) + .build(), + }, + &TOKENKEG_PROGRAM, + ); + + let inp_lsd = gen_lst_state( + GenLstStateArgs { + is_input_disabled: u8_to_bool(inp_state.is_input_disabled), + sol_value: inp_state.sol_value, + pks: NewLstStatePksBuilder::start() + .with_mint(inp_state.mint) + .with_sol_value_calculator(inp_state.sol_value_calculator) + .build(), + bumps: NewLstStateBumpsBuilder::start() + .with_pool_reserves_bump(inp_state.pool_reserves_bump) + .with_protocol_fee_accumulator_bump(inp_state.protocol_fee_accumulator_bump) + .build(), + }, + &TOKENKEG_PROGRAM, + ); + + let mut lsl_data = LstStateListData { + lst_state_list: lst_state_bytes, + protocol_fee_accumulators: HashMap::new(), + all_pool_reserves: HashMap::new(), + }; + + lsl_data.upsert(out_lsd); + lsl_data.upsert(inp_lsd); + + (pool, lsl_data, out_lsd, inp_lsd) +} + +#[allow(clippy::too_many_arguments)] +pub fn add_common_accounts( + accounts: &mut Vec, + pool: &PoolState, + lst_state_list: &[u8], + pool_reserves_map: Option<&HashMap<[u8; 32], [u8; 32]>>, + rebalance_auth: [u8; 32], + out_mint: [u8; 32], + inp_mint: [u8; 32], + withdraw_to: [u8; 32], + out_balance: u64, + inp_balance: u64, +) { + upsert_account( + accounts, + ( + LST_STATE_LIST_ID.into(), + lst_state_list_account(lst_state_list.to_vec()), + ), + ); + upsert_account(accounts, (POOL_STATE_ID.into(), pool_state_account(*pool))); + upsert_account( + accounts, + ( + Pubkey::new_from_array(rebalance_auth), + Account { + lamports: u64::MAX, + owner: Pubkey::new_from_array(SYSTEM_PROGRAM_ID), + ..Default::default() + }, + ), + ); + upsert_account( + accounts, + ( + Pubkey::new_from_array(out_mint), + mock_mint(raw_mint(None, None, 0, 9)), + ), + ); + upsert_account( + accounts, + ( + Pubkey::new_from_array(inp_mint), + mock_mint(raw_mint(None, None, 0, 9)), + ), + ); + upsert_account( + accounts, + ( + pool_reserves_map + .and_then(|m| m.get(&out_mint).copied()) + .map(Pubkey::new_from_array) + .unwrap_or_else(|| { + inf1_test_utils::find_pool_reserves_ata(&TOKENKEG_PROGRAM, &out_mint).0 + }), + mock_token_acc(raw_token_acc(out_mint, POOL_STATE_ID, out_balance)), + ), + ); + upsert_account( + accounts, + ( + pool_reserves_map + .and_then(|m| m.get(&inp_mint).copied()) + .map(Pubkey::new_from_array) + .unwrap_or_else(|| { + inf1_test_utils::find_pool_reserves_ata(&TOKENKEG_PROGRAM, &inp_mint).0 + }), + mock_token_acc(raw_token_acc(inp_mint, POOL_STATE_ID, inp_balance)), + ), + ); + upsert_account( + accounts, + ( + Pubkey::new_from_array(withdraw_to), + mock_token_acc(raw_token_acc(out_mint, withdraw_to, 0)), + ), + ); +} diff --git a/test-utils/Cargo.toml b/test-utils/Cargo.toml index 2d48d897..f5604c91 100644 --- a/test-utils/Cargo.toml +++ b/test-utils/Cargo.toml @@ -16,6 +16,7 @@ inf1-svc-marinade-core = { workspace = true } inf1-svc-spl-core = { workspace = true } inf1-svc-wsol-core = { workspace = true } jiminy-program-error = { workspace = true } +jiminy-sysvar-instructions = { workspace = true } jiminy-sysvar-rent = { workspace = true } lazy_static = { workspace = true } mollusk-svm = { workspace = true } @@ -29,7 +30,9 @@ serde_json = { workspace = true } solana-account = { workspace = true } solana-account-decoder-client-types = { workspace = true } solana-instruction = { workspace = true } +solana-instructions-sysvar = { workspace = true } solana-logger = { workspace = true } solana-program-error = { workspace = true } solana-pubkey = { workspace = true } +solana-sdk = { workspace = true } solido-legacy-core = { workspace = true } diff --git a/test-utils/src/accounts/sysvars.rs b/test-utils/src/accounts/sysvars.rs index 07b76148..806e254b 100644 --- a/test-utils/src/accounts/sysvars.rs +++ b/test-utils/src/accounts/sysvars.rs @@ -1,4 +1,7 @@ +use jiminy_sysvar_instructions::sysvar::OWNER_ID; use solana_account::Account; +use solana_instruction::{BorrowedAccountMeta, BorrowedInstruction, Instruction}; +use solana_instructions_sysvar::construct_instructions_data; use solana_pubkey::Pubkey; /// Clock with everything = 0 @@ -12,3 +15,35 @@ pub fn mock_clock() -> Account { rent_epoch: u64::MAX, } } + +pub fn mock_instructions_sysvar(instructions: &[Instruction], curr_idx: u16) -> Account { + let mut data = construct_instructions_data( + instructions + .iter() + .map(|instruction| BorrowedInstruction { + program_id: &instruction.program_id, + accounts: instruction + .accounts + .iter() + .map(|meta| BorrowedAccountMeta { + pubkey: &meta.pubkey, + is_signer: meta.is_signer, + is_writable: meta.is_writable, + }) + .collect(), + data: &instruction.data, + }) + .collect::>() + .as_slice(), + ); + + *data.split_last_chunk_mut().unwrap().1 = curr_idx.to_le_bytes(); + + Account { + data, + owner: Pubkey::new_from_array(OWNER_ID), + lamports: 10_000_000, + executable: false, + rent_epoch: 0, + } +} diff --git a/test-utils/src/accounts/token.rs b/test-utils/src/accounts/token.rs index d1052cbd..d3543125 100644 --- a/test-utils/src/accounts/token.rs +++ b/test-utils/src/accounts/token.rs @@ -2,7 +2,10 @@ //! sanctum-spl-token repo use jiminy_sysvar_rent::Rent; -use sanctum_spl_token_core::state::{account::RawTokenAccount, mint::RawMint}; +use sanctum_spl_token_core::state::{ + account::{RawTokenAccount, TokenAccount}, + mint::RawMint, +}; use solana_account::Account; use solido_legacy_core::TOKENKEG_PROGRAM; @@ -94,3 +97,10 @@ pub fn mock_mint_with_prog(a: RawMint, token_prog: [u8; 32]) -> Account { pub fn mock_mint(a: RawMint) -> Account { mock_mint_with_prog(a, TOKENKEG_PROGRAM) } + +pub fn get_token_account_amount(token_acc_data: &[u8]) -> u64 { + RawTokenAccount::of_acc_data(token_acc_data) + .and_then(TokenAccount::try_from_raw) + .expect("valid token account") + .amount() +} diff --git a/test-utils/src/diff/mod.rs b/test-utils/src/diff/mod.rs index c032ecfb..a78e55b2 100644 --- a/test-utils/src/diff/mod.rs +++ b/test-utils/src/diff/mod.rs @@ -13,11 +13,14 @@ pub enum Diff { /// where old value != new value StrictChanged(T, T), + /// assert that new value is >= the given minimum + GreaterOrEqual(T), + /// no-op, dont care Pass, } -impl Diff { +impl Diff { /// # Panics /// - if difference is not same as `self` #[inline] @@ -41,6 +44,10 @@ impl Diff { assert!(old != new, "Expected old != new but got both {old:#?}"); } } + Diff::GreaterOrEqual(min) => assert!( + new >= min, + "Expected new value to be >= {min:#?} but got {new:#?}" + ), Diff::Pass => (), } } diff --git a/test-utils/src/mollusk.rs b/test-utils/src/mollusk.rs index c99d83f5..e45eb312 100644 --- a/test-utils/src/mollusk.rs +++ b/test-utils/src/mollusk.rs @@ -170,3 +170,17 @@ pub fn assert_jiminy_prog_err>(program_result: &ProgramRes } } } + +pub fn assert_balanced(bef: &[PkAccountTup], aft: &[PkAccountTup]) { + // Filter out executable accounts (programs and sysvars) which are not + // relevant for lamport balance checks - only user accounts matter + let [lamports_bef, lamports_aft] = [bef, aft].map(|accounts| { + accounts + .iter() + .filter(|(_, acc)| !acc.executable) + .map(|(_, acc)| acc.lamports as u128) + .sum::() + }); + + assert_eq!(lamports_bef, lamports_aft, "lamports not balanced"); +}