diff --git a/Cargo.lock b/Cargo.lock index ae06ca2e..df25d6b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1679,12 +1679,13 @@ checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" [[package]] name = "drift-mocks" version = "0.1.0" -source = "git+https://github.com/mrgnlabs/marginfi-v2?tag=mrgn-0.1.8-rc3#ef7768874ae362a0a20cd6737781941a5b6cda6b" +source = "git+https://github.com/mrgnlabs/marginfi-v2?rev=4ce8a15e70b4108059b74e1aedbfde1e3e564ace#4ce8a15e70b4108059b74e1aedbfde1e3e564ace" dependencies = [ "anchor-lang", "bytemuck", "fixed", "fixed-macro", + "marginfi-type-crate", "static_assertions", ] @@ -2805,7 +2806,7 @@ dependencies = [ [[package]] name = "id-crate" version = "0.1.0" -source = "git+https://github.com/mrgnlabs/marginfi-v2?tag=mrgn-0.1.8-rc3#ef7768874ae362a0a20cd6737781941a5b6cda6b" +source = "git+https://github.com/mrgnlabs/marginfi-v2?rev=4ce8a15e70b4108059b74e1aedbfde1e3e564ace#4ce8a15e70b4108059b74e1aedbfde1e3e564ace" dependencies = [ "anchor-lang", "cfg-if", @@ -3079,7 +3080,7 @@ dependencies = [ [[package]] name = "juplend-mocks" version = "0.1.0" -source = "git+https://github.com/mrgnlabs/marginfi-v2?tag=mrgn-0.1.8-rc3#ef7768874ae362a0a20cd6737781941a5b6cda6b" +source = "git+https://github.com/mrgnlabs/marginfi-v2?rev=4ce8a15e70b4108059b74e1aedbfde1e3e564ace#4ce8a15e70b4108059b74e1aedbfde1e3e564ace" dependencies = [ "anchor-lang", "bytemuck", @@ -3088,7 +3089,7 @@ dependencies = [ [[package]] name = "kamino-mocks" version = "0.1.0" -source = "git+https://github.com/mrgnlabs/marginfi-v2?tag=mrgn-0.1.8-rc3#ef7768874ae362a0a20cd6737781941a5b6cda6b" +source = "git+https://github.com/mrgnlabs/marginfi-v2?rev=4ce8a15e70b4108059b74e1aedbfde1e3e564ace#4ce8a15e70b4108059b74e1aedbfde1e3e564ace" dependencies = [ "anchor-lang", "bytemuck", @@ -3274,8 +3275,8 @@ checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" [[package]] name = "marginfi" -version = "0.1.8" -source = "git+https://github.com/mrgnlabs/marginfi-v2?tag=mrgn-0.1.8-rc3#ef7768874ae362a0a20cd6737781941a5b6cda6b" +version = "0.1.9" +source = "git+https://github.com/mrgnlabs/marginfi-v2?rev=4ce8a15e70b4108059b74e1aedbfde1e3e564ace#4ce8a15e70b4108059b74e1aedbfde1e3e564ace" dependencies = [ "anchor-lang", "anchor-spl", @@ -3301,8 +3302,8 @@ dependencies = [ [[package]] name = "marginfi-type-crate" -version = "0.1.8" -source = "git+https://github.com/mrgnlabs/marginfi-v2?tag=mrgn-0.1.8-rc3#ef7768874ae362a0a20cd6737781941a5b6cda6b" +version = "0.1.9" +source = "git+https://github.com/mrgnlabs/marginfi-v2?rev=4ce8a15e70b4108059b74e1aedbfde1e3e564ace#4ce8a15e70b4108059b74e1aedbfde1e3e564ace" dependencies = [ "anchor-lang", "bs58 0.5.1", @@ -3311,6 +3312,7 @@ dependencies = [ "fixed-macro", "id-crate", "solana-instruction", + "spl-associated-token-account 6.0.0", "static_assertions", ] @@ -6851,7 +6853,7 @@ dependencies = [ [[package]] name = "solend-mocks" version = "0.1.0" -source = "git+https://github.com/mrgnlabs/marginfi-v2?tag=mrgn-0.1.8-rc3#ef7768874ae362a0a20cd6737781941a5b6cda6b" +source = "git+https://github.com/mrgnlabs/marginfi-v2?rev=4ce8a15e70b4108059b74e1aedbfde1e3e564ace#4ce8a15e70b4108059b74e1aedbfde1e3e564ace" dependencies = [ "anchor-lang", "bytemuck", diff --git a/Cargo.toml b/Cargo.toml index 6e1041c6..6b082ef9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,13 +54,22 @@ solana-dex-superagg = { git = "https://github.com/0dotxyz/solana-dex-superagg", [dependencies.marginfi_type_crate] git = "https://github.com/mrgnlabs/marginfi-v2" -tag = "mrgn-0.1.8-rc3" +# tag = "mrgn-0.1.8-rc3" +rev = "4ce8a15e70b4108059b74e1aedbfde1e3e564ace" package = "marginfi-type-crate" +#STAGE +#default-features = false +#features = ["staging", "client"] [dependencies.marginfi] git = "https://github.com/mrgnlabs/marginfi-v2" -tag = "mrgn-0.1.8-rc3" +# tag = "mrgn-0.1.8-rc3" +rev = "4ce8a15e70b4108059b74e1aedbfde1e3e564ace" +#PROD features = ["mainnet-beta", "client", "no-entrypoint"] +#STAGE +#default-features = false +#features = ["staging", "client", "no-entrypoint"] [dependencies.jupiter-swap-api-client] git = "https://github.com/IliaZyrin/jupiter-swap-api-client" diff --git a/eva.Dockerfile b/Dockerfile similarity index 88% rename from eva.Dockerfile rename to Dockerfile index 42857b7a..024ad3be 100644 --- a/eva.Dockerfile +++ b/Dockerfile @@ -11,7 +11,7 @@ RUN cargo chef cook --release --recipe-path recipe.json COPY . . -RUN cargo build --release --bin eva01 --features publish_to_db +RUN cargo build --release --bin eva01 FROM debian:bookworm-slim AS runner diff --git a/run-eva.sh b/run-eva.sh index 0b2d69e2..1359960b 100755 --- a/run-eva.sh +++ b/run-eva.sh @@ -7,12 +7,11 @@ source docker.prod.env set +o allexport # Add hardcoded vars -export RUST_LOG="debug,hyper=warn,h2::codec=warn" +export RUST_LOG="info,hyper=warn,h2::codec=warn" export RUST_BACKTRACE=0 # Run all steps cargo fmt -cargo clippy -- -D warnings +# cargo clippy -- -D warnings cargo build --bin eva01 --package eva01 cargo run --bin eva01 --features pretty_logs -# --features publish_to_db diff --git a/src/cache.rs b/src/cache.rs index 8bdc071b..de0d63d1 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -10,10 +10,13 @@ use std::{ }; use accounts::MarginfiAccountsCache; -use anyhow::Result; +use anchor_lang::AccountDeserialize; +use anyhow::{anyhow, Result}; use banks::BanksCache; use log::info; -use marginfi_type_crate::constants::FEE_STATE_SEED; +use marginfi_type_crate::{ + constants::FEE_STATE_SEED, pdas::derive_kamino_lending_market_authority, +}; use mints::MintsCache; use oracles::OraclesCache; use solana_client::{rpc_client::RpcClient, rpc_config::RpcSendTransactionConfig}; @@ -28,6 +31,7 @@ use solana_sdk::{ use tokens::TokensCache; use crate::{ + clock_manager, drift::accounts::{SpotMarket, User as DriftUser}, juplend_earn::accounts::Lending, kamino_lending::accounts::Reserve, @@ -51,18 +55,17 @@ pub struct Cache { pub luts: Arc>>, pub global_fee_state_key: Pubkey, pub global_fee_wallet: Pubkey, - pub kamino_reserves: HashMap, - pub drift_markets: HashMap, pub drift_users: HashMap, - pub juplend_lending_states: HashMap, } +#[derive(Clone)] pub struct KaminoReserve { pub address: Pubkey, pub reserve: Reserve, pub lending_market_authority: Pubkey, } +#[derive(Clone)] pub struct DriftSpotMarket { pub address: Pubkey, pub market: SpotMarket, @@ -91,13 +94,89 @@ impl Cache { luts, global_fee_state_key, global_fee_wallet: Pubkey::default(), - kamino_reserves: HashMap::new(), - drift_markets: HashMap::new(), drift_users: HashMap::new(), - juplend_lending_states: HashMap::new(), } } + fn build_kamino_reserve(address: Pubkey, reserve: Reserve) -> KaminoReserve { + let lending_market_authority = + derive_kamino_lending_market_authority(&reserve.lending_market).0; + KaminoReserve { + address, + reserve, + lending_market_authority, + } + } + + pub fn try_get_kamino_reserve(&self, address: &Pubkey) -> Result { + let account = self.oracles.try_get_account(address)?; + let mut data: &[u8] = &account.data; + let reserve = Reserve::try_deserialize(&mut data).map_err(|e| { + anyhow!( + "Failed to deserialize Kamino reserve {} from OracleCache: {}", + address, + e + ) + })?; + + Ok(Self::build_kamino_reserve(*address, reserve)) + } + + pub fn try_get_kamino_reserves(&self) -> Result> { + self.try_get_kamino_reserve_addresses()? + .into_iter() + .map(|address| Ok((address, self.try_get_kamino_reserve(&address)?))) + .collect() + } + + pub fn try_get_kamino_reserve_addresses(&self) -> Result> { + Ok(self.banks.get_kamino_reserves().into_iter().collect()) + } + + pub fn try_get_drift_market(&self, address: &Pubkey) -> Result { + let account = self.oracles.try_get_account(address)?; + let mut data: &[u8] = &account.data; + let market = SpotMarket::try_deserialize(&mut data).map_err(|e| { + anyhow!( + "Failed to deserialize Drift spot market {} from OracleCache: {}", + address, + e + ) + })?; + + Ok(DriftSpotMarket { + address: *address, + market, + }) + } + + pub fn try_get_juplend_lending_state(&self, address: &Pubkey) -> Result { + let account = self.oracles.try_get_account(address)?; + let mut data: &[u8] = &account.data; + Lending::try_deserialize(&mut data).map_err(|e| { + anyhow!( + "Failed to deserialize Juplend lending state {} from OracleCache: {}", + address, + e + ) + }) + } + + pub fn try_get_juplend_lending_states(&self) -> Result> { + self.try_get_juplend_lending_state_addresses()? + .into_iter() + .map(|address| Ok((address, self.try_get_juplend_lending_state(&address)?))) + .collect() + } + + pub fn try_get_juplend_lending_state_addresses(&self) -> Result> { + Ok(self + .banks + .get_juplend_lending_states() + .into_iter() + .collect()) + } + pub fn add_lut(&mut self, lut: AddressLookupTableAccount) { self.luts.lock().unwrap().push(lut) } @@ -110,7 +189,8 @@ impl Cache { let token_account = self.tokens.try_get_account(token_address)?; let bank_address = self.banks.try_get_account_for_mint(mint_address)?; let bank_wrapper = self.banks.try_get_bank(&bank_address)?; - let oracle_wrapper = T::build(self, &bank_address)?; + let clock = clock_manager::get_clock(&self.clock)?; + let oracle_wrapper = T::build(self, &clock, &bank_address)?; Ok(TokenAccountWrapper { balance: accessor::amount(&token_account.data)?, @@ -119,7 +199,6 @@ impl Cache { }) } - // TODO: think of a better place for this pub fn add_addresses_to_lut( &self, rpc_client: &RpcClient, @@ -233,93 +312,3 @@ fn extend_lut( Ok(lut.addresses.to_vec()) } - -#[cfg(test)] -pub mod test_utils { - use std::sync::{Arc, Mutex}; - - use solana_sdk::{account::Account, clock::Clock, pubkey::Pubkey}; - - use crate::wrappers::{bank::BankWrapper, oracle::test_utils::create_empty_oracle_account}; - - use super::Cache; - - pub fn create_test_cache(bank_wrappers: &Vec) -> Cache { - let mut cache = Cache::new( - Pubkey::new_unique(), - Pubkey::new_unique(), - Pubkey::new_unique(), - Arc::new(Mutex::new(Clock::default())), - ); - - for bank_wrapper in bank_wrappers { - let token_address = Pubkey::new_unique(); - let mut token_account = Account::default(); - token_account.data.resize(128, 0); - cache - .mints - .insert(bank_wrapper.bank.mint, Account::default(), token_address); - cache - .tokens - .try_insert(token_address, token_account, bank_wrapper.bank.mint) - .unwrap(); - cache.banks.insert(bank_wrapper.address, bank_wrapper.bank); - - let oracle_account = create_empty_oracle_account(); - cache - .oracles - .try_insert(bank_wrapper.bank.config.oracle_keys[0], oracle_account) - .unwrap(); - } - - cache - } -} - -#[cfg(test)] -mod tests { - use std::sync::Arc; - - use super::*; - use solana_sdk::pubkey::Pubkey; - - #[test] - fn test_cache() { - let signer_pk = Pubkey::new_unique(); - let marginfi_program_id = Pubkey::new_unique(); - let marginfi_group_address = Pubkey::new_unique(); - let cache = Cache::new( - signer_pk, - marginfi_program_id, - marginfi_group_address, - Arc::new(Mutex::new(Clock::default())), - ); - assert_eq!(cache.signer_pk, signer_pk); - assert_eq!(cache.marginfi_program_id, marginfi_program_id); - assert_eq!(cache.marginfi_group_address, marginfi_group_address); - } - - #[test] - fn test_add_lut() { - let signer_pk = Pubkey::new_unique(); - let marginfi_program_id = Pubkey::new_unique(); - let marginfi_group_address = Pubkey::new_unique(); - let mut cache = Cache::new( - signer_pk, - marginfi_program_id, - marginfi_group_address, - Arc::new(Mutex::new(Clock::default())), - ); - let lut = AddressLookupTableAccount { - key: Pubkey::new_unique(), - addresses: vec![Pubkey::new_unique()], - }; - - assert_eq!(cache.luts.lock().unwrap().len(), 0); - cache.add_lut(lut.clone()); - - let luts = cache.luts.lock().unwrap(); - assert_eq!(luts.len(), 1); - assert_eq!(luts[0].key, lut.key); - } -} diff --git a/src/cache/accounts.rs b/src/cache/accounts.rs index ddc6166c..972f4c71 100644 --- a/src/cache/accounts.rs +++ b/src/cache/accounts.rs @@ -64,101 +64,3 @@ impl MarginfiAccountsCache { .len()) } } -#[cfg(test)] -mod tests { - use marginfi_type_crate::{ - constants::ASSET_TAG_DEFAULT, - types::{Balance, LendingAccount, WrappedI80F48}, - }; - - use super::*; - - fn create_test_account(address: Pubkey) -> MarginfiAccountWrapper { - MarginfiAccountWrapper { - address, - liquidation_record: Pubkey::default(), - lending_account: LendingAccount { - balances: [Balance { - active: 1, - bank_pk: Pubkey::new_unique(), - bank_asset_tag: ASSET_TAG_DEFAULT, - tag: 0, - _pad0: [0; 4], - asset_shares: WrappedI80F48::default(), - liability_shares: WrappedI80F48::default(), - emissions_outstanding: WrappedI80F48::default(), - last_update: 0, - _padding: [0_u64], - }; 16], - last_tag_used: 0, - _pad1: [0; 6], - _padding: [0; 7], - }, - } - } - - #[test] - fn test_try_insert_and_try_get_account() { - let cache = MarginfiAccountsCache::default(); - let address = Pubkey::new_unique(); - let account = create_test_account(address); - - // Test insertion - assert!(cache.try_insert(account.clone()).is_ok()); - - // Test retrieval - let retrieved_account = cache.try_get_account(&address).unwrap(); - assert_eq!(retrieved_account.address, account.address); - } - - #[test] - fn test_try_get_account_not_found() { - let cache = MarginfiAccountsCache::default(); - let address = Pubkey::new_unique(); - - // Test retrieval of non-existent account - let result = cache.try_get_account(&address); - assert!(result.is_err()); - } - - #[test] - fn test_get_account_by_index() { - let cache = MarginfiAccountsCache::default(); - let address1 = Pubkey::new_unique(); - let address2 = Pubkey::new_unique(); - let account1 = create_test_account(address1); - let account2 = create_test_account(address2); - - // Insert accounts - cache.try_insert(account1.clone()).unwrap(); - cache.try_insert(account2.clone()).unwrap(); - - // Test retrieval by index - let retrieved_account1 = cache.try_get_account_by_index(0).unwrap(); - let retrieved_account2 = cache.try_get_account_by_index(1).unwrap(); - assert_eq!(retrieved_account1.address, account1.address); - assert_eq!(retrieved_account2.address, account2.address); - - // Test out-of-bounds index - assert!(cache.try_get_account_by_index(2).is_err()); - } - - #[test] - fn test_len() { - let cache = MarginfiAccountsCache::default(); - let address1 = Pubkey::new_unique(); - let address2 = Pubkey::new_unique(); - let account1 = create_test_account(address1); - let account2 = create_test_account(address2); - - // Initially empty - assert_eq!(cache.len().unwrap(), 0); - - // Insert accounts and check length - cache.try_insert(account1).unwrap(); - assert_eq!(cache.len().unwrap(), 1); - - cache.try_insert(account2).unwrap(); - assert_eq!(cache.len().unwrap(), 2); - } -} diff --git a/src/cache/banks.rs b/src/cache/banks.rs index 141a341f..f9313b84 100644 --- a/src/cache/banks.rs +++ b/src/cache/banks.rs @@ -1,24 +1,31 @@ -use std::collections::{HashMap, HashSet}; - +use crate::{utils::find_oracle_keys, wrappers::bank::BankWrapper}; +use anyhow::{anyhow, Result}; use marginfi_type_crate::{ - constants::{ASSET_TAG_DRIFT, ASSET_TAG_JUPLEND, ASSET_TAG_KAMINO}, + constants::{ + ASSET_TAG_DEFAULT, ASSET_TAG_DRIFT, ASSET_TAG_JUPLEND, ASSET_TAG_KAMINO, ASSET_TAG_SOL, + ASSET_TAG_STAKED, + }, types::{Bank, OracleSetup}, }; -use solana_sdk::pubkey::Pubkey; +use solana_sdk::{account::Account, pubkey::Pubkey}; +use std::collections::{HashMap, HashSet}; -use crate::{utils::find_oracle_keys, wrappers::bank::BankWrapper}; -use anyhow::{anyhow, Result}; #[derive(Default)] pub struct BanksCache { banks: HashMap, - mint_to_bank: HashMap, + mint_to_p0_bank: HashMap, } impl BanksCache { - pub fn insert(&mut self, bank_address: Pubkey, bank: Bank) { + pub fn insert(&mut self, bank_address: Pubkey, bank: Bank, account: Account) { self.banks - .insert(bank_address, BankWrapper::new(bank_address, bank)); - self.mint_to_bank.insert(bank.mint, bank_address); + .insert(bank_address, BankWrapper::new(bank_address, bank, account)); + if matches!( + bank.config.asset_tag, + ASSET_TAG_DEFAULT | ASSET_TAG_SOL | ASSET_TAG_STAKED + ) { + self.mint_to_p0_bank.insert(bank.mint, bank_address); + } } pub fn try_get_bank(&self, address: &Pubkey) -> Result { @@ -28,11 +35,6 @@ impl BanksCache { .cloned() } - #[cfg(test)] - pub fn get_bank(&self, address: &Pubkey) -> Option { - self.try_get_bank(address).ok() - } - pub fn get_oracles(&self) -> HashSet { self.banks .iter() @@ -72,19 +74,6 @@ impl BanksCache { .collect() } - pub fn get_drift_spot_markets(&self) -> HashSet { - self.banks - .iter() - .filter_map(|(_, bank)| { - if bank.bank.config.asset_tag == ASSET_TAG_DRIFT { - Some(bank.bank.integration_acc_1) - } else { - None - } - }) - .collect() - } - pub fn get_drift_users(&self) -> HashSet { self.banks .iter() @@ -112,7 +101,7 @@ impl BanksCache { } pub fn try_get_account_for_mint(&self, mint_address: &Pubkey) -> Result { - self.mint_to_bank + self.mint_to_p0_bank .get(mint_address) .ok_or(anyhow!( "Failed to find Bank for the Mint {} in Cache!", @@ -125,6 +114,8 @@ impl BanksCache { self.banks .values() .map(|bank| bank.bank.mint) + .collect::>() + .into_iter() .collect::>() } @@ -132,141 +123,3 @@ impl BanksCache { self.banks.len() } } - -#[cfg(test)] -pub mod test_utils { - use std::time::{SystemTime, UNIX_EPOCH}; - - use super::*; - use fixed::types::I80F48; - use fixed_macro::types::I80F48; - use marginfi_type_crate::{ - constants::MAX_ORACLE_KEYS, - types::{BankConfig, InterestRateConfig, OracleSetup}, - }; - - pub fn create_test_bank(mint: Pubkey) -> Bank { - let current_timestamp = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs() as i64; - - Bank { - mint, - asset_share_value: I80F48::ONE.into(), - liability_share_value: I80F48::ONE.into(), - total_liability_shares: I80F48!(207_112_621_602).into(), - total_asset_shares: I80F48!(10_000_000_000_000).into(), - last_update: current_timestamp, - config: BankConfig { - oracle_setup: OracleSetup::SwitchboardPull, - oracle_keys: [Pubkey::new_unique(); MAX_ORACLE_KEYS], - asset_weight_init: I80F48!(0.5).into(), - asset_weight_maint: I80F48!(0.75).into(), - liability_weight_init: I80F48!(1.5).into(), - liability_weight_maint: I80F48!(1.25).into(), - borrow_limit: u64::MAX, - deposit_limit: u64::MAX, - interest_rate_config: InterestRateConfig { - optimal_utilization_rate: I80F48!(0.6).into(), - plateau_interest_rate: I80F48!(0.40).into(), - protocol_fixed_fee_apr: I80F48!(0.01).into(), - ..Default::default() - }, - ..Default::default() - }, - ..Default::default() - } - } -} - -#[cfg(test)] -pub mod tests { - use crate::cache::banks::test_utils::create_test_bank; - - use super::*; - - #[test] - fn test_insert_and_get_account() { - let mut cache = BanksCache::default(); - let bank_address = Pubkey::new_unique(); - let bank = create_test_bank(Pubkey::new_unique()); - - cache.insert(bank_address, bank); - let retrieved_bank = cache.get_bank(&bank_address); - - assert!(retrieved_bank.is_some()); - assert_eq!(retrieved_bank.unwrap().address, bank_address); - } - - #[test] - fn test_try_get_account() { - let mut cache = BanksCache::default(); - let bank_address = Pubkey::new_unique(); - let bank = create_test_bank(Pubkey::new_unique()); - - cache.insert(bank_address, bank); - let result = cache.try_get_bank(&bank_address); - - assert!(result.is_ok()); - assert_eq!(result.unwrap().address, bank_address); - } - - #[test] - fn test_get_oracles() { - let mut cache = BanksCache::default(); - let bank_address = Pubkey::new_unique(); - let bank = create_test_bank(Pubkey::new_unique()); - - cache.insert(bank_address, bank); - let oracles = cache.get_oracles(); - - assert_eq!(oracles.len(), 1); - assert_eq!(oracles.iter().next().unwrap(), &bank.config.oracle_keys[0]); - } - - #[test] - fn test_try_get_account_for_mint() { - let mut cache = BanksCache::default(); - let bank_address = Pubkey::new_unique(); - let mint = Pubkey::new_unique(); - let bank = create_test_bank(mint); - - cache.insert(bank_address, bank); - let result = cache.try_get_account_for_mint(&mint); - - assert!(result.is_ok()); - assert_eq!(result.unwrap(), bank_address); - } - - #[test] - fn test_get_mints() { - let mut cache = BanksCache::default(); - let bank_address1 = Pubkey::new_unique(); - let bank_address2 = Pubkey::new_unique(); - let mint1 = Pubkey::new_unique(); - let mint2 = Pubkey::new_unique(); - let bank1 = create_test_bank(mint1); - let bank2 = create_test_bank(mint2); - - cache.insert(bank_address1, bank1); - cache.insert(bank_address2, bank2); - - let mints = cache.get_mints(); - assert_eq!(mints.len(), 2); - assert!(mints.contains(&mint1)); - assert!(mints.contains(&mint2)); - } - - #[test] - fn test_len() { - let mut cache = BanksCache::default(); - assert_eq!(cache.len(), 0); - - let bank_address = Pubkey::new_unique(); - let bank = create_test_bank(Pubkey::new_unique()); - cache.insert(bank_address, bank); - - assert_eq!(cache.len(), 1); - } -} diff --git a/src/cache/oracles.rs b/src/cache/oracles.rs index b56ff199..70c94b75 100644 --- a/src/cache/oracles.rs +++ b/src/cache/oracles.rs @@ -47,25 +47,6 @@ impl OraclesCache { .cloned() } - #[cfg(test)] - fn try_get_accounts(&self, addresses: &[Pubkey]) -> Result> { - let accounts_guard = self.accounts.read().map_err(|e| { - anyhow!( - "Failed to lock the Oracle accounts map for returning specific accounts! {}", - e - ) - })?; - - Ok(addresses - .iter() - .filter_map(|address| { - accounts_guard - .get(address) - .map(|account| (*address, account.clone())) - }) - .collect()) - } - pub fn try_get_addresses(&self) -> Result> { let accounts_guard = self.accounts.read().map_err(|e| { anyhow!( @@ -90,57 +71,3 @@ impl OraclesCache { .len()) } } - -#[cfg(test)] -mod tests { - use crate::wrappers::oracle::test_utils::TestOracleWrapper; - - use super::*; - - #[test] - fn test_try_insert() { - let mut cache = OraclesCache::default(); - let oracle_account = Account::default(); - let oracle_wrapper = TestOracleWrapper::test_sol(); - let oracle_address = oracle_wrapper.address; - - assert!(cache - .try_insert(oracle_address, oracle_account.clone()) - .is_ok()); - assert_eq!( - cache.try_get_account(&oracle_address).unwrap(), - oracle_account - ); - } - - #[test] - fn test_get_accounts() { - let mut cache = OraclesCache::default(); - let address1 = Pubkey::new_unique(); - let account1 = Account::default(); - cache.try_insert(address1, account1.clone()).unwrap(); - let address2 = Pubkey::new_unique(); - let account2 = Account::default(); - cache.try_insert(address2, account2.clone()).unwrap(); - - let accounts = cache.try_get_accounts(&[address1, address2]).unwrap(); - assert_eq!(cache.len().unwrap(), 2); - assert!(accounts.contains(&(address1, account1))); - assert!(accounts.contains(&(address2, account2))); - } - - #[test] - fn test_get_addresses() { - let mut cache = OraclesCache::default(); - let address1 = Pubkey::new_unique(); - let address2 = Pubkey::new_unique(); - - cache.try_insert(address1, Account::default()).unwrap(); - cache.try_insert(address2, Account::default()).unwrap(); - - let addresses = cache.try_get_addresses().unwrap(); - assert_eq!(addresses.len(), 2); - assert!(addresses.contains(&address1)); - assert!(addresses.contains(&address2)); - } -} diff --git a/src/cache_loader.rs b/src/cache_loader.rs index 1d67b97d..50230b22 100644 --- a/src/cache_loader.rs +++ b/src/cache_loader.rs @@ -23,13 +23,10 @@ use solana_sdk::{ }; use crate::{ - cache::{Cache, DriftSpotMarket, KaminoReserve}, + cache::Cache, drift, geyser::AccountType, - juplend_earn, kamino_lending, - utils::{ - batch_get_multiple_accounts, kamino::derive_lending_market_authority, BatchLoadingConfig, - }, + utils::{batch_get_multiple_accounts, BatchLoadingConfig}, wrappers::marginfi_account::MarginfiAccountWrapper, }; use anchor_client::Program; @@ -71,10 +68,7 @@ impl CacheLoader { self.load_mints(cache)?; self.load_oracles(cache)?; self.load_tokens(cache)?; - self.load_kamino_reserves(cache)?; - self.load_drift_spot_markets(cache)?; self.load_drift_users(cache)?; - self.load_juplend_lending_states(cache)?; Ok(()) } @@ -125,23 +119,24 @@ impl CacheLoader { max_concurrent_calls: 32, }, )?; + let marginfi_accounts_length = marginfi_accounts.len(); for (address, account_opt) in marginfi_accounts_pubkeys - .iter() - .zip(marginfi_accounts.iter()) + .into_iter() + .zip(marginfi_accounts.into_iter()) { if let Some(account) = account_opt { let marginfi_account = bytemuck::from_bytes::(&account.data[8..]); - let maw = MarginfiAccountWrapper::new(*address, marginfi_account); + let maw = MarginfiAccountWrapper::new(address, *marginfi_account); cache.marginfi_accounts.try_insert(maw)?; } else { - warn!("Couldn't load Marginfi account for key: {}", *address); + warn!("Couldn't load Marginfi account for key: {}", address); } } info!( "Loaded {} marginfi accounts in {:?}", - marginfi_accounts.len(), + marginfi_accounts_length, start.elapsed() ); @@ -203,14 +198,33 @@ impl CacheLoader { let program: Program> = anchor_client.program(cache.marginfi_program_id)?; info!("Loading banks..."); - for (bank_address, bank) in program - .accounts::(vec![RpcFilterType::Memcmp(Memcmp::new_base58_encoded( - BANK_GROUP_PK_OFFSET, - cache.marginfi_group_address.as_ref(), - ))])? - .iter() - { - cache.banks.insert(*bank_address, *bank); + let bank_accounts = program.rpc().get_program_accounts_with_config( + &cache.marginfi_program_id, + RpcProgramAccountsConfig { + account_config: RpcAccountInfoConfig { + encoding: Some(UiAccountEncoding::Base64), + ..Default::default() + }, + filters: Some(vec![ + RpcFilterType::Memcmp(Memcmp::new( + 0, + MemcmpEncodedBytes::Base58(bs58::encode(Bank::DISCRIMINATOR).into_string()), + )), + RpcFilterType::Memcmp(Memcmp::new_base58_encoded( + BANK_GROUP_PK_OFFSET, + cache.marginfi_group_address.as_ref(), + )), + ]), + with_context: Some(false), + sort_results: None, + }, + )?; + + for (bank_address, bank_account) in bank_accounts.into_iter() { + let mut data = bank_account.data.as_slice(); + let bank = Bank::try_deserialize(&mut data) + .map_err(|e| anyhow!("Failed to deserialize Bank {}: {:?}", bank_address, e))?; + cache.banks.insert(bank_address, bank, bank_account); debug!("Loaded the Bank {:?}.", bank_address); } @@ -396,82 +410,6 @@ impl CacheLoader { Ok(()) } - fn load_kamino_reserves(&self, cache: &mut Cache) -> anyhow::Result<()> { - info!("Loading Kamino Reserves..."); - - let reserve_addresses = cache - .banks - .get_kamino_reserves() - .into_iter() - .collect::>(); - let reserve_accounts = batch_get_multiple_accounts( - &self.rpc_client, - &reserve_addresses, - BatchLoadingConfig::DEFAULT, - )?; - - for (&address, account) in reserve_addresses.iter().zip(reserve_accounts.iter()) { - if let Some(account) = account { - debug!("Loaded the Kamino Reserve: {:?}", address); - let mut data: &[u8] = &account.data; - let reserve = kamino_lending::accounts::Reserve::try_deserialize(&mut data)?; - let lending_market_authority = - derive_lending_market_authority(&reserve.lending_market); - - cache.kamino_reserves.insert( - address, - KaminoReserve { - address, - reserve, - lending_market_authority, - }, - ); - } else { - error!("Reserve account {:?} not found.", address); - } - } - - info!("Loaded {} Kamino Reserves.", cache.kamino_reserves.len()); - - Ok(()) - } - - fn load_drift_spot_markets(&self, cache: &mut Cache) -> anyhow::Result<()> { - info!("Loading Drift Spot Markets..."); - - let spot_market_addresses = cache - .banks - .get_drift_spot_markets() - .into_iter() - .collect::>(); - let spot_market_accounts = batch_get_multiple_accounts( - &self.rpc_client, - &spot_market_addresses, - BatchLoadingConfig::DEFAULT, - )?; - - for (&address, account) in spot_market_addresses - .iter() - .zip(spot_market_accounts.iter()) - { - if let Some(account) = account { - debug!("Loaded the Drift Spot Market: {:?}", address); - let mut data: &[u8] = &account.data; - let market = drift::accounts::SpotMarket::try_deserialize(&mut data)?; - - cache - .drift_markets - .insert(address, DriftSpotMarket { address, market }); - } else { - error!("Spot Market account {:?} not found.", address); - } - } - - info!("Loaded {} Drift Spot Markets.", cache.drift_markets.len()); - - Ok(()) - } - fn load_drift_users(&self, cache: &mut Cache) -> anyhow::Result<()> { info!("Loading Drift Users..."); @@ -502,40 +440,6 @@ impl CacheLoader { Ok(()) } - - fn load_juplend_lending_states(&self, cache: &mut Cache) -> anyhow::Result<()> { - info!("Loading Juplend Lending States..."); - - let state_addresses = cache - .banks - .get_juplend_lending_states() - .into_iter() - .collect::>(); - let lending_states = batch_get_multiple_accounts( - &self.rpc_client, - &state_addresses, - BatchLoadingConfig::DEFAULT, - )?; - - for (&address, account) in state_addresses.iter().zip(lending_states.iter()) { - if let Some(account) = account { - debug!("Loaded Juplend Lending State: {:?}", address); - let mut data: &[u8] = &account.data; - let state = juplend_earn::accounts::Lending::try_deserialize(&mut data)?; - - cache.juplend_lending_states.insert(address, state); - } else { - error!("Lending State account {:?} not found.", address); - } - } - - info!( - "Loaded {} Juplend Lending States.", - cache.juplend_lending_states.len() - ); - - Ok(()) - } } pub fn get_accounts_to_track(cache: &Cache) -> Result> { diff --git a/src/cli/entrypoints.rs b/src/cli/entrypoints.rs index a6cdc063..6ffae27c 100644 --- a/src/cli/entrypoints.rs +++ b/src/cli/entrypoints.rs @@ -124,7 +124,7 @@ pub fn run_liquidator(config: Eva01Config, stop_liquidator: Arc) -> LIQUIDATION_ATTEMPTS.get(), FAILED_LIQUIDATIONS.get() ); - thread::sleep(std::time::Duration::from_secs(5)); + thread::sleep(std::time::Duration::from_secs(30)); } info!("The Main loop stopped."); diff --git a/src/clock_manager.rs b/src/clock_manager.rs index 535dc435..f3e87aaf 100644 --- a/src/clock_manager.rs +++ b/src/clock_manager.rs @@ -8,7 +8,6 @@ use std::time::Duration; const REFRESH_INTERVAL_SEC: u64 = 10; -// TODO: merge into Cache pub struct ClockManager { rpc_client: RpcClient, clock: Arc>, @@ -20,7 +19,6 @@ impl ClockManager { info!("Initializing ClockManager with RPC URL: {}", rpc_url); let rpc_client = RpcClient::new(rpc_url); - // let clock = Arc::new(Mutex::new(fetch_clock(&rpc_client)?)); let refresh_interval = Duration::from_secs(REFRESH_INTERVAL_SEC); Ok(Self { diff --git a/src/config.rs b/src/config.rs index c834ffa7..513f21a4 100644 --- a/src/config.rs +++ b/src/config.rs @@ -23,6 +23,7 @@ pub struct Eva01Config { pub healthcheck_port: u16, pub metrics_bind_addr: String, pub metrics_port: u16, + pub swb_program_id: Pubkey, pub crossbar_api_url: Option, pub jup_swap_api_url: String, pub swap_mint: Pubkey, @@ -87,6 +88,12 @@ impl Eva01Config { .parse() .expect("Invalid METRICS_PORT number"); + let swb_program_id = Pubkey::from_str( + &std::env::var("SWB_PROGRAM_ID") + .unwrap_or_else(|_| "A43DyUGA7s8eXPxqEjJY6EBu1KKbNgfxF8h17VAHn13w".to_string()), + ) + .expect("Invalid SWB_PROGRAM_ID Pubkey"); + let crossbar_api_url = std::env::var("CROSSBAR_API_URL").ok(); let jup_swap_api_url = std::env::var("JUP_SWAP_API_URL") @@ -139,6 +146,7 @@ impl Eva01Config { healthcheck_port, metrics_bind_addr, metrics_port, + swb_program_id, crossbar_api_url, jup_swap_api_url, swap_mint, diff --git a/src/drift_ixs.rs b/src/drift_ixs.rs index 4b2c0a67..50586246 100644 --- a/src/drift_ixs.rs +++ b/src/drift_ixs.rs @@ -2,12 +2,10 @@ use std::collections::HashSet; use anchor_lang::{Id, InstructionData, ToAccountMetas}; +use marginfi_type_crate::pdas::derive_drift_state; use solana_sdk::{instruction::Instruction, pubkey::Pubkey}; -use crate::{ - drift::{client as drift, program::Drift}, - utils::drift::derive_drift_state, -}; +use crate::drift::{client as drift, program::Drift}; pub fn make_refresh_spot_market_ix( spot_market: Pubkey, @@ -16,7 +14,7 @@ pub fn make_refresh_spot_market_ix( participating_accounts: &mut HashSet, ) -> Instruction { let accounts = drift::accounts::UpdateSpotMarketCumulativeInterest { - state: derive_drift_state(), + state: derive_drift_state().0, spot_market, oracle, spot_market_vault, diff --git a/src/geyser.rs b/src/geyser.rs index ca3c413a..7746d298 100644 --- a/src/geyser.rs +++ b/src/geyser.rs @@ -207,7 +207,6 @@ impl GeyserService { continue; } - // TODO: need more elaborate message payload check, it is hiding invalid messages. let account_update = ward!(&account.account, continue); let account = ward!(account_update_to_account(account_update).ok(), continue); diff --git a/src/geyser_processor.rs b/src/geyser_processor.rs index e83e742d..059ad705 100644 --- a/src/geyser_processor.rs +++ b/src/geyser_processor.rs @@ -73,7 +73,7 @@ impl GeyserProcessor { bytemuck::from_bytes::(&msg.account.data[8..]); self.cache .marginfi_accounts - .try_insert(MarginfiAccountWrapper::new(msg.address, marginfi_account))?; + .try_insert(MarginfiAccountWrapper::new(msg.address, *marginfi_account))?; self.run_liquidation.store(true, Ordering::Relaxed); GEYSER_TRIGGERED_SCANS_TOTAL.inc(); @@ -88,90 +88,3 @@ impl GeyserProcessor { Ok(()) } } - -#[cfg(test)] -mod tests { - use crate::{ - cache::test_utils::create_test_cache, - wrappers::bank::test_utils::{test_sol, test_usdc}, - }; - - use super::*; - use crossbeam::channel::unbounded; - use solana_sdk::{account::Account, pubkey::Pubkey}; - use std::sync::{atomic::AtomicBool, Arc}; - - #[test] - fn test_geyser_processor_new() { - let (_, receiver) = unbounded(); - let run_liquidation = Arc::new(AtomicBool::new(false)); - let stop = Arc::new(AtomicBool::new(false)); - let cache = Arc::new(create_test_cache(&Vec::new())); - - let processor = GeyserProcessor::new( - receiver, - run_liquidation.clone(), - stop.clone(), - cache.clone(), - ); - - assert!(processor.is_ok()); - } - - #[test] - fn test_geyser_processor_start_stop() { - let (_, receiver) = unbounded(); - let run_liquidation = Arc::new(AtomicBool::new(false)); - let stop = Arc::new(AtomicBool::new(false)); - let cache = Arc::new(create_test_cache(&Vec::new())); - - let processor = GeyserProcessor::new( - receiver, - run_liquidation.clone(), - stop.clone(), - cache.clone(), - ) - .unwrap(); - - // Simulate stopping the processor - stop.store(true, Ordering::Relaxed); - let result = processor.start(); - assert!(result.is_ok()); - } - - #[test] - fn test_process_update_token() { - let (_, receiver) = unbounded(); - let run_liquidation = Arc::new(AtomicBool::new(false)); - let stop = Arc::new(AtomicBool::new(false)); - - let sol_bank = test_sol(); - let usdc_bank = test_usdc(); - let mut cache = create_test_cache(&vec![sol_bank.clone(), usdc_bank.clone()]); - - let token_address = Pubkey::new_unique(); - cache - .tokens - .try_insert(token_address, Account::default(), sol_bank.bank.mint) - .unwrap(); - - let cache = Arc::new(cache); - - let processor = GeyserProcessor::new( - receiver, - run_liquidation.clone(), - stop.clone(), - cache.clone(), - ) - .unwrap(); - - let geyser_update = GeyserUpdate { - account_type: AccountType::Token, - address: token_address, - account: Default::default(), - }; - - let result = processor.process_update(geyser_update); - result.unwrap(); - } -} diff --git a/src/liquidator.rs b/src/liquidator.rs index 937edb3a..e1f8a454 100644 --- a/src/liquidator.rs +++ b/src/liquidator.rs @@ -1,5 +1,6 @@ use crate::{ cache::Cache, + clock_manager, config::{Eva01Config, TokenThresholds}, metrics::{ record_liquidation_failure, ACCOUNTS_SCANNED_TOTAL, ACCOUNT_SCAN_DURATION_SECONDS, @@ -9,7 +10,7 @@ use crate::{ }, rebalancer::Rebalancer, utils::{ - calc_total_weighted_assets_liabs, + format_error_chain, swb_cranker::{SwbCranker, SWB_STALE_HANDLED_ERROR, SWB_STALE_PRICE_ERROR_CODE_NUMBER}, }, wrappers::{ @@ -21,21 +22,20 @@ use crate::{ oracle::{OracleWrapper, OracleWrapperTrait}, }, }; -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, Context, Result}; use fixed::types::I80F48; use fixed_macro::types::I80F48; use log::{debug, error, info, warn}; use marginfi::state::{ - bank::BankImpl, - marginfi_account::RequirementType, - price::{OraclePriceType, PriceAdapter}, + bank::BankImpl, marginfi_account::get_health_components, price::PriceAdapter, }; use marginfi_type_crate::{ constants::BANKRUPT_THRESHOLD, - types::{BalanceSide, BankOperationalState, RiskTier}, + types::{BalanceSide, BankOperationalState, HealthPriceMode, OraclePriceType, RequirementType}, }; use solana_client::client_error::ClientError; use solana_program::pubkey::Pubkey; +use solana_sdk::{account_info::IntoAccountInfo, clock::Clock}; use std::{ cmp::min, collections::{HashMap, HashSet}, @@ -58,6 +58,29 @@ pub struct Liquidator { token_dust_threshold: I80F48, } +#[derive(Debug, Clone, Copy)] +struct LiquidationAmounts { + max_liquidatable_asset_amount: I80F48, + max_liquidatable_liab_amount: I80F48, + liquidator_profit: I80F48, + dust_liab_threshold: I80F48, +} + +impl LiquidationAmounts { + fn none() -> Self { + Self { + max_liquidatable_asset_amount: I80F48::ZERO, + max_liquidatable_liab_amount: I80F48::ZERO, + liquidator_profit: I80F48::ZERO, + dust_liab_threshold: I80F48::ZERO, + } + } + + fn is_liquidatable(&self) -> bool { + !self.max_liquidatable_asset_amount.is_zero() + } +} + impl Liquidator { pub fn new( config: Eva01Config, @@ -85,15 +108,14 @@ impl Liquidator { } pub fn start(&mut self) -> Result<()> { - // Fund the liquidator account, if needed - if !self.liquidator_account.has_funds()? { - warn!("Liquidator has no funds."); + if let Err(err) = self.simulate_oracles_and_integrations() { + warn!( + "Failed pre-rebalancing simulation round: {}", + format_error_chain(&err) + ); } - self.rebalancer.run(HashMap::new())?; - let mut liquidation_rounds = 0; - info!("Staring the Liquidator loop."); while !self.stop_liquidator.load(Ordering::Relaxed) { debug!("Waiting for any data change..."); @@ -105,21 +127,13 @@ impl Liquidator { info!("Running the Liquidation process..."); self.run_liquidation.store(false, Ordering::Relaxed); - if let Err(err) = self.swb_cranker.simulate_oracles(self.cache.as_ref()) { + if let Err(err) = self.simulate_oracles_and_integrations() { error!( - "Failed to simulate all Switchboard Oracles before evaluation: {}", - err + "Failed pre-liquidation simulation round: {}", + format_error_chain(&err) ); continue; } - // TODO: come up with a better heuristics here - if liquidation_rounds % 5 == 0 { - if let Err(e) = self.liquidator_account.refresh_integrations() { - error!("Integrations failed to refresh: {}", e); - } - } - - liquidation_rounds += 1; let mut missing_tokens: HashMap = HashMap::new(); let mut stale_swb_oracles: HashSet = HashSet::new(); @@ -128,27 +142,32 @@ impl Liquidator { accounts.sort_by(|a, b| a.profit.cmp(&b.profit)); accounts.reverse(); + if !stale_swb_oracles.is_empty() { + error!("STALE stale_swb_oracles: {:?}", stale_swb_oracles); + continue; + } + let mut tokens_in_shortage: HashSet = HashSet::new(); for acc in accounts { + let liquidatee = acc.liquidatee_account.address; + let liab_bank = acc.liab_bank; + let liab_amount = acc.liab_amount; // Get the liability mint for metrics let liab_mint = self .cache .banks - .try_get_bank(&acc.liab_bank) + .try_get_bank(&liab_bank) .ok() .map(|bank| bank.bank.mint); if let Err(e) = self.liquidator_account.liquidate( - &acc, + acc, &stale_swb_oracles, &mut tokens_in_shortage, ) { match e { LiquidationError::Anyhow(e) => { - error!( - "Failed to liquidate account {:?}: {:?}", - acc.liquidatee_account.address, e - ); + error!("Failed to liquidate account {:?}", liquidatee); let reason = if e.downcast_ref::().is_some() { FAILURE_REASON_RPC_ERROR } else { @@ -169,9 +188,9 @@ impl Liquidator { } LiquidationError::NotEnoughFunds => { missing_tokens - .entry(acc.liab_bank) - .and_modify(|m| *m += acc.liab_amount) - .or_insert(acc.liab_amount); + .entry(liab_bank) + .and_modify(|m| *m += liab_amount) + .or_insert(liab_amount); record_liquidation_failure( FAILURE_REASON_NOT_ENOUGH_FUNDS, liab_mint, @@ -195,6 +214,12 @@ impl Liquidator { info!("The Liquidation process is complete."); + if let Err(err) = self.simulate_oracles_and_integrations() { + warn!( + "Failed pre-rebalancing simulation round: {}", + format_error_chain(&err) + ); + } if let Err(error) = self.rebalancer.run(missing_tokens) { error!("Rebalancing failed: {:?}", error); ERROR_COUNT.inc(); @@ -205,6 +230,21 @@ impl Liquidator { Ok(()) } + fn simulate_oracles_and_integrations(&self) -> Result<()> { + self.liquidator_account + .simulate_refresh_integrations() + .context("simulate_refresh_integrations failed")?; + let swb_oracle_count = self.cache.banks.get_swb_oracles().len(); + self.swb_cranker + .simulate_oracles(self.cache.as_ref()) + .with_context(|| { + format!( + "simulate_oracles failed (switchboard feed count: {})", + swb_oracle_count + ) + }) + } + /// Checks if liquidation is needed, for each account one by one fn evaluate_all_accounts( &mut self, @@ -213,6 +253,7 @@ impl Liquidator { LIQUIDATION_SCAN_IN_PROGRESS.set(1); let scan_started = Instant::now(); let mut total_scanned: u64 = 0; + let clock = clock_manager::get_clock(&self.cache.clock)?; let evaluation_result = { let mut index: usize = 0; @@ -225,7 +266,7 @@ impl Liquidator { index += 1; continue; } - match self.process_account(&account, stale_swb_oracles) { + match self.process_account(&account, clock.clone(), stale_swb_oracles) { Ok(acc_opt) => { if let Some(acc) = acc_opt { result.push(acc); @@ -276,14 +317,16 @@ impl Liquidator { fn process_account( &self, account: &MarginfiAccountWrapper, + clock: Clock, stale_swb_oracles: &mut HashSet, ) -> Result> { let (deposit_shares, liab_shares) = account.get_deposits_and_liabilities_shares(); - if liab_shares.is_empty() { + if deposit_shares.is_empty() || liab_shares.is_empty() { return Ok(None); } let deposit_values = self.get_value_of_shares( + &clock, deposit_shares, &BalanceSide::Assets, RequirementType::Maintenance, @@ -291,6 +334,7 @@ impl Liquidator { )?; let liab_values = self.get_value_of_shares( + &clock, liab_shares, &BalanceSide::Liabilities, RequirementType::Maintenance, @@ -306,38 +350,49 @@ impl Liquidator { let asset_bank_wrapper = self.cache.banks.try_get_bank(&asset_bank_pk)?; let liab_bank_wrapper = self.cache.banks.try_get_bank(&liab_bank_pk)?; + let banks_to_include: Vec = vec![]; + let banks_to_exclude: Vec = vec![]; + let observation_accounts = MarginfiAccountWrapper::get_observation_accounts::( + &account.account.lending_account, + &banks_to_include, + &banks_to_exclude, + &self.cache, + &clock, + )?; + // Calculated max liquidatable amount is the defining factor for liquidation. - let ( - max_liquidatable_asset_amount, - max_liquidatable_liab_amount, - profit, - dust_liab_threshold, - ) = self.compute_max_liquidatable_amounts_with_banks( + let liquidation_amounts = self.compute_max_liquidatable_amounts_with_banks( + &self.cache, + clock, account, + observation_accounts.observation_accounts.as_slice(), &asset_bank_wrapper, &liab_bank_wrapper, )?; - if max_liquidatable_asset_amount.is_zero() { + if !liquidation_amounts.is_liquidatable() { return Ok(None); } - let slippage_adjusted_asset_amount = max_liquidatable_asset_amount * I80F48!(0.90); - let slippage_adjusted_liab_amount = max_liquidatable_liab_amount * I80F48!(0.90); + let slippage_adjusted_asset_amount = + liquidation_amounts.max_liquidatable_asset_amount * I80F48!(0.90); + let slippage_adjusted_liab_amount = + liquidation_amounts.max_liquidatable_liab_amount * I80F48!(0.90); debug!( "asset_amount_to_liquidate: {:?}, slippage_adjusted_asset_amount: {:?}, slippage_adjusted_liab_amount: {:?}", - max_liquidatable_asset_amount, slippage_adjusted_asset_amount, slippage_adjusted_liab_amount + liquidation_amounts.max_liquidatable_asset_amount, slippage_adjusted_asset_amount, slippage_adjusted_liab_amount ); Ok(Some(PreparedLiquidatableAccount { liquidatee_account: account.clone(), + observation_accounts, asset_bank: asset_bank_pk, liab_bank: liab_bank_pk, asset_amount: slippage_adjusted_asset_amount, liab_amount: slippage_adjusted_liab_amount, - profit: profit.to_num(), - dust_liab_threshold, + profit: liquidation_amounts.liquidator_profit.to_num(), + dust_liab_threshold: liquidation_amounts.dust_liab_threshold, })) } @@ -371,7 +426,6 @@ impl Liquidator { .iter() .max_by(|a, b| { //debug!("Liab Bank {:?} value: {:?}", a.1, a.0); - a.0.cmp(&b.0) }) .ok_or_else(|| anyhow!("No liability bank found"))?; @@ -381,26 +435,48 @@ impl Liquidator { fn compute_max_liquidatable_amounts_with_banks( &self, + cache: &Cache, + clock: Clock, account: &MarginfiAccountWrapper, + banks_and_oracles: &[Pubkey], asset_bank_wrapper: &BankWrapper, liab_bank_wrapper: &BankWrapper, - ) -> Result<(I80F48, I80F48, I80F48, I80F48)> { - let (total_weighted_assets, total_weighted_liabilities) = calc_total_weighted_assets_liabs( - &self.cache, - &account.lending_account, + ) -> Result { + let mut accounts: Vec<_> = vec![]; + for pk in banks_and_oracles.iter() { + let bank_account = cache.banks.try_get_bank(pk); + let account = match bank_account { + Ok(wrapper) => wrapper.account, + Err(_) => cache.oracles.try_get_account(pk)?, + }; + accounts.push(account); + } + + let remaining_ais: Vec<_> = accounts + .iter_mut() + .zip(banks_and_oracles.iter()) + .map(|(account, pk)| (pk, account).into_account_info()) + .collect(); + + let (total_weighted_assets, total_weighted_liabilities) = get_health_components( + &account.account, + &remaining_ais, RequirementType::Maintenance, + &mut None, + HealthPriceMode::Client(clock.clone()), )?; + let maintenance_health = total_weighted_assets - total_weighted_liabilities; debug!( "Account {} maintenance_health = {:?} (assets {:?}, liabilities {:?})", account.address, maintenance_health, total_weighted_assets, total_weighted_liabilities ); if maintenance_health >= I80F48::ZERO { - // TODO: revisit this crazy return type - return Ok((I80F48::ZERO, I80F48::ZERO, I80F48::ZERO, I80F48::ZERO)); + return Ok(LiquidationAmounts::none()); } - let asset_oracle_wrapper = OracleWrapper::build(&self.cache, &asset_bank_wrapper.address)?; + let asset_oracle_wrapper = + OracleWrapper::build(&self.cache, &clock, &asset_bank_wrapper.address)?; let asset_price = asset_oracle_wrapper .price_adapter .get_price_of_type_ignore_conf(OraclePriceType::RealTime, None)? @@ -412,7 +488,7 @@ impl Liquidator { "Asset ({}) price is lower than the declared range: {} < {}", asset_bank_wrapper.bank.mint, asset_price, min_asset_price ); - return Ok((I80F48::ZERO, I80F48::ZERO, I80F48::ZERO, I80F48::ZERO)); + return Ok(LiquidationAmounts::none()); } let max_asset_price = thresholds.declared_value * (1.0 + DECLARED_VALUE_RANGE); if asset_price > max_asset_price { @@ -420,11 +496,12 @@ impl Liquidator { "Asset ({}) price is higher than the declared range: {} > {}", asset_bank_wrapper.bank.mint, asset_price, max_asset_price ); - return Ok((I80F48::ZERO, I80F48::ZERO, I80F48::ZERO, I80F48::ZERO)); + return Ok(LiquidationAmounts::none()); } } - let liab_oracle_wrapper = OracleWrapper::build(&self.cache, &liab_bank_wrapper.address)?; + let liab_oracle_wrapper = + OracleWrapper::build(&self.cache, &clock, &liab_bank_wrapper.address)?; let liab_price = liab_oracle_wrapper .price_adapter .get_price_of_type_ignore_conf(OraclePriceType::RealTime, None)? @@ -436,7 +513,7 @@ impl Liquidator { "Liability ({}) price is lower than the declared range: {} < {}", liab_bank_wrapper.bank.mint, liab_price, min_liab_price ); - return Ok((I80F48::ZERO, I80F48::ZERO, I80F48::ZERO, I80F48::ZERO)); + return Ok(LiquidationAmounts::none()); } let max_liab_price = thresholds.declared_value * (1.0 + DECLARED_VALUE_RANGE); if liab_price > max_liab_price { @@ -444,7 +521,7 @@ impl Liquidator { "Liability ({}) price is higher than the declared range: {} > {}", liab_bank_wrapper.bank.mint, liab_price, max_liab_price ); - return Ok((I80F48::ZERO, I80F48::ZERO, I80F48::ZERO, I80F48::ZERO)); + return Ok(LiquidationAmounts::none()); } } @@ -457,14 +534,14 @@ impl Liquidator { if all >= I80F48::ZERO { debug!("Account {:?} has no liquidatable amount: {:?}, asset_weight_maint: {:?}, liab_weight_maint: {:?}", account.address, all, asset_weight_maint, liab_weight_maint); - return Ok((I80F48::ZERO, I80F48::ZERO, I80F48::ZERO, I80F48::ZERO)); + return Ok(LiquidationAmounts::none()); } let underwater_maint_value = maintenance_health / (asset_weight_maint - liab_weight_maint * liquidation_discount); - let (asset_amount, _) = self.get_balance_for_bank(account, asset_bank_wrapper)?; - let (_, liab_amount) = self.get_balance_for_bank(account, liab_bank_wrapper)?; + let (asset_amount, _) = account.get_balance_for_bank(asset_bank_wrapper)?; + let (_, liab_amount) = account.get_balance_for_bank(liab_bank_wrapper)?; let asset_value = asset_bank_wrapper.calc_value( &asset_oracle_wrapper, @@ -486,7 +563,7 @@ impl Liquidator { .unwrap(); if liquidator_profit <= self.min_profit { - return Ok((I80F48::ZERO, I80F48::ZERO, I80F48::ZERO, I80F48::ZERO)); + return Ok(LiquidationAmounts::none()); } let max_liquidatable_asset_amount = asset_bank_wrapper.calc_amount( @@ -519,50 +596,17 @@ impl Liquidator { liab_bank_wrapper.address, liab_bank_wrapper.bank.config.liability_weight_maint, liab_amount, liab_value, max_liquidatable_value, max_liquidatable_asset_amount, max_liquidatable_liab_amount, liquidator_profit); - Ok(( + Ok(LiquidationAmounts { max_liquidatable_asset_amount, max_liquidatable_liab_amount, liquidator_profit, dust_liab_threshold, - )) - } - - /// Gets the balance for a given [`MarginfiAccount`] and [`Bank`] - // TODO: merge with `get_balance_for_bank` in `MarginfiAccountWrapper` - fn get_balance_for_bank( - &self, - account: &MarginfiAccountWrapper, - bank_wrapper: &BankWrapper, - ) -> Result<(I80F48, I80F48)> { - let balance = account - .lending_account - .balances - .iter() - .find(|b| b.bank_pk == bank_wrapper.address && b.is_active()) - .map(|b| match b.get_side()? { - BalanceSide::Assets => { - let amount = bank_wrapper - .bank - .get_asset_amount(b.asset_shares.into()) - .ok()?; - Some((amount, I80F48::ZERO)) - } - BalanceSide::Liabilities => { - let amount = bank_wrapper - .bank - .get_liability_amount(b.liability_shares.into()) - .ok()?; - Some((I80F48::ZERO, amount)) - } - }) - .map(|e| e.unwrap_or_default()) - .unwrap_or_default(); - - Ok(balance) + }) } fn get_value_of_shares( &self, + clock: &Clock, shares: Vec<(I80F48, Pubkey)>, balance_side: &BalanceSide, requirement_type: RequirementType, @@ -576,7 +620,7 @@ impl Liquidator { return Err(anyhow!(SWB_STALE_HANDLED_ERROR)); } - let oracle_wrapper = match OracleWrapper::build(&self.cache, &bank_pk) { + let oracle_wrapper = match OracleWrapper::build(&self.cache, clock, &bank_pk) { Ok(oracle_wrapper) => oracle_wrapper, Err(err) => { if err @@ -598,19 +642,13 @@ impl Liquidator { } }; - // TODO: add support for isolated or deprecate completely? - if matches!(bank_wrapper.bank.config.risk_tier, RiskTier::Isolated) { - continue; - } - if !matches!( bank_wrapper.bank.config.operational_state, - BankOperationalState::Operational + BankOperationalState::Operational | BankOperationalState::ReduceOnly ) { continue; } - // TODO: add Banks to Geyser!!! if bank_wrapper.bank.check_utilization_ratio().is_err() { debug!("Skipping bankrupt bank from evaluation: {}", bank_pk); continue; diff --git a/src/main.rs b/src/main.rs index a0b6a9d8..3ad399b1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -44,6 +44,9 @@ use drift_idl::*; fn main() -> Result<(), Box> { init_logging(); + //FIX MINTING TO WSOL !!!! + //FIX MINTING TO WSOL !!!! + //FIX MINTING TO WSOL !!!! std::panic::set_hook(Box::new(|panic_info| { eprintln!("Panic occurred: {:#?}", panic_info); diff --git a/src/marginfi_ixs.rs b/src/marginfi_ixs.rs index 300dfef2..3042eb1f 100644 --- a/src/marginfi_ixs.rs +++ b/src/marginfi_ixs.rs @@ -4,7 +4,14 @@ use anchor_lang::{Id, InstructionData, Key, ToAccountMetas}; use anchor_spl::{associated_token, token_2022}; use log::{debug, info, trace}; -use marginfi_type_crate::constants::LIQUIDATION_RECORD_SEED; +use marginfi_type_crate::{ + constants::LIQUIDATION_RECORD_SEED, + pdas::{ + derive_drift_signer, derive_drift_state, derive_juplend_lending_admin, + derive_juplend_liquidity, derive_juplend_liquidity_vault, derive_juplend_rate_model, + derive_kamino_user_state, + }, +}; use solana_client::{rpc_client::RpcClient, rpc_config::RpcSendTransactionConfig}; use solana_sdk::{ commitment_config::CommitmentConfig, @@ -21,14 +28,7 @@ use crate::{ juplend_earn::accounts::Lending, kamino_farms::program::Farms as KaminoFarms, kamino_lending::program::KaminoLending, - utils::{ - drift::{derive_drift_signer, derive_drift_state, derive_spot_market_vault}, - find_bank_liquidity_vault_authority, - juplend::{ - derive_lending_admin, derive_liquidity, derive_liquidity_vault, derive_rate_model, - }, - kamino::derive_user_state, - }, + utils::find_bank_liquidity_vault_authority, wrappers::{bank::BankWrapper, mint::MintWrapper}, }; @@ -355,10 +355,13 @@ pub fn make_kamino_withdraw_ix( } else { ( Some(kamino_reserve.reserve.farm_collateral), - Some(derive_user_state( - &kamino_reserve.reserve.farm_collateral, - &kamino_obligation, - )), + Some( + derive_kamino_user_state( + &kamino_reserve.reserve.farm_collateral, + &kamino_obligation, + ) + .0, + ), ) }; @@ -385,9 +388,8 @@ pub fn make_kamino_withdraw_ix( reserve_farm_state, kamino_program: KaminoLending::id(), farms_program: KaminoFarms::id(), - // FIX - collateral_token_program: mint_wrapper.account.owner, // assuming Kamino liquidity and collateral are the same as our bank's mint - liquidity_token_program: mint_wrapper.account.owner, // assuming Kamino liquidity and collateral are the same as our bank's mint + collateral_token_program: spl_token::ID, + liquidity_token_program: mint_wrapper.account.owner, instruction_sysvar_account: sysvar::instructions::id(), } .to_account_metas(None); @@ -406,7 +408,7 @@ pub fn make_kamino_withdraw_ix( accounts, data: marginfi::instruction::KaminoWithdraw { amount, - withdraw_all: Some(withdraw_all), + flags: if withdraw_all { Some(1) } else { None }, } .data(), } @@ -436,11 +438,6 @@ pub fn make_drift_withdraw_ix( Some(drift_spot_market.market.oracle) }; - assert_eq!( - derive_spot_market_vault(drift_spot_market.market.market_index), - drift_spot_market.market.vault - ); - let mut accounts = marginfi::accounts::DriftWithdraw { group, marginfi_account, @@ -453,7 +450,7 @@ pub fn make_drift_withdraw_ix( ), liquidity_vault: bank.bank.liquidity_vault, destination_token_account: mint_wrapper.token, - drift_state: derive_drift_state(), + drift_state: derive_drift_state().0, integration_acc_1: bank.bank.integration_acc_1, // spot market integration_acc_2: bank.bank.integration_acc_2, // user integration_acc_3: bank.bank.integration_acc_3, // user stats @@ -476,7 +473,7 @@ pub fn make_drift_withdraw_ix( drift_reward_mint_2: reward_spot_market_2 .map(|m| m.market.mint) .or(Some(marginfi_program_id)), - drift_signer: derive_drift_signer(), + drift_signer: derive_drift_signer().0, mint: bank.bank.mint, drift_program: Drift::id(), token_program: mint_wrapper.account.owner, @@ -534,13 +531,13 @@ pub fn make_juplend_withdraw_ix( integration_acc_1: bank.bank.integration_acc_1, // lending state integration_acc_2: bank.bank.integration_acc_2, // f_token_vault integration_acc_3: bank.bank.integration_acc_3, // intermediary ATA - lending_admin: derive_lending_admin(), + lending_admin: derive_juplend_lending_admin().0, supply_token_reserves_liquidity: lending_state.token_reserves_liquidity, lending_supply_position_on_liquidity: lending_state.supply_position_on_liquidity, - rate_model: derive_rate_model(&bank.bank.mint), - vault: derive_liquidity_vault(&bank.bank.mint, &mint_wrapper.account.owner), + rate_model: derive_juplend_rate_model(&bank.bank.mint).0, + vault: derive_juplend_liquidity_vault(&bank.bank.mint, &mint_wrapper.account.owner), claim_account: bank.bank.integration_acc_1, // NOT used -> can be any mutable account - liquidity: derive_liquidity(), + liquidity: derive_juplend_liquidity().0, liquidity_program: crate::liquidity::ID, rewards_rate_model: lending_state.rewards_rate_model, juplend_program: crate::juplend_earn::ID, diff --git a/src/rebalancer.rs b/src/rebalancer.rs index bfb4dfe8..92d6fe3d 100644 --- a/src/rebalancer.rs +++ b/src/rebalancer.rs @@ -18,7 +18,10 @@ use solana_dex_superagg::{ }; use solana_program::pubkey::Pubkey; use solana_sdk::{account::ReadableAccount, commitment_config::CommitmentLevel}; -use std::{collections::HashMap, sync::Arc}; +use std::{ + collections::{HashMap, HashSet}, + sync::Arc, +}; use tokio::runtime::{Builder, Runtime}; const SLIPPAGE_MULTIPLIER: I80F48 = I80F48!(1.05); @@ -34,6 +37,7 @@ pub struct Rebalancer { default_token_max_threshold: I80F48, token_thresholds: HashMap, dex_client: Arc, + empty_stake_banks: HashSet, } impl Rebalancer { @@ -96,24 +100,30 @@ impl Rebalancer { default_token_max_threshold, token_thresholds, dex_client, + empty_stake_banks: HashSet::new(), }) } pub fn run(&mut self, missing_tokens: HashMap) -> anyhow::Result<()> { info!("Running the Rebalancing process..."); - // TODO: expand directly in this function? - if let Err(e) = self.handle_token_accounts(missing_tokens) { + let swap_token_address = self.cache.tokens.try_get_token_for_mint(&self.swap_mint)?; + let swap_wrapper = self + .cache + .try_get_token_wrapper::(&self.swap_mint, &swap_token_address)?; + + if let Err(e) = self.handle_token_accounts(missing_tokens, &swap_wrapper) { error!("Failed to handle the Liquidator's tokens: {}", e); // Note: Stale oracle errors from withdraw operations are now handled // inside handle_token_accounts where we have access to the bank context } - if let Err(error) = self.deposit_preferred_token() { + if let Err(error) = self.deposit_preferred_token(&swap_wrapper) { error!("Failed to deposit preferred token: {}", error); // Check if this is a stale oracle error and record it in metrics if let Some(client_err) = error.downcast_ref::() { if is_stale_swb_price_error(client_err) { + error!("MUST NEVER HAPPEN"); record_liquidation_failure(FAILURE_REASON_STALE_ORACLES, None, None); } } @@ -127,14 +137,11 @@ impl Rebalancer { fn handle_token_accounts( &mut self, missing_tokens: HashMap, + swap_wrapper: &TokenAccountWrapper, ) -> anyhow::Result<()> { let (necessary_swap_value, missing_mint_to_value) = self.sell_excessive_tokens_and_calculate_necessary_swap_value(missing_tokens)?; - let swap_token_address = self.cache.tokens.try_get_token_for_mint(&self.swap_mint)?; - let swap_wrapper = self - .cache - .try_get_token_wrapper::(&self.swap_mint, &swap_token_address)?; let existing_swap_value = swap_wrapper.get_value()?; if necessary_swap_value > existing_swap_value { @@ -178,7 +185,7 @@ impl Rebalancer { let mut necessary_swap_value = I80F48::ZERO; for mint in self.cache.mints.get_mints() { debug!("Processing token {}...", mint); - if mint == self.swap_mint { + if mint == self.swap_mint || self.empty_stake_banks.contains(&mint) { continue; } @@ -188,7 +195,9 @@ impl Rebalancer { .try_get_token_wrapper::(&mint, &token); if let Err(e) = wrapper { // Ignore empty stake banks - if !e.to_string().contains("Stake pool supply is zero") { + if e.to_string().contains("Stake pool supply is zero") { + self.empty_stake_banks.insert(mint); + } else { warn!("Skipping the token {} in rebalancing: {}", mint, e); } continue; @@ -198,11 +207,15 @@ impl Rebalancer { if let Some(&amount) = bank_to_amount.get(&wrapper.bank_wrapper.address) { let value_to_swap = wrapper.get_value_for_amount(amount)?; - mint_to_value.insert( - mint, - value_to_swap.checked_mul(SLIPPAGE_MULTIPLIER).unwrap(), - ); - necessary_swap_value += value_to_swap; + let missing_value = if value_to_swap < I80F48::from_num(1) { + I80F48::from_num(1) + } else { + value_to_swap + .checked_mul(SLIPPAGE_MULTIPLIER) + .ok_or_else(|| anyhow::anyhow!("Failed to calculate missing token value"))? + }; + mint_to_value.insert(mint, missing_value); + necessary_swap_value += missing_value; continue; } @@ -240,7 +253,7 @@ impl Rebalancer { fn buy_missing_tokens( &mut self, - swap_token_wrapper: TokenAccountWrapper, + swap_token_wrapper: &TokenAccountWrapper, mint_to_value: HashMap, ) -> anyhow::Result<()> { for mint in self.cache.mints.get_mints() { @@ -258,17 +271,14 @@ impl Rebalancer { Ok(()) } - fn deposit_preferred_token(&self) -> anyhow::Result<()> { + fn deposit_preferred_token( + &self, + swap_wrapper: &TokenAccountWrapper, + ) -> anyhow::Result<()> { let amount = self .get_token_balance_for_mint(&self.swap_mint) .unwrap_or_default(); - // TODO: move on the higher level - let swap_token_address = self.cache.tokens.try_get_token_for_mint(&self.swap_mint)?; - let swap_wrapper = self - .cache - .try_get_token_wrapper::(&self.swap_mint, &swap_token_address)?; - let max_value = self .token_thresholds .get(&self.swap_mint) @@ -326,6 +336,10 @@ impl Rebalancer { amount, input_mint, output_mint ); + if output_mint == Pubkey::from_str_const("So11111111111111111111111111111111111111112") { + return Err(anyhow::anyhow!("FIX WSOL SWAPS!")); + } + let result = self.tokio_rt.block_on(self.dex_client.swap( &input_mint.to_string(), &output_mint.to_string(), diff --git a/src/utils/drift.rs b/src/utils/drift.rs deleted file mode 100644 index 3ef8497f..00000000 --- a/src/utils/drift.rs +++ /dev/null @@ -1,37 +0,0 @@ -use crate::drift; -use solana_program::pubkey::Pubkey; - -const SEED_STATE: &str = "drift_state"; -const SEED_SPOT_MARKET: &str = "spot_market"; -const SEED_SPOT_MARKET_VAULT: &str = "spot_market_vault"; -const SEED_DRIFT_SIGNER: &str = "drift_signer"; - -pub fn derive_drift_state() -> Pubkey { - Pubkey::find_program_address(&[SEED_STATE.as_bytes()], &drift::ID).0 -} - -pub fn derive_spot_market(market_index: u16) -> Pubkey { - Pubkey::find_program_address( - &[ - SEED_SPOT_MARKET.as_bytes(), - market_index.to_le_bytes().as_ref(), - ], - &drift::ID, - ) - .0 -} - -pub fn derive_spot_market_vault(market_index: u16) -> Pubkey { - Pubkey::find_program_address( - &[ - SEED_SPOT_MARKET_VAULT.as_bytes(), - market_index.to_le_bytes().as_ref(), - ], - &drift::ID, - ) - .0 -} - -pub fn derive_drift_signer() -> Pubkey { - Pubkey::find_program_address(&[SEED_DRIFT_SIGNER.as_bytes()], &drift::ID).0 -} diff --git a/src/utils/juplend.rs b/src/utils/juplend.rs deleted file mode 100644 index 3cc7c537..00000000 --- a/src/utils/juplend.rs +++ /dev/null @@ -1,25 +0,0 @@ -use crate::{juplend_earn, liquidity}; -use anchor_spl::associated_token; -use solana_program::pubkey::Pubkey; - -const SEED_LENDING_ADMIN: &str = "lending_admin"; -const SEED_LIQUIDITY: &str = "liquidity"; -const SEED_RATE_MODEL: &str = "rate_model"; - -pub fn derive_lending_admin() -> Pubkey { - Pubkey::find_program_address(&[SEED_LENDING_ADMIN.as_bytes()], &juplend_earn::ID).0 -} - -pub fn derive_liquidity() -> Pubkey { - Pubkey::find_program_address(&[SEED_LIQUIDITY.as_bytes()], &liquidity::ID).0 -} - -pub fn derive_rate_model(mint: &Pubkey) -> Pubkey { - Pubkey::find_program_address(&[SEED_RATE_MODEL.as_bytes(), mint.as_ref()], &liquidity::ID).0 -} - -pub fn derive_liquidity_vault(mint: &Pubkey, token_program: &Pubkey) -> Pubkey { - let liquidity = derive_liquidity(); - - associated_token::get_associated_token_address_with_program_id(&liquidity, mint, token_program) -} diff --git a/src/utils/kamino.rs b/src/utils/kamino.rs deleted file mode 100644 index cb9e3e99..00000000 --- a/src/utils/kamino.rs +++ /dev/null @@ -1,27 +0,0 @@ -use crate::{kamino_farms::program::Farms as KaminoFarms, kamino_lending::program::KaminoLending}; -use anchor_lang::Id; -use solana_program::pubkey::Pubkey; - -pub const SEED_LENDING_MARKET_AUTH: &str = "lma"; -pub const SEED_USER_STATE: &str = "user"; - -// TODO: expose these from the program side? -pub fn derive_lending_market_authority(lending_market: &Pubkey) -> Pubkey { - Pubkey::find_program_address( - &[SEED_LENDING_MARKET_AUTH.as_bytes(), lending_market.as_ref()], - &KaminoLending::id(), - ) - .0 -} - -pub fn derive_user_state(farm_state: &Pubkey, obligation: &Pubkey) -> Pubkey { - Pubkey::find_program_address( - &[ - SEED_USER_STATE.as_bytes(), - farm_state.as_ref(), - obligation.as_ref(), - ], - &KaminoFarms::id(), - ) - .0 -} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index a80d2d26..09ef3a07 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,33 +1,12 @@ -pub mod drift; pub mod healthcheck; -pub mod juplend; -pub mod kamino; +pub mod simulation_cache; pub mod swb_cranker; use anyhow::{anyhow, Error, Result}; use backoff::ExponentialBackoff; -use fixed::types::I80F48; use log::{debug, error}; -use marginfi::{ - bank_authority_seed, - constants::DRIFT_SCALED_BALANCE_DECIMALS, - errors::MarginfiError, - state::{ - bank::{BankImpl, BankVaultType}, - bank_config::BankConfigImpl, - marginfi_account::{calc_value, RequirementType}, - price::PriceBias, - }, -}; -use marginfi_type_crate::{ - constants::{ - ASSET_TAG_DEFAULT, ASSET_TAG_DRIFT, ASSET_TAG_KAMINO, ASSET_TAG_SOL, ASSET_TAG_STAKED, - }, - types::{ - reconcile_emode_configs, Balance, BalanceSide, Bank, BankConfig, EmodeConfig, - LendingAccount, RiskTier, - }, -}; +use marginfi::{bank_authority_seed, errors::MarginfiError, state::bank::BankVaultType}; +use marginfi_type_crate::types::BankConfig; use rayon::{iter::ParallelIterator, slice::ParallelSlice}; use solana_account_decoder::{UiAccountEncoding, UiDataSliceConfig}; use solana_client::{ @@ -40,15 +19,6 @@ use solana_sdk::account::Account; use std::sync::{atomic::AtomicUsize, Arc}; use yellowstone_grpc_proto::geyser::SubscribeUpdateAccountInfo; -use crate::{ - cache::Cache, - wrappers::{ - bank::BankWrapper, - oracle::{OracleWrapper, OracleWrapperTrait}, - }, -}; -use std::cmp::max; - pub struct BatchLoadingConfig { pub max_batch_size: usize, pub max_concurrent_calls: usize, @@ -163,7 +133,7 @@ pub mod accessor { Ok(u64::from_le_bytes(amount_bytes)) } - #[allow(dead_code)] + #[cfg(test)] pub fn mint(bytes: &[u8]) -> Pubkey { let mut mint_bytes = [0u8; 32]; mint_bytes.copy_from_slice(&bytes[..32]); @@ -195,128 +165,6 @@ pub fn account_update_to_account(account_update: &SubscribeUpdateAccountInfo) -> Ok(account) } -pub struct BankAccountWithPriceFeedEva<'a, T: OracleWrapperTrait> { - pub bank: BankWrapper, - pub oracle: T, - balance: &'a Balance, -} - -impl<'a, T: OracleWrapperTrait> BankAccountWithPriceFeedEva<'a, T> { - pub fn load( - lending_account: &'a LendingAccount, - cache: &Arc, - ) -> Result>> { - let active_balances = lending_account - .balances - .iter() - .filter(|balance| balance.is_active()); - - active_balances - .map(move |balance| { - let bank_wrapper = cache.banks.try_get_bank(&balance.bank_pk)?; - let oracle_wrapper = T::build(cache, &balance.bank_pk)?; - Ok(BankAccountWithPriceFeedEva { - bank: bank_wrapper, - oracle: oracle_wrapper, - balance, - }) - }) - .collect::>>() - } - - #[inline(always)] - /// Calculate the value of weighted assets and liabilities of the account in the form of (assets, liabilities) - /// - /// Nuances: - /// 1. Maintenance requirement is calculated using the real time price feed. - /// 2. Initial requirement is calculated using the time weighted price feed, if available. - /// 3. Initial requirement is discounted by the initial discount, if enabled and the usd limit is exceeded. - /// 4. Assets are only calculated for collateral risk tier. - /// 5. Oracle errors are ignored for deposits in isolated risk tier. - pub fn calc_weighted_assets_liabs( - &self, - requirement_type: RequirementType, - emode_config: &EmodeConfig, - ) -> Result<(I80F48, I80F48)> { - match self.balance.get_side() { - Some(side) => match side { - BalanceSide::Assets => Ok(( - self.calc_weighted_assets(requirement_type, emode_config)?, - I80F48::ZERO, - )), - BalanceSide::Liabilities => { - Ok((I80F48::ZERO, self.calc_weighted_liabs(requirement_type)?)) - } - }, - None => Ok((I80F48::ZERO, I80F48::ZERO)), - } - } - - #[inline(always)] - fn calc_weighted_assets( - &self, - requirement_type: RequirementType, - emode_config: &EmodeConfig, - ) -> Result { - let bank = &self.bank.bank; - match bank.config.risk_tier { - RiskTier::Collateral => { - let amount = bank - .get_asset_amount(self.balance.asset_shares.into()) - .map_err(Error::from)?; - - calc_weighted_bank_assets( - bank, - &self.oracle, - amount, - requirement_type, - emode_config, - ) - } - RiskTier::Isolated => Ok(I80F48::ZERO), - } - } - - #[inline(always)] - fn calc_weighted_liabs(&self, requirement_type: RequirementType) -> Result { - let bank = &self.bank.bank; - let liability_amount = bank - .get_liability_amount(self.balance.liability_shares.into()) - .map_err(|err| anyhow!("Failed to calculate liability amount: {}", err))?; - calc_weighted_bank_liabs(bank, &self.oracle, liability_amount, requirement_type) - } -} - -pub fn calc_total_weighted_assets_liabs( - cache: &Arc, - account: &LendingAccount, - requirement_type: RequirementType, -) -> Result<(I80F48, I80F48)> { - let baws = BankAccountWithPriceFeedEva::::load(account, cache)?; - let emode_config = build_emode_config(&baws)?; - - let mut total_assets = I80F48::ZERO; - let mut total_liabs = I80F48::ZERO; - - for baw in baws.iter() { - let (assets, liabs) = baw.calc_weighted_assets_liabs(requirement_type, &emode_config)?; - total_assets += assets; - total_liabs += liabs; - } - - Ok((total_assets, total_liabs)) -} - -pub fn build_emode_config( - baws: &Vec>, -) -> Result { - let configs = baws - .iter() - .filter(|baw| !baw.balance.is_empty(BalanceSide::Liabilities)) - .map(|baw| baw.bank.bank.emode.emode_config); - Ok(reconcile_emode_configs(configs)) -} - pub fn find_bank_liquidity_vault_authority(bank_pk: &Pubkey, program_id: &Pubkey) -> Pubkey { Pubkey::find_program_address( bank_authority_seed!(BankVaultType::Liquidity, bank_pk), @@ -325,111 +173,6 @@ pub fn find_bank_liquidity_vault_authority(bank_pk: &Pubkey, program_id: &Pubkey .0 } -pub fn calc_weighted_bank_assets( - bank: &Bank, - oracle_wrapper: &impl OracleWrapperTrait, - amount: I80F48, - requirement_type: RequirementType, - emode_config: &EmodeConfig, -) -> Result { - let mut asset_weight = calculate_bank_asset_weight(bank, emode_config, requirement_type); - - let price_bias = if matches!(requirement_type, RequirementType::Equity) { - None - } else { - Some(PriceBias::Low) - }; - - let lower_price = oracle_wrapper.get_price_of_type( - requirement_type.get_oracle_price_type(), - price_bias, - bank.config.oracle_max_confidence, - )?; - - if matches!(requirement_type, RequirementType::Initial) { - if let Some(discount) = bank.maybe_get_asset_weight_init_discount(lower_price)? { - asset_weight = asset_weight - .checked_mul(discount) - .ok_or_else(|| anyhow!("math error"))?; - } - } - - let decimals = if bank.config.asset_tag == ASSET_TAG_DRIFT { - DRIFT_SCALED_BALANCE_DECIMALS - } else { - bank.mint_decimals - }; - - Ok(calc_value( - amount, - lower_price, - decimals, - Some(asset_weight), - )?) -} - -#[inline(always)] -// Copy pasta from https://github.com/mrgnlabs/marginfi-v2/blob/87f1b8fdcde591566ab51e26a3c47554af4bf856/programs/marginfi/src/state/marginfi_account.rs#L322 -// TODO: replace with the on-chain program function call when it becomes available -fn calculate_bank_asset_weight( - bank: &Bank, - emode_config: &EmodeConfig, - requirement_type: RequirementType, -) -> I80F48 { - if let Some(emode_entry) = emode_config.find_with_tag(bank.emode.emode_tag) { - let bank_weight = bank - .config - .get_weight(requirement_type, BalanceSide::Assets); - let emode_weight = match requirement_type { - RequirementType::Initial => I80F48::from(emode_entry.asset_weight_init), - RequirementType::Maintenance => I80F48::from(emode_entry.asset_weight_maint), - // Note: For equity (which is only used for bankruptcies) emode does not - // apply, as the asset weight is always 1 - RequirementType::Equity => I80F48::ONE, - }; - max(bank_weight, emode_weight) - } else { - bank.config - .get_weight(requirement_type, BalanceSide::Assets) - } -} - -#[inline(always)] -pub fn calc_weighted_bank_liabs( - bank: &Bank, - oracle_wrapper: &impl OracleWrapperTrait, - amount: I80F48, - requirement_type: RequirementType, -) -> Result { - let liability_weight = bank - .config - .get_weight(requirement_type, BalanceSide::Liabilities); - - let price_bias = if matches!(requirement_type, RequirementType::Equity) { - None - } else { - Some(PriceBias::High) - }; - - let higher_price = oracle_wrapper.get_price_of_type( - requirement_type.get_oracle_price_type(), - price_bias, - bank.config.oracle_max_confidence, - )?; - - let decimals = if bank.config.asset_tag == ASSET_TAG_DRIFT { - DRIFT_SCALED_BALANCE_DECIMALS - } else { - bank.mint_decimals - }; - Ok(calc_value( - amount, - higher_price, - decimals, - Some(liability_weight), - )?) -} - pub fn find_oracle_keys(bank_config: &BankConfig) -> Vec { bank_config .oracle_keys @@ -491,31 +234,19 @@ pub fn log_genuine_error(prefix: &str, error: Error) { } } -// TODO: expose from program -pub fn check_asset_tags_matching(bank: &Bank, lending_account: &LendingAccount) -> bool { - let mut has_default_asset = false; - let mut has_staked_asset = false; - - for balance in lending_account.balances.iter() { - if balance.is_active() { - match balance.bank_asset_tag { - ASSET_TAG_DEFAULT => has_default_asset = true, - ASSET_TAG_SOL => { /* Do nothing, SOL can mix with any asset type */ } - ASSET_TAG_STAKED => has_staked_asset = true, - // Kamino isn't strictly a default asset but it's close enough - ASSET_TAG_KAMINO => has_default_asset = true, - _ => panic!("unsupported asset tag"), - } - } - } +pub fn format_error_chain(err: &Error) -> String { + let mut chain = err.chain(); + let primary = chain + .next() + .map(|cause| cause.to_string()) + .unwrap_or_else(|| "unknown error".to_string()); - if bank.config.asset_tag == ASSET_TAG_DEFAULT { - has_default_asset = true; - } else if bank.config.asset_tag == ASSET_TAG_STAKED { - has_staked_asset = true; + let causes = chain.map(ToString::to_string).collect::>(); + if causes.is_empty() { + return primary; } - !(has_default_asset && has_staked_asset) + format!("{primary} | caused by: {}", causes.join(" -> ")) } pub fn marginfi_account_by_authority( @@ -562,8 +293,9 @@ pub fn marginfi_account_by_authority( mod tests { use crate::utils::find_oracle_keys; + use anyhow::anyhow; - use super::accessor; + use super::{accessor, format_error_chain}; use marginfi_type_crate::types::{BankConfig, OracleSetup}; use solana_program::pubkey::Pubkey; @@ -679,4 +411,18 @@ mod tests { assert_eq!(keys.len(), 1); assert_eq!(keys[0], feed_id); } + + #[test] + fn test_format_error_chain_includes_all_causes() { + let err = anyhow!("root failure") + .context("middle failure") + .context("top-level failure"); + + let formatted = format_error_chain(&err); + + assert!(formatted.contains("top-level failure")); + assert!(formatted.contains("middle failure")); + assert!(formatted.contains("root failure")); + assert!(formatted.contains("caused by:")); + } } diff --git a/src/utils/simulation_cache.rs b/src/utils/simulation_cache.rs new file mode 100644 index 00000000..9c44dad3 --- /dev/null +++ b/src/utils/simulation_cache.rs @@ -0,0 +1,36 @@ +use anyhow::{anyhow, Result}; +use solana_account_decoder::UiAccount; +use solana_sdk::{account::Account, pubkey::Pubkey}; + +pub fn decode_and_apply_simulated_accounts( + addresses: &[Pubkey], + simulated_accounts: &[Option], + source: &str, + mut apply: F, +) -> Result<()> +where + F: FnMut(&Pubkey, Account) -> Result<()>, +{ + if simulated_accounts.len() != addresses.len() { + return Err(anyhow!( + "{} returned {} accounts, expected {}", + source, + simulated_accounts.len(), + addresses.len() + )); + } + + for (address, ui_account_opt) in addresses.iter().zip(simulated_accounts.iter()) { + let Some(ui_account) = ui_account_opt else { + return Err(anyhow!("{} returned null account for {}", source, address)); + }; + + let account = ui_account + .decode::() + .ok_or_else(|| anyhow!("Failed to decode simulated account for {}", address))?; + + apply(address, account)?; + } + + Ok(()) +} diff --git a/src/utils/swb_cranker.rs b/src/utils/swb_cranker.rs index 27127cd4..b27fad13 100644 --- a/src/utils/swb_cranker.rs +++ b/src/utils/swb_cranker.rs @@ -1,4 +1,4 @@ -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, Context, Result}; use base64::{prelude::BASE64_STANDARD, Engine}; use log::warn; use serde::Deserialize; @@ -15,7 +15,6 @@ use solana_client::{ rpc_request::{RpcError, RpcRequest}, }; use solana_sdk::{ - account::Account, commitment_config::{CommitmentConfig, CommitmentLevel}, genesis_config::ClusterType, message::{v0, VersionedMessage}, @@ -24,16 +23,12 @@ use solana_sdk::{ signer::Signer, transaction::VersionedTransaction, }; -use std::str::FromStr; use switchboard_on_demand_client::{ CrossbarClient, FetchUpdateManyParams, Gateway, PullFeed, QueueAccountData, SbContext, }; use tokio::runtime::{Builder, Runtime}; -use crate::config::Eva01Config; - -//TODO: parametrize the Swb Program ID. -pub const SWB_PROGRAM_ID: &str = "A43DyUGA7s8eXPxqEjJY6EBu1KKbNgfxF8h17VAHn13w"; +use crate::{config::Eva01Config, utils::simulation_cache::decode_and_apply_simulated_accounts}; pub const SWB_STALE_PRICE_ERROR_CODE: &str = "17a1"; pub const SWB_STALE_PRICE_ERROR_CODE_NUMBER: u32 = 6049; @@ -42,6 +37,8 @@ pub const SWB_STALE_HANDLED_ERROR: &str = "STALE HANDLED"; const CHUNK_SIZE: usize = 6; const JITO_SIMULATE_BUNDLE_METHOD: &str = "simulateBundle"; const RAW_SIMULATE_BUNDLE_RESPONSE_LOG_LIMIT: usize = 8_000; +const SIMULATION_LOG_LINE_LIMIT: usize = 30; +const SIMULATION_LOG_CHAR_LIMIT: usize = 8_000; struct SimulateBundleTx { encoded_tx: String, @@ -60,7 +57,8 @@ struct RpcSimulateBundleResult { #[serde(rename_all = "camelCase")] struct RpcSimulateBundleTransactionResult { err: Option, - post_execution_accounts: Option>>, + #[serde(default)] + logs: Option>, } pub struct SwbCranker { @@ -92,7 +90,7 @@ impl SwbCranker { ); let queue = tokio_rt.block_on(QueueAccountData::load( &non_blocking_rpc_client, - &Pubkey::from_str(SWB_PROGRAM_ID).unwrap(), + &config.swb_program_id, ))?; // Prefer private gateway from env; fall back to first on-chain gateway @@ -145,9 +143,19 @@ impl SwbCranker { let bundle_txs: Vec = self .all_swb_oracles .chunks(CHUNK_SIZE) - .map(|chunk| { + .enumerate() + .map(|(chunk_index, chunk)| { let chunk_oracles = chunk.to_vec(); - let tx = self.build_crank_transaction(chunk_oracles.clone())?; + let tx = self + .build_crank_transaction(chunk_oracles.clone()) + .with_context(|| { + format!( + "failed to build SWB simulation transaction for chunk {} ({} feeds): {:?}", + chunk_index, + chunk_oracles.len(), + chunk_oracles + ) + })?; let encoded_tx = BASE64_STANDARD.encode(bincode::serialize(&tx)?); Ok(SimulateBundleTx { encoded_tx, @@ -157,45 +165,19 @@ impl SwbCranker { }) .collect::>>()?; - let (simulation_result, raw_response) = match self.simulate_bundle(&bundle_txs) { - Ok(result) => result, - Err(err) if err.to_string().contains("incomplete postExecutionAccounts") => { - warn!( - "simulateBundle omitted postExecutionAccounts; falling back to simulateTransaction for account capture" - ); - let tx_accounts = self.simulate_transactions_for_accounts(&bundle_txs)?; - - for (bundle_tx, post_execution_accounts) in - bundle_txs.iter().zip(tx_accounts.iter()) - { - for (oracle_address, ui_account_opt) in bundle_tx - .oracle_addresses - .iter() - .zip(post_execution_accounts.iter()) - { - let Some(ui_account) = ui_account_opt else { - return Err(anyhow!( - "simulateTransaction returned null account for oracle {}", - oracle_address - )); - }; - - let account = ui_account.decode::().ok_or_else(|| { - anyhow!("Failed to decode simulated account for {}", oracle_address) - })?; - - cache.oracles.try_update(oracle_address, account)?; - } - } - - return Ok(()); - } - Err(err) => return Err(err), - }; + let (simulation_result, raw_response) = self.simulate_bundle(&bundle_txs)?; if let Some(summary) = simulation_result.summary.as_ref() { if !simulation_summary_succeeded(summary) { - warn!("simulateBundle summary indicates failure: {}", summary); + let raw = truncate_for_log( + &raw_response.to_string(), + RAW_SIMULATE_BUNDLE_RESPONSE_LOG_LIMIT, + ); + return Err(anyhow!( + "simulateBundle summary indicates failure: {} (raw response truncated: {})", + summary, + raw + )); } } @@ -221,49 +203,41 @@ impl SwbCranker { )); } - for (bundle_tx, tx_result) in bundle_txs + for (chunk_index, (bundle_tx, tx_result)) in bundle_txs .iter() .zip(simulation_result.transaction_results.iter()) + .enumerate() { - if tx_result.err.is_some() { - warn!( - "simulateBundle returned transaction error for {} feeds: {:?}", - bundle_tx.oracle_addresses.len(), - tx_result.err + if let Some(err) = tx_result.err.as_ref() { + let logs = format_simulation_logs( + tx_result.logs.as_deref(), + SIMULATION_LOG_LINE_LIMIT, + SIMULATION_LOG_CHAR_LIMIT, ); - } - - let post_execution_accounts = tx_result - .post_execution_accounts - .as_ref() - .ok_or_else(|| anyhow!("simulateBundle did not return post execution accounts"))?; - - if post_execution_accounts.len() != bundle_tx.oracle_addresses.len() { return Err(anyhow!( - "simulateBundle returned {} post accounts, expected {}", - post_execution_accounts.len(), + "simulateBundle chunk {} failed for {} feeds {:?}: err={:?}; logs={}", + chunk_index, bundle_tx.oracle_addresses.len(), + bundle_tx.oracle_addresses, + err, + logs )); } + } - for (oracle_address, ui_account_opt) in bundle_tx - .oracle_addresses - .iter() - .zip(post_execution_accounts.iter()) - { - let Some(ui_account) = ui_account_opt else { - return Err(anyhow!( - "simulateBundle returned null account for oracle {}", - oracle_address - )); - }; - - let account = ui_account.decode::().ok_or_else(|| { - anyhow!("Failed to decode simulated account for {}", oracle_address) - })?; - - cache.oracles.try_update(oracle_address, account)?; - } + // Keep bundle simulation as the authoritative bundle-level check, but capture account + // states via simulateTransaction because some RPCs omit bundle pre/post account payloads. + let tx_accounts = self + .simulate_transactions_for_accounts(&bundle_txs) + .context("failed to capture simulated accounts via simulateTransaction")?; + + for (bundle_tx, post_execution_accounts) in bundle_txs.iter().zip(tx_accounts.iter()) { + decode_and_apply_simulated_accounts( + &bundle_tx.oracle_addresses, + post_execution_accounts, + "simulateTransaction", + |oracle_address, account| cache.oracles.try_update(oracle_address, account), + )?; } Ok(()) @@ -378,21 +352,7 @@ impl SwbCranker { anyhow!("simulateBundle response parse failed: {err}") })?; - if has_complete_post_execution_accounts(&parsed, bundle_txs) { - Ok((parsed, raw_result)) - } else { - let raw = truncate_for_log( - &raw_result.to_string(), - RAW_SIMULATE_BUNDLE_RESPONSE_LOG_LIMIT, - ); - warn!( - "simulateBundle returned incomplete postExecutionAccounts. Raw response (truncated): {}", - raw - ); - Err(anyhow!( - "simulateBundle returned incomplete postExecutionAccounts" - )) - } + Ok((parsed, raw_result)) } fn simulate_transactions_for_accounts( @@ -401,7 +361,8 @@ impl SwbCranker { ) -> Result>>> { bundle_txs .iter() - .map(|bundle_tx| { + .enumerate() + .map(|(chunk_index, bundle_tx)| { let accounts_config = RpcSimulateTransactionAccountsConfig { encoding: Some(UiAccountEncoding::Base64), addresses: bundle_tx @@ -421,28 +382,50 @@ impl SwbCranker { let response = self .rpc_client - .simulate_transaction_with_config(&bundle_tx.transaction, config)?; - - if response.value.err.is_some() { - warn!( - "simulateTransaction fallback returned transaction error for {} feeds: {:?}", - bundle_tx.oracle_addresses.len(), - response.value.err + .simulate_transaction_with_config(&bundle_tx.transaction, config) + .with_context(|| { + format!( + "simulateTransaction RPC failed for chunk {} ({} feeds): {:?}", + chunk_index, + bundle_tx.oracle_addresses.len(), + bundle_tx.oracle_addresses + ) + })?; + + let simulation_value = response.value; + + if let Some(err) = simulation_value.err.as_ref() { + let logs = format_simulation_logs( + simulation_value.logs.as_deref(), + SIMULATION_LOG_LINE_LIMIT, + SIMULATION_LOG_CHAR_LIMIT, ); + return Err(anyhow!( + "simulateTransaction chunk {} failed for {} feeds {:?}: err={:?}; logs={}", + chunk_index, + bundle_tx.oracle_addresses.len(), + bundle_tx.oracle_addresses, + err, + logs + )); } - let accounts = response.value.accounts.ok_or_else(|| { + let accounts = simulation_value.accounts.ok_or_else(|| { anyhow!( - "simulateTransaction fallback did not return accounts for {} feeds", - bundle_tx.oracle_addresses.len() + "simulateTransaction chunk {} did not return accounts for {} feeds: {:?}", + chunk_index, + bundle_tx.oracle_addresses.len(), + bundle_tx.oracle_addresses ) })?; if accounts.len() != bundle_tx.oracle_addresses.len() { return Err(anyhow!( - "simulateTransaction fallback returned {} accounts, expected {}", + "simulateTransaction chunk {} returned {} accounts, expected {} for feeds: {:?}", + chunk_index, accounts.len(), - bundle_tx.oracle_addresses.len() + bundle_tx.oracle_addresses.len(), + bundle_tx.oracle_addresses )); } @@ -462,24 +445,23 @@ fn truncate_for_log(input: &str, max_len: usize) -> String { out } -fn has_complete_post_execution_accounts( - result: &RpcSimulateBundleResult, - bundle_txs: &[SimulateBundleTx], -) -> bool { - if result.transaction_results.len() != bundle_txs.len() { - return false; +fn format_simulation_logs(logs: Option<&[String]>, max_lines: usize, max_chars: usize) -> String { + let Some(logs) = logs else { + return "none".to_string(); + }; + if logs.is_empty() { + return "none".to_string(); } - result - .transaction_results - .iter() - .zip(bundle_txs.iter()) - .all(|(tx_result, bundle_tx)| { - tx_result - .post_execution_accounts - .as_ref() - .is_some_and(|accounts| accounts.len() == bundle_tx.oracle_addresses.len()) - }) + let mut lines: Vec = logs.iter().take(max_lines).cloned().collect(); + if logs.len() > max_lines { + lines.push(format!( + "...<{} additional log lines truncated>", + logs.len() - max_lines + )); + } + + truncate_for_log(&lines.join(" | "), max_chars) } fn simulation_summary_succeeded(summary: &Value) -> bool { @@ -557,4 +539,25 @@ mod tests { }; assert!(!is_stale_swb_price_error(&err)); } + + #[test] + fn test_format_simulation_logs_with_none() { + assert_eq!( + format_simulation_logs(None, SIMULATION_LOG_LINE_LIMIT, SIMULATION_LOG_CHAR_LIMIT), + "none" + ); + } + + #[test] + fn test_format_simulation_logs_line_limit() { + let logs = vec![ + "line1".to_string(), + "line2".to_string(), + "line3".to_string(), + ]; + let formatted = format_simulation_logs(Some(&logs), 2, 1024); + assert!(formatted.contains("line1")); + assert!(formatted.contains("line2")); + assert!(formatted.contains("additional log lines truncated")); + } } diff --git a/src/wrappers/bank.rs b/src/wrappers/bank.rs index 4fba0462..aba97c25 100644 --- a/src/wrappers/bank.rs +++ b/src/wrappers/bank.rs @@ -1,27 +1,24 @@ use crate::wrappers::oracle::OracleWrapperTrait; use fixed::types::I80F48; -use marginfi::{ - constants::DRIFT_SCALED_BALANCE_DECIMALS, - state::{ - marginfi_account::{calc_amount, calc_value, RequirementType}, - price::{OraclePriceType, PriceBias}, - }, -}; -use marginfi_type_crate::{ - constants::ASSET_TAG_DRIFT, - types::{BalanceSide, Bank}, -}; +use marginfi::state::marginfi_account::{calc_amount, calc_value}; +use marginfi_type_crate::types::{BalanceSide, Bank, OraclePriceType, PriceBias, RequirementType}; use solana_program::pubkey::Pubkey; +use solana_sdk::account::Account; #[derive(Clone)] pub struct BankWrapper { pub address: Pubkey, pub bank: Bank, + pub account: Account, } impl BankWrapper { - pub fn new(address: Pubkey, bank: Bank) -> Self { - Self { address, bank } + pub fn new(address: Pubkey, bank: Bank, account: Account) -> Self { + Self { + address, + bank, + account, + } } fn get_pricing_params( @@ -85,207 +82,8 @@ impl BankWrapper { Ok(calc_value( amount, price, - self.get_balance_decimals(), + self.bank.get_balance_decimals(), None, )?) } - - // TODO: export from type crate - pub fn get_balance_decimals(&self) -> u8 { - if self.bank.config.asset_tag == ASSET_TAG_DRIFT { - DRIFT_SCALED_BALANCE_DECIMALS - } else { - self.bank.mint_decimals - } - } -} - -#[cfg(test)] -pub mod test_utils { - use crate::wrappers::oracle::test_utils::TestOracleWrapper; - - use super::*; - use marginfi::state::bank::BankImpl; - use marginfi_type_crate::types::{BankConfig, OracleSetup}; - use std::str::FromStr; - - const SOL_BANK_ADDRESS: &str = "1111111Bs8Haw3nAsWf5hmLfKzc6PMEzcxUCKkVYK"; - const USDC_BANK_ADDRESS: &str = "11111117353mdUKehx9GW6JNHznGt5oSZs9fWkVkB"; - const BONK_BANK_ADDRESS: &str = "DeyH7QxWvnbbaVB4zFrf4hoq7Q8z1ZT14co42BGwGtfM"; - - pub fn test_sol() -> BankWrapper { - let mut bank = Bank::new( - Pubkey::new_unique(), - BankConfig::default(), - Pubkey::new_unique(), - 6u8, - Pubkey::new_unique(), - Pubkey::new_unique(), - Pubkey::new_unique(), - 0i64, - 0u8, - 0u8, - 0u8, - 0u8, - 0u8, - 0u8, - ); - - let oracle = TestOracleWrapper::test_sol(); - bank.config.oracle_setup = OracleSetup::PythPushOracle; - bank.config.oracle_keys[0] = oracle.address; - BankWrapper::new(Pubkey::from_str(SOL_BANK_ADDRESS).unwrap(), bank) - } - - pub fn test_usdc() -> BankWrapper { - let mut bank = Bank::new( - Pubkey::new_unique(), - BankConfig::default(), - Pubkey::new_unique(), - 2u8, - Pubkey::new_unique(), - Pubkey::new_unique(), - Pubkey::new_unique(), - 0i64, - 0u8, - 0u8, - 0u8, - 0u8, - 0u8, - 0u8, - ); - let oracle = TestOracleWrapper::test_usdc(); - bank.config.oracle_setup = OracleSetup::PythPushOracle; - bank.config.oracle_keys[0] = oracle.address; - BankWrapper::new(Pubkey::from_str(USDC_BANK_ADDRESS).unwrap(), bank) - } - - pub fn test_bonk() -> BankWrapper { - let mut bank = Bank::new( - Pubkey::new_unique(), - BankConfig::default(), - Pubkey::new_unique(), - 2u8, - Pubkey::new_unique(), - Pubkey::new_unique(), - Pubkey::new_unique(), - 0i64, - 0u8, - 0u8, - 0u8, - 0u8, - 0u8, - 0u8, - ); - let oracle = TestOracleWrapper::test_bonk(); - bank.config.oracle_setup = OracleSetup::SwitchboardPull; - bank.config.oracle_keys[0] = oracle.address; - BankWrapper::new(Pubkey::from_str(BONK_BANK_ADDRESS).unwrap(), bank) - } -} -#[cfg(test)] -mod tests { - use crate::wrappers::oracle::test_utils::TestOracleWrapper; - - use super::*; - - fn setup_sol_bank_and_oracle() -> (BankWrapper, TestOracleWrapper) { - let bank = test_utils::test_sol(); - let oracle = TestOracleWrapper::test_sol(); - (bank, oracle) - } - - fn setup_usdc_bank_and_oracle() -> (BankWrapper, TestOracleWrapper) { - let bank = test_utils::test_usdc(); - let oracle = TestOracleWrapper::test_usdc(); - (bank, oracle) - } - - #[test] - fn test_calc_amount_sol_assets_initial() { - let (bank, oracle) = setup_sol_bank_and_oracle(); - let value = I80F48::from_num(1000); - let result = bank.calc_amount( - &oracle, - value, - BalanceSide::Assets, - RequirementType::Initial, - ); - assert!(result.is_ok()); - let amount = result.unwrap(); - assert!(amount > I80F48::from_num(0)); - } - - #[test] - fn test_calc_amount_sol_liabilities_maintenance() { - let (bank, oracle) = setup_sol_bank_and_oracle(); - let value = I80F48::from_num(500); - let result = bank.calc_amount( - &oracle, - value, - BalanceSide::Liabilities, - RequirementType::Maintenance, - ); - assert!(result.is_ok()); - let amount = result.unwrap(); - assert!(amount > I80F48::from_num(0)); - } - - #[test] - fn test_calc_value_usdc_assets_initial() { - let (bank, oracle) = setup_usdc_bank_and_oracle(); - let amount = I80F48::from_num(100); - let result = bank.calc_value( - &oracle, - amount, - BalanceSide::Assets, - RequirementType::Initial, - ); - assert!(result.is_ok()); - let value = result.unwrap(); - assert!(value > I80F48::from_num(0)); - } - - #[test] - fn test_calc_value_usdc_liabilities_maintenance() { - let (bank, oracle) = setup_usdc_bank_and_oracle(); - let amount = I80F48::from_num(200); - let result = bank.calc_value( - &oracle, - amount, - BalanceSide::Liabilities, - RequirementType::Maintenance, - ); - assert!(result.is_ok()); - let value = result.unwrap(); - assert!(value > I80F48::from_num(0)); - } - - #[test] - fn test_calc_amount_with_zero_value() { - let (bank, oracle) = setup_sol_bank_and_oracle(); - let value = I80F48::from_num(0); - let result = bank.calc_amount( - &oracle, - value, - BalanceSide::Assets, - RequirementType::Initial, - ); - assert!(result.is_ok()); - assert_eq!(result.unwrap(), I80F48::from_num(0)); - } - - #[test] - fn test_calc_value_with_zero_amount() { - let (bank, oracle) = setup_usdc_bank_and_oracle(); - let amount = I80F48::from_num(0); - let result = bank.calc_value( - &oracle, - amount, - BalanceSide::Assets, - RequirementType::Initial, - ); - assert!(result.is_ok()); - assert_eq!(result.unwrap(), I80F48::from_num(0)); - } } diff --git a/src/wrappers/liquidator_account.rs b/src/wrappers/liquidator_account.rs index 48dfd366..264f62aa 100644 --- a/src/wrappers/liquidator_account.rs +++ b/src/wrappers/liquidator_account.rs @@ -1,6 +1,10 @@ -use super::{bank::BankWrapper, marginfi_account::MarginfiAccountWrapper}; +use super::{ + bank::BankWrapper, + marginfi_account::{MarginfiAccountWrapper, ObservationAccounts}, +}; use crate::{ cache::{Cache, DriftSpotMarket}, + clock_manager, config::Eva01Config, drift_ixs::make_refresh_spot_market_ix, juplend_ixs::make_update_lending_rate_ix, @@ -12,7 +16,7 @@ use crate::{ }, metrics::{LIQUIDATION_ATTEMPTS, LIQUIDATION_LATENCY_SECONDS, LIQUIDATION_SUCCESSES}, utils::{ - self, check_asset_tags_matching, drift::derive_spot_market, marginfi_account_by_authority, + self, marginfi_account_by_authority, simulation_cache::decode_and_apply_simulated_accounts, swb_cranker::is_stale_swb_price_error, }, wrappers::oracle::OracleWrapper, @@ -24,12 +28,17 @@ use marginfi_type_crate::{ constants::{ ASSET_TAG_DEFAULT, ASSET_TAG_DRIFT, ASSET_TAG_JUPLEND, ASSET_TAG_KAMINO, ASSET_TAG_SOL, }, - types::{BalanceSide, Bank}, + pdas::derive_drift_spot_market, + types::{validate_asset_tags, Bank}, }; +use solana_account_decoder::UiAccountEncoding; use solana_client::{ client_error::{ClientError, ClientErrorKind}, rpc_client::RpcClient, - rpc_config::RpcSendTransactionConfig, + rpc_config::{ + RpcSendTransactionConfig, RpcSimulateTransactionAccountsConfig, + RpcSimulateTransactionConfig, + }, rpc_request::RpcError, }; @@ -45,11 +54,17 @@ use solana_sdk::{ signature::{Keypair, Signature}, signer::{Signer, SignerError}, system_instruction::transfer, - transaction::VersionedTransaction, + transaction::{TransactionError, VersionedTransaction}, +}; +use std::{ + collections::HashSet, + sync::{Arc, Mutex}, + thread, + time::Duration, }; -use std::{collections::HashSet, sync::Arc, thread, time::Duration}; pub const PROFIT_SHARE: f64 = 0.085; +const DEFAULT_INTEGRATION_REFRESH_BATCH_HINT: usize = 12; #[derive(Debug)] pub enum LiquidationError { @@ -78,6 +93,7 @@ impl LiquidationError { pub struct PreparedLiquidatableAccount { pub liquidatee_account: MarginfiAccountWrapper, + pub observation_accounts: ObservationAccounts, pub asset_bank: Pubkey, pub liab_bank: Pubkey, pub asset_amount: I80F48, @@ -95,6 +111,20 @@ pub struct LiquidatorAccount { rpc_client: RpcClient, cu_limit_ix: Instruction, pub cache: Arc, + integration_refresh_batch_hint: Mutex, +} + +#[derive(Clone, Debug)] +struct IntegrationRefreshEntry { + kind: &'static str, + address: Pubkey, + instruction: Instruction, +} + +enum RefreshBatchError { + TooLarge(anyhow::Error), + TooManyAccountLocks(anyhow::Error), + Other(anyhow::Error), } impl LiquidatorAccount { @@ -154,28 +184,10 @@ impl LiquidatorAccount { rpc_client, cu_limit_ix: ComputeBudgetInstruction::set_compute_unit_limit(1400000), cache, + integration_refresh_batch_hint: Mutex::new(DEFAULT_INTEGRATION_REFRESH_BATCH_HINT), }) } - pub fn has_funds(&self) -> Result { - let account = self - .cache - .marginfi_accounts - .try_get_account(&self.liquidator_address)?; - - let preferred_mint_bank = self.cache.banks.try_get_bank(&self.preferred_mint_bank)?; - - let validation_result = - account - .get_balance_for_bank(&preferred_mint_bank) - .map(|(balance, side)| match side { - BalanceSide::Assets => balance > 0, - _ => true, - }); - - Ok(validation_result.unwrap_or(false)) - } - pub fn init_liq_record(&self, liquidatee_account: &MarginfiAccountWrapper) -> Result { info!( "Initializing liquidation record for account {:?} with liquidator account {:?}.", @@ -229,12 +241,13 @@ impl LiquidatorAccount { pub fn liquidate( &self, - account: &PreparedLiquidatableAccount, + account: PreparedLiquidatableAccount, stale_swb_oracles: &HashSet, tokens_in_shortage: &mut HashSet, ) -> Result<(), LiquidationError> { let PreparedLiquidatableAccount { liquidatee_account, + observation_accounts, asset_bank, liab_bank, asset_amount, @@ -250,13 +263,13 @@ impl LiquidatorAccount { let asset_bank_wrapper = self .cache .banks - .try_get_bank(asset_bank) + .try_get_bank(&asset_bank) .map_err(LiquidationError::from_anyhow)?; let liab_bank_wrapper = self .cache .banks - .try_get_bank(liab_bank) + .try_get_bank(&liab_bank) .map_err(LiquidationError::from_anyhow)?; let signer_pk = self.signer.pubkey(); @@ -276,9 +289,8 @@ impl LiquidatorAccount { .try_get_account(&self.liquidator_address) .map_err(LiquidationError::from_anyhow)?; - let lending_account = &liquidator_account.lending_account; for bank_to_validate_against in [&asset_bank_wrapper, &liab_bank_wrapper] { - if !check_asset_tags_matching(&bank_to_validate_against.bank, lending_account) { + if !validate_asset_tags(&bank_to_validate_against.bank, &liquidator_account.account) { // This is a precaution to not attempt to liquidate staked collateral positions when liquidator has non-SOL positions open. // Expected to happen quite often for now. Later on, we can add a more sophisticated filtering logic on the higher level. debug!("Bank {:?} does not match the asset tags of the lending account -> skipping liquidation attempt", bank_to_validate_against.address); @@ -289,48 +301,40 @@ impl LiquidatorAccount { let liab_token_balance = I80F48::from_num(self.get_token_balance_for_mint(&liab_mint).unwrap()); - if liab_token_balance < *dust_liab_threshold { + if liab_token_balance < dust_liab_threshold { tokens_in_shortage.insert(liab_mint); info!("No tokens: {}", liab_mint); return Err(LiquidationError::NotEnoughFunds); } - let (asset_amount, liab_amount) = if liab_token_balance < *liab_amount { + let (asset_amount, liab_amount) = if liab_token_balance < liab_amount { tokens_in_shortage.insert(liab_mint); info!( "Not enough {} tokens: liquidating for: {} (of {})", liab_mint, liab_token_balance, liab_amount ); - let proportion = liab_token_balance.checked_div(*liab_amount).unwrap(); + let proportion = liab_token_balance.checked_div(liab_amount).unwrap(); ( asset_amount.checked_mul(proportion).unwrap(), liab_token_balance, ) } else { ( - *asset_amount, + asset_amount, liab_amount .checked_mul(I80F48::from_num(1.0 - PROFIT_SHARE)) .unwrap(), ) }; - let banks_to_include: Vec = vec![]; - let banks_to_exclude: Vec = vec![]; - let ( - liquidatee_observation_accounts, - liquidatee_swb_oracles, - liquidatee_banks, + let ObservationAccounts { + observation_accounts: liquidatee_observation_accounts, + swb_oracles: liquidatee_swb_oracles, + bank_pks: liquidatee_banks, kamino_reserves, drift_spot_markets, juplend_states, - ) = MarginfiAccountWrapper::get_observation_accounts::( - &liquidatee_account.lending_account, - &banks_to_include, - &banks_to_exclude, - self.cache.clone(), - ) - .map_err(LiquidationError::from_anyhow)?; + } = observation_accounts; // Note: liquidatee_observation_accounts include Kamino reserves, Drift spot markets and all of the banks and oracles. participating_accounts.extend(liquidatee_observation_accounts.iter()); @@ -344,14 +348,15 @@ impl LiquidatorAccount { return Ok(()); } - let liquidation_record = if liquidatee_account.liquidation_record == Pubkey::default() { - // warn!("IGNORING UNINITIALIZED LIQ RECORD"); - // return Ok(()); - self.init_liq_record(liquidatee_account) - .map_err(LiquidationError::from_anyhow)? - } else { - liquidatee_account.liquidation_record - }; + let liquidation_record = + if liquidatee_account.account.liquidation_record == Pubkey::default() { + // warn!("IGNORING UNINITIALIZED LIQ RECORD"); + // return Ok(()); + self.init_liq_record(&liquidatee_account) + .map_err(LiquidationError::from_anyhow)? + } else { + liquidatee_account.account.liquidation_record + }; participating_accounts.insert(liquidation_record); let luts: Vec = self.cache.luts.lock().unwrap().clone(); @@ -359,6 +364,8 @@ impl LiquidatorAccount { ixs.push(self.cu_limit_ix.clone()); + // TODO: think about posting an swb_crank ix here + let start_ix = make_start_liquidate_ix( self.program_id, liquidatee_account_address, @@ -372,12 +379,7 @@ impl LiquidatorAccount { for kamino_reserve_address in kamino_reserves { let kamino_reserve = self .cache - .kamino_reserves - .get(&kamino_reserve_address) - .context(format!( - "Couldn't find the data for Kamino reserve: {}", - kamino_reserve_address - )) + .try_get_kamino_reserve(&kamino_reserve_address) .map_err(LiquidationError::from_anyhow)?; debug!( @@ -387,7 +389,7 @@ impl LiquidatorAccount { let refresh_reserve_ix = make_refresh_reserve_ix( kamino_reserve_address, - kamino_reserve, + &kamino_reserve, &mut participating_accounts, ); ixs.push(refresh_reserve_ix); @@ -396,12 +398,7 @@ impl LiquidatorAccount { for spot_market_address in drift_spot_markets { let spot_market = self .cache - .drift_markets - .get(&spot_market_address) - .context(format!( - "Couldn't find the data for Drift spot market: {}", - spot_market_address - )) + .try_get_drift_market(&spot_market_address) .map_err(LiquidationError::from_anyhow)?; let refresh_spot_market_ix = make_refresh_spot_market_ix( @@ -416,17 +413,12 @@ impl LiquidatorAccount { for lending_state_address in juplend_states { let lending_state = self .cache - .juplend_lending_states - .get(&lending_state_address) - .context(format!( - "Couldn't find the data for Juplend lending state: {}", - lending_state_address - )) + .try_get_juplend_lending_state(&lending_state_address) .map_err(LiquidationError::from_anyhow)?; let update_lending_rate_ix = make_update_lending_rate_ix( lending_state_address, - lending_state, + &lending_state, &mut participating_accounts, ); ixs.push(update_lending_rate_ix); @@ -454,12 +446,7 @@ impl LiquidatorAccount { ASSET_TAG_KAMINO => { let kamino_reserve = self .cache - .kamino_reserves - .get(&asset_bank_wrapper.bank.integration_acc_1) - .context(format!( - "Couldn't find the data for Kamino reserve: {}", - asset_bank_wrapper.bank.integration_acc_1 - )) + .try_get_kamino_reserve(&asset_bank_wrapper.bank.integration_acc_1) .map_err(LiquidationError::from_anyhow)?; let refresh_obligation_ix = make_refresh_obligation_ix( @@ -478,7 +465,7 @@ impl LiquidatorAccount { &asset_bank_wrapper, &asset_mint_wrapper, asset_bank_wrapper.bank.integration_acc_2, - kamino_reserve, + &kamino_reserve, liquidatee_observation_accounts.as_ref(), asset_amount.to_num(), false, @@ -497,9 +484,9 @@ impl LiquidatorAccount { signer_pk, &asset_bank_wrapper, &asset_mint_wrapper, - drift_spot_market, - reward_spot_market, - reward_spot_market_2, + &drift_spot_market, + reward_spot_market.as_ref(), + reward_spot_market_2.as_ref(), liquidatee_observation_accounts.as_ref(), asset_amount.to_num(), false, @@ -509,12 +496,7 @@ impl LiquidatorAccount { ASSET_TAG_JUPLEND => { let lending_state = self .cache - .juplend_lending_states - .get(&asset_bank_wrapper.bank.integration_acc_1) - .context(format!( - "Couldn't find the data for Juplend lending state: {}", - asset_bank_wrapper.bank.integration_acc_1 - )) + .try_get_juplend_lending_state(&asset_bank_wrapper.bank.integration_acc_1) .map_err(LiquidationError::from_anyhow)?; make_juplend_withdraw_ix( @@ -524,7 +506,7 @@ impl LiquidatorAccount { signer_pk, &asset_bank_wrapper, &asset_mint_wrapper, - lending_state, + &lending_state, liquidatee_observation_accounts.as_ref(), asset_amount.to_num(), false, @@ -628,7 +610,6 @@ impl LiquidatorAccount { .downcast_ref::() .is_some_and(is_stale_swb_price_error) { - // TODO: Should we just crank always?? Also refresh Kamino reserves? Err(LiquidationError::StaleOracles(liquidatee_swb_oracles)) } else { Err(LiquidationError::Anyhow(err)) @@ -636,7 +617,6 @@ impl LiquidatorAccount { } } } else if is_stale_swb_price_error(&err) { - // TODO: Should we just crank always?? Also refresh Kamino reserves? Err(LiquidationError::StaleOracles(liquidatee_swb_oracles)) } else { Err(LiquidationError::from_client(err)) @@ -688,19 +668,24 @@ impl LiquidatorAccount { vec![] }; + let clock = clock_manager::get_clock(&self.cache.clock)?; + debug!("Collecting observation accounts for the account: {:?} with banks_to_include {:?} and banks_to_exclude {:?}", &self.liquidator_address, &banks_to_include, &banks_to_exclude); - let (observation_accounts, _, _, _, _, _) = + let observation_accounts = MarginfiAccountWrapper::get_observation_accounts::( &self .cache .marginfi_accounts .try_get_account(&self.liquidator_address)? + .account .lending_account, &banks_to_include, &banks_to_exclude, - self.cache.clone(), - )?; + &self.cache, + &clock, + )? + .observation_accounts; let mint_wrapper = self.cache.mints.try_get_account(&bank.bank.mint)?; let withdraw_ix = make_withdraw_ix( @@ -833,18 +818,11 @@ impl LiquidatorAccount { &self, bank: &Bank, ) -> Result<( - &DriftSpotMarket, - Option<&DriftSpotMarket>, - Option<&DriftSpotMarket>, + DriftSpotMarket, + Option, + Option, )> { - let drift_spot_market = self - .cache - .drift_markets - .get(&bank.integration_acc_1) - .context(format!( - "Couldn't find the data for Drift spot market: {}", - bank.integration_acc_1 - ))?; + let drift_spot_market = self.cache.try_get_drift_market(&bank.integration_acc_1)?; let drift_user = self .cache @@ -859,29 +837,19 @@ impl LiquidatorAccount { let (reward_spot_market, reward_spot_market_2) = if drift_user.spot_positions[2].scaled_balance > 0 { let reward_spot_market_address = - derive_spot_market(drift_user.spot_positions[2].market_index); + derive_drift_spot_market(drift_user.spot_positions[2].market_index).0; let reward_spot_market = self .cache - .drift_markets - .get(&reward_spot_market_address) - .context(format!( - "Couldn't find the data for Drift spot market: {}", - reward_spot_market_address - ))?; + .try_get_drift_market(&reward_spot_market_address)?; if drift_user.spot_positions[3].scaled_balance > 0 { let reward_spot_market_2_address = - derive_spot_market(drift_user.spot_positions[3].market_index); + derive_drift_spot_market(drift_user.spot_positions[3].market_index).0; let reward_spot_market_2 = self .cache - .drift_markets - .get(&reward_spot_market_2_address) - .context(format!( - "Couldn't find the data for Drift spot market: {}", - reward_spot_market_2_address - ))?; + .try_get_drift_market(&reward_spot_market_2_address)?; (Some(reward_spot_market), Some(reward_spot_market_2)) } else { @@ -894,89 +862,342 @@ impl LiquidatorAccount { Ok((drift_spot_market, reward_spot_market, reward_spot_market_2)) } - pub fn refresh_integrations(&self) -> Result<()> { - let luts: Vec = self.cache.luts.lock().unwrap().clone(); - let mut ixs = Vec::new(); + fn build_refresh_integrations_entries(&self) -> Result> { + let mut entries = Vec::new(); let mut participating_accounts: HashSet = HashSet::new(); - ixs.push(self.cu_limit_ix.clone()); + for (address, reserve) in self.cache.try_get_kamino_reserves()? { + let instruction = + make_refresh_reserve_ix(address, &reserve, &mut participating_accounts); + debug!( + "Refreshing: reserve {}, mint {}", + address, reserve.reserve.collateral.mint_pubkey + ); + entries.push(IntegrationRefreshEntry { + kind: "kamino", + address, + instruction, + }); + } - self.cache - .kamino_reserves - .iter() - .for_each(|(address, reserve)| { - let refresh_reserve_ix = - make_refresh_reserve_ix(*address, reserve, &mut participating_accounts); - debug!( - "Refreshing: reserve {}, mint {}", - address, reserve.reserve.collateral.mint_pubkey - ); + // TODO: bring back Drift here if it's ever resurrected - ixs.push(refresh_reserve_ix); + for (address, lending_state) in self.cache.try_get_juplend_lending_states()? { + debug!("Refreshing: state {}, mint {}", address, lending_state.mint); + let instruction = + make_update_lending_rate_ix(address, &lending_state, &mut participating_accounts); + entries.push(IntegrationRefreshEntry { + kind: "juplend", + address, + instruction, }); + } - self.cache - .drift_markets - .iter() - .for_each(|(address, spot_market)| { - debug!( - "Refreshing: market {}, mint {}, oracle {}", - address, spot_market.market.mint, spot_market.market.oracle - ); + Ok(entries) + } - let refresh_spot_market_ix = make_refresh_spot_market_ix( - *address, - spot_market.market.vault, - spot_market.market.oracle, - &mut participating_accounts, - ); + pub fn simulate_refresh_integrations(&self) -> Result<()> { + let luts: Vec = self.cache.luts.lock().unwrap().clone(); + let entries = self.build_refresh_integrations_entries()?; + if entries.is_empty() { + return Ok(()); + } - ixs.push(refresh_spot_market_ix); - }); + let mut skipped_entries: Vec = vec![]; + let mut refreshed_batches = 0usize; + let mut offset = 0usize; + let stored_batch_hint = *self.integration_refresh_batch_hint.lock().unwrap(); + let mut preferred_batch_size = stored_batch_hint.min(entries.len()).max(1); + let mut resized_down = false; + + while offset < entries.len() { + let remaining = entries.len() - offset; + let mut batch_size = preferred_batch_size.min(remaining).max(1); + + loop { + let batch_entries = entries[offset..offset + batch_size].to_vec(); + match self.simulate_refresh_integrations_batch( + &luts, + batch_entries, + &mut skipped_entries, + ) { + Ok(batch_refreshed_any) => { + if batch_refreshed_any { + refreshed_batches += 1; + } + offset += batch_size; + break; + } + Err(RefreshBatchError::TooLarge(_err)) if batch_size > 1 => { + let new_batch_size = (batch_size / 2).max(1); + debug!( + "Integrations refresh simulation tx too large with {} instructions; retrying with {} instructions", + batch_size, new_batch_size + ); + batch_size = new_batch_size; + preferred_batch_size = new_batch_size; + resized_down = true; + } + Err(RefreshBatchError::TooManyAccountLocks(_err)) if batch_size > 1 => { + let new_batch_size = (batch_size / 2).max(1); + debug!( + "Integrations refresh simulation hit TooManyAccountLocks with {} instructions; retrying with {} instructions", + batch_size, new_batch_size + ); + batch_size = new_batch_size; + preferred_batch_size = new_batch_size; + resized_down = true; + } + Err(RefreshBatchError::TooLarge(err)) => { + let failed_entry = entries[offset].clone(); + warn!( + "Skipping integration refresh instruction {} for {} because tx is too large even as a single instruction: {}", + failed_entry.kind, + failed_entry.address, + err + ); + skipped_entries.push(failed_entry); + offset += 1; + break; + } + Err(RefreshBatchError::TooManyAccountLocks(err)) => { + let failed_entry = entries[offset].clone(); + warn!( + "Skipping integration refresh instruction {} for {} because TooManyAccountLocks persisted even as a single instruction: {}", + failed_entry.kind, + failed_entry.address, + err + ); + skipped_entries.push(failed_entry); + offset += 1; + break; + } + Err(RefreshBatchError::Other(err)) => { + let batch_summary = + format_integration_entries(&entries[offset..offset + batch_size], 10); + return Err(err).with_context(|| { + format!( + "Integrations refresh simulation failed for batch [{}..{}) (size {}, entries: {})", + offset, + offset + batch_size, + batch_size, + batch_summary, + ) + }); + } + } + } + } - self.cache - .juplend_lending_states - .iter() - .for_each(|(address, lending_state)| { - debug!("Refreshing: state {}, mint {}", address, lending_state.mint); - - let update_lending_rate_ix = make_update_lending_rate_ix( - *address, - lending_state, - &mut participating_accounts, + if refreshed_batches == 0 { + return Err(anyhow!( + "Integrations refresh simulation failed for all integrations; skipped entries: {}", + format_skipped_integration_entries(&skipped_entries) + )); + } + + if !skipped_entries.is_empty() { + warn!( + "Integrations refresh simulation completed while skipping {} failing integrations: {}", + skipped_entries.len(), + format_skipped_integration_entries(&skipped_entries) + ); + } + + if resized_down { + let mut batch_hint = self.integration_refresh_batch_hint.lock().unwrap(); + if preferred_batch_size < *batch_hint { + info!( + "Reduced integrations refresh batch-size hint from {} to {} after simulation backoff", + *batch_hint, preferred_batch_size ); - ixs.push(update_lending_rate_ix); - }); + *batch_hint = preferred_batch_size; + } + } - let recent_blockhash = self.rpc_client.get_latest_blockhash()?; + Ok(()) + } - let signer_pk = self.signer.pubkey(); - let msg = Message::try_compile(&signer_pk, &ixs, &luts, recent_blockhash)?; + fn simulate_refresh_integrations_batch( + &self, + luts: &[AddressLookupTableAccount], + batch_entries: Vec, + skipped_entries: &mut Vec, + ) -> std::result::Result { + let mut entries = batch_entries; + + loop { + if entries.is_empty() { + return Ok(false); + } - let tx = VersionedTransaction::try_new(VersionedMessage::V0(msg), &[&self.signer])?; + let integration_addresses: Vec = + entries.iter().map(|entry| entry.address).collect(); + let entry_summary = format_integration_entries(&entries, 10); + let mut ixs: Vec = Vec::with_capacity(entries.len() + 1); + ixs.push(self.cu_limit_ix.clone()); + ixs.extend(entries.iter().map(|entry| entry.instruction.clone())); + + let recent_blockhash = self + .rpc_client + .get_latest_blockhash() + .map_err(|err| RefreshBatchError::Other(err.into()))?; + let signer_pk = self.signer.pubkey(); + let msg = Message::try_compile(&signer_pk, &ixs, luts, recent_blockhash) + .map_err(|err| RefreshBatchError::Other(err.into()))?; + let tx = VersionedTransaction::try_new(VersionedMessage::V0(msg), &[&self.signer]) + .map_err(|err| RefreshBatchError::Other(err.into()))?; + + let simulation = self + .rpc_client + .simulate_transaction_with_config( + &tx, + RpcSimulateTransactionConfig { + sig_verify: false, + replace_recent_blockhash: true, + commitment: Some(CommitmentConfig::confirmed()), + accounts: Some(RpcSimulateTransactionAccountsConfig { + encoding: Some(UiAccountEncoding::Base64), + addresses: integration_addresses + .iter() + .map(|pk| pk.to_string()) + .collect(), + }), + ..Default::default() + }, + ) + .map_err(|err| { + if is_tx_too_large_client(&err) { + RefreshBatchError::TooLarge(err.into()) + } else if is_tx_too_many_account_locks_client(&err) { + RefreshBatchError::TooManyAccountLocks(err.into()) + } else { + RefreshBatchError::Other(err.into()) + } + })?; + + if let Some(err) = simulation.value.err.clone() { + if matches!(err, TransactionError::TooManyAccountLocks) { + let logs = format_simulation_logs(simulation.value.logs.as_deref(), 40, 8_000); + return Err(RefreshBatchError::TooManyAccountLocks(anyhow!( + "Integrations refresh simulation hit TooManyAccountLocks for batch of {} entries [{}]; logs={}", + entries.len(), + entry_summary, + logs + ))); + } - if let Err(err) = self - .rpc_client - .send_and_confirm_transaction_with_spinner_and_config( - &tx, - CommitmentConfig::confirmed(), - RpcSendTransactionConfig { - skip_preflight: false, - preflight_commitment: Some(CommitmentLevel::Processed), - ..Default::default() - }, - ) - { - if is_tx_too_large_client(&err) { - warn!("The refresh tx was too large: adding the observation accounts to a LUT and retrying"); - self.retry_with_new_luts(ixs, participating_accounts)?; + if let TransactionError::InstructionError(ix_index, instruction_error) = &err { + let ix_index = usize::from(*ix_index); + if ix_index > 0 && ix_index <= entries.len() { + let failed_entry = entries.remove(ix_index - 1); + warn!( + "Skipping failing integration refresh instruction {} for {} (tx instruction index {}, error {:?})", + failed_entry.kind, + failed_entry.address, + ix_index, + instruction_error + ); + skipped_entries.push(failed_entry); + continue; + } + } + + let logs = format_simulation_logs(simulation.value.logs.as_deref(), 40, 8_000); + return Err(RefreshBatchError::Other(anyhow!( + "Integrations refresh simulation failed with transaction error: {:?}; batch_size={}; entries=[{}]; logs={}", + err, + entries.len(), + entry_summary, + logs + ))); + } + + let simulated_accounts = simulation.value.accounts.ok_or_else(|| { + RefreshBatchError::Other(anyhow!( + "Integrations refresh simulation did not return post-simulation accounts" + )) + })?; + + if simulated_accounts.len() != integration_addresses.len() { + return Err(RefreshBatchError::Other(anyhow!( + "Integrations refresh simulation returned {} accounts, expected {}", + simulated_accounts.len(), + integration_addresses.len() + ))); } - return Err(anyhow!(err.to_string())); + + decode_and_apply_simulated_accounts( + &integration_addresses, + &simulated_accounts, + "simulateTransaction integrations refresh", + |address, account| self.cache.oracles.try_update(address, account), + ) + .map_err(RefreshBatchError::Other)?; + + return Ok(true); } - Ok(()) } } +fn format_skipped_integration_entries(entries: &[IntegrationRefreshEntry]) -> String { + if entries.is_empty() { + return "none".to_string(); + } + + entries + .iter() + .map(|entry| format!("{}:{}", entry.kind, entry.address)) + .collect::>() + .join(", ") +} + +fn format_integration_entries(entries: &[IntegrationRefreshEntry], max_entries: usize) -> String { + if entries.is_empty() { + return "none".to_string(); + } + + let mut out = entries + .iter() + .take(max_entries) + .map(|entry| format!("{}:{}", entry.kind, entry.address)) + .collect::>(); + + if entries.len() > max_entries { + out.push(format!( + "...<{} additional entries>", + entries.len() - max_entries + )); + } + + out.join(", ") +} + +fn format_simulation_logs(logs: Option<&[String]>, max_lines: usize, max_chars: usize) -> String { + let Some(logs) = logs else { + return "none".to_string(); + }; + if logs.is_empty() { + return "none".to_string(); + } + + let mut lines: Vec = logs.iter().take(max_lines).cloned().collect(); + if logs.len() > max_lines { + lines.push(format!( + "...<{} additional log lines truncated>", + logs.len() - max_lines + )); + } + + let mut joined = lines.join(" | "); + if joined.len() > max_chars { + joined.truncate(max_chars); + joined.push_str("..."); + } + + joined +} + fn contains_stale_oracles(stale_oracles: &HashSet, account_oracles: &[Pubkey]) -> bool { if let Some(oracle) = account_oracles .iter() @@ -1059,3 +1280,22 @@ pub fn is_tx_too_large_client(err: &ClientError) -> bool { _ => false, } } + +fn is_tx_too_many_account_locks_client(err: &ClientError) -> bool { + match err.kind() { + ClientErrorKind::RpcError(rpc) => match rpc { + RpcError::RpcResponseError { message, .. } => { + message.contains("TooManyAccountLocks") + || message + .to_ascii_lowercase() + .contains("too many account locks") + } + RpcError::RpcRequestError(msg) | RpcError::ForUser(msg) => { + msg.contains("TooManyAccountLocks") + || msg.to_ascii_lowercase().contains("too many account locks") + } + _ => false, + }, + _ => false, + } +} diff --git a/src/wrappers/marginfi_account.rs b/src/wrappers/marginfi_account.rs index baa15693..4735f8c3 100644 --- a/src/wrappers/marginfi_account.rs +++ b/src/wrappers/marginfi_account.rs @@ -1,56 +1,73 @@ use crate::cache::Cache; -use super::{bank::BankWrapper, oracle::OracleWrapperTrait}; +use crate::wrappers::bank::BankWrapper; +use marginfi::state::bank::BankImpl; +use solana_sdk::clock::Clock; + +use super::oracle::OracleWrapperTrait; use anyhow::{Error, Result}; use fixed::types::I80F48; -use marginfi::state::bank::BankImpl; use marginfi_type_crate::types::{BalanceSide, LendingAccount, MarginfiAccount, OracleSetup}; use solana_program::pubkey::Pubkey; -use std::{collections::HashSet, sync::Arc}; +use std::collections::HashSet; #[derive(Clone)] pub struct MarginfiAccountWrapper { pub address: Pubkey, - pub liquidation_record: Pubkey, - pub lending_account: LendingAccount, + pub account: MarginfiAccount, } type Shares = Vec<(I80F48, Pubkey)>; +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ObservationAccounts { + pub observation_accounts: Vec, + pub swb_oracles: Vec, + pub bank_pks: Vec, + pub kamino_reserves: HashSet, + pub drift_spot_markets: HashSet, + pub juplend_states: HashSet, +} + impl MarginfiAccountWrapper { - pub fn new(address: Pubkey, account: &MarginfiAccount) -> Self { - MarginfiAccountWrapper { - address, - liquidation_record: account.liquidation_record, - lending_account: account.lending_account, - } + pub fn new(address: Pubkey, account: MarginfiAccount) -> Self { + MarginfiAccountWrapper { address, account } } - pub fn get_balance_for_bank(&self, bank: &BankWrapper) -> Option<(I80F48, BalanceSide)> { - self.lending_account + pub fn get_balance_for_bank(&self, bank_wrapper: &BankWrapper) -> Result<(I80F48, I80F48)> { + let balance = self + .account + .lending_account .balances .iter() - .find(|b| b.bank_pk == bank.address) - .and_then(|b| match b.get_side()? { + .find(|b| b.bank_pk == bank_wrapper.address && b.is_active()) + .map(|b| match b.get_side()? { BalanceSide::Assets => { - let amount = bank.bank.get_asset_amount(b.asset_shares.into()).ok()?; - Some((amount, BalanceSide::Assets)) + let amount = bank_wrapper + .bank + .get_asset_amount(b.asset_shares.into()) + .ok()?; + Some((amount, I80F48::ZERO)) } BalanceSide::Liabilities => { - let amount = bank + let amount = bank_wrapper .bank .get_liability_amount(b.liability_shares.into()) .ok()?; - Some((amount, BalanceSide::Liabilities)) + Some((I80F48::ZERO, amount)) } }) + .map(|e| e.unwrap_or_default()) + .unwrap_or_default(); + + Ok(balance) } pub fn get_deposits_and_liabilities_shares(&self) -> (Shares, Shares) { let mut liabilities = Vec::new(); let mut deposits = Vec::new(); - for balance in &self.lending_account.balances { + for balance in &self.account.lending_account.balances { if balance.is_active() { match balance.get_side() { Some(BalanceSide::Liabilities) => { @@ -75,21 +92,13 @@ impl MarginfiAccountWrapper { .collect::>() } - // TODO: refactor - #[allow(clippy::type_complexity)] pub fn get_observation_accounts( lending_account: &LendingAccount, include_banks: &[Pubkey], exclude_banks: &[Pubkey], - cache: Arc, - ) -> Result<( - Vec, - Vec, - Vec, - HashSet, - HashSet, - HashSet, - )> { + cache: &Cache, + clock: &Clock, + ) -> Result { let mut bank_pks: HashSet = MarginfiAccountWrapper::get_active_banks(lending_account) .into_iter() @@ -113,7 +122,7 @@ impl MarginfiAccountWrapper { for bank_pk in bank_pks.iter() { let bank_wrapper = cache.banks.try_get_bank(bank_pk)?; - let oracle_wrapper = T::build(&cache, bank_pk)?; + let oracle_wrapper = T::build(cache, clock, bank_pk)?; let bank_and_oracles: Vec = match bank_wrapper.bank.config.oracle_setup { OracleSetup::PythPushOracle => { vec![*bank_pk, oracle_wrapper.get_address()] @@ -206,339 +215,13 @@ impl MarginfiAccountWrapper { observation_accounts.extend(bank_and_oracles); } - Ok(( + Ok(ObservationAccounts { observation_accounts, swb_oracles, bank_pks, kamino_reserves, drift_spot_markets, juplend_states, - )) - } -} - -#[cfg(test)] -pub mod test_utils { - use std::array; - - use marginfi_type_crate::{ - constants::ASSET_TAG_DEFAULT, - types::{Balance, WrappedI80F48}, - }; - - use crate::wrappers::bank::test_utils::{test_sol, test_usdc}; - - use super::*; - - impl MarginfiAccountWrapper { - pub fn test_healthy(asset_bank: &BankWrapper, liability_bank: &BankWrapper) -> Self { - let balances: [Balance; 16] = array::from_fn(|i| match i { - 0 => Balance { - active: 1, - bank_pk: asset_bank.address, - bank_asset_tag: ASSET_TAG_DEFAULT, - tag: 0, - _pad0: [0; 4], - asset_shares: WrappedI80F48::from(I80F48::from_num(100)), - liability_shares: WrappedI80F48::from(I80F48::ZERO), - emissions_outstanding: WrappedI80F48::from(I80F48::ZERO), - last_update: 0, - _padding: [0; 1], - }, - 1 => Balance { - active: 1, - bank_pk: liability_bank.address, - bank_asset_tag: ASSET_TAG_DEFAULT, - tag: 0, - _pad0: [0; 4], - asset_shares: WrappedI80F48::from(I80F48::ZERO), - liability_shares: WrappedI80F48::from(I80F48::from_num(100)), - emissions_outstanding: WrappedI80F48::from(I80F48::ZERO), - last_update: 0, - _padding: [0; 1], - }, - - _ => Balance::empty_deactivated(), - }); - - let lending_account = LendingAccount { - balances, - last_tag_used: 0, - _pad1: [0; 6], - _padding: [0; 7], - }; - Self { - address: Pubkey::new_unique(), - liquidation_record: Pubkey::default(), - lending_account, - } - } - - pub fn test_unhealthy() -> Self { - let asset_bank = test_usdc(); - let liability_bank = test_sol(); - let balances: [Balance; 16] = array::from_fn(|i| match i { - 0 => Balance { - active: 1, - bank_pk: asset_bank.address, - bank_asset_tag: ASSET_TAG_DEFAULT, - tag: 0, - _pad0: [0; 4], - asset_shares: WrappedI80F48::from(I80F48::from_num(100)), - liability_shares: WrappedI80F48::from(I80F48::ZERO), - emissions_outstanding: WrappedI80F48::from(I80F48::ZERO), - last_update: 0, - _padding: [0; 1], - }, - 1 => Balance { - active: 1, - bank_pk: liability_bank.address, - bank_asset_tag: ASSET_TAG_DEFAULT, - tag: 0, - _pad0: [0; 4], - asset_shares: WrappedI80F48::from(I80F48::ZERO), - liability_shares: WrappedI80F48::from(I80F48::from_num(100)), - emissions_outstanding: WrappedI80F48::from(I80F48::ZERO), - last_update: 0, - _padding: [0; 1], - }, - - _ => Balance::empty_deactivated(), - }); - - let lending_account = LendingAccount { - balances, - last_tag_used: 0, - _pad1: [0; 6], - _padding: [0; 7], - }; - Self { - address: Pubkey::new_unique(), - liquidation_record: Pubkey::default(), - lending_account, - } - } - } -} - -#[cfg(test)] -mod tests { - - use crate::wrappers::{ - bank::test_utils::{test_bonk, test_sol, test_usdc}, - oracle::test_utils::TestOracleWrapper, - }; - - use super::*; - - use crate::cache::test_utils::create_test_cache; - - #[test] - fn test_marginfi_account() { - let sol_bank = test_sol(); - let usdc_bank = test_usdc(); - - let mut cache = create_test_cache(&Vec::new()); - cache.banks.insert(sol_bank.address, sol_bank.bank); - cache.banks.insert(usdc_bank.address, usdc_bank.bank); - - let healthy = MarginfiAccountWrapper::test_healthy(&sol_bank, &usdc_bank); - let (balance, side) = healthy.get_balance_for_bank(&sol_bank).unwrap(); - assert_eq!(balance, I80F48::from_num(100)); - match side { - BalanceSide::Assets => assert!(true, "Side is Assets"), - BalanceSide::Liabilities => assert!(false, "Side is Liabilities"), - } - let (balance, side) = healthy.get_balance_for_bank(&usdc_bank).unwrap(); - assert_eq!(balance, I80F48::from_num(100)); - match side { - BalanceSide::Assets => assert!(false, "Side is Assets"), - BalanceSide::Liabilities => assert!(true, "Side is Liabilities"), - } - assert_eq!( - healthy.get_deposits_and_liabilities_shares(), - ( - vec![(I80F48::from_num(100), sol_bank.address)], - vec![(I80F48::from_num(100), usdc_bank.address)] - ) - ); - assert_eq!( - MarginfiAccountWrapper::get_active_banks(&healthy.lending_account), - vec![sol_bank.address, usdc_bank.address] - ); - - let mut unhealthy = MarginfiAccountWrapper::test_unhealthy(); - let (balance, side) = unhealthy.get_balance_for_bank(&sol_bank).unwrap(); - assert_eq!(balance, I80F48::from_num(100)); - match side { - BalanceSide::Assets => assert!(false, "Side is Assets"), - BalanceSide::Liabilities => assert!(true, "Side is Liabilities"), - } - let (balance, side) = unhealthy.get_balance_for_bank(&usdc_bank).unwrap(); - assert_eq!(balance, I80F48::from_num(100)); - match side { - BalanceSide::Assets => assert!(true, "Side is Assets"), - BalanceSide::Liabilities => assert!(false, "Side is Liabilities"), - } - assert_eq!( - unhealthy.get_deposits_and_liabilities_shares(), - ( - vec![(I80F48::from_num(100), usdc_bank.address)], - vec![(I80F48::from_num(100), sol_bank.address)] - ) - ); - assert_eq!( - MarginfiAccountWrapper::get_active_banks(&unhealthy.lending_account), - vec![usdc_bank.address, sol_bank.address] - ); - - unhealthy.lending_account.balances.swap(1, 2); // swap the elements to create a "gap" at index 1 - unhealthy.lending_account.balances.swap(2, 3); // swap the elements to create a "gap" at index 2 as well - - // Check that the gaps are handled correctly -> get_active_banks returns the same result as before - assert_eq!( - MarginfiAccountWrapper::get_active_banks(&unhealthy.lending_account), - vec![usdc_bank.address, sol_bank.address] - ); - - // Now swap two active banks' positions and verify that the new order is respected - unhealthy.lending_account.balances.swap(0, 3); - assert_eq!( - MarginfiAccountWrapper::get_active_banks(&unhealthy.lending_account), - vec![sol_bank.address, usdc_bank.address] - ); - - // Finally "turn off" the first active bank and check that only the second one is returned - unhealthy.lending_account.balances[0].active = 0; - assert_eq!( - MarginfiAccountWrapper::get_active_banks(&unhealthy.lending_account), - vec![usdc_bank.address] - ); - } - - #[test] - fn test_get_healthy_observation_accounts() { - let sol_bank = test_sol(); - let usdc_bank = test_usdc(); - let healthy_wrapper = MarginfiAccountWrapper::test_healthy(&sol_bank, &usdc_bank); - - let cache = create_test_cache(&vec![sol_bank.clone(), usdc_bank.clone()]); - let cache = Arc::new(cache); - - assert_eq!( - MarginfiAccountWrapper::get_observation_accounts::( - &healthy_wrapper.lending_account, - &[], - &[], - cache.clone() - ) - .unwrap(), - ( - vec![ - sol_bank.address, - sol_bank.bank.config.oracle_keys[0], - usdc_bank.address, - usdc_bank.bank.config.oracle_keys[0], - ], - vec![], - vec![sol_bank.address, usdc_bank.address], - HashSet::new(), - HashSet::new(), - HashSet::new() - ) - ); - } - - #[test] - fn test_get_observation_accounts_with_banks_to_include() { - let sol_bank_wrapper = test_sol(); - let usdc_bank_wrapper = test_usdc(); - let bonk_bank_wrapper = test_bonk(); - let cache = create_test_cache(&vec![ - sol_bank_wrapper.clone(), - usdc_bank_wrapper.clone(), - bonk_bank_wrapper.clone(), - ]); - let cache = Arc::new(cache); - - let healthy_wrapper = - MarginfiAccountWrapper::test_healthy(&sol_bank_wrapper, &usdc_bank_wrapper); - - let banks_to_include = vec![bonk_bank_wrapper.address, sol_bank_wrapper.address]; - let banks_to_exclude = vec![]; - - assert_eq!( - MarginfiAccountWrapper::get_observation_accounts::( - &healthy_wrapper.lending_account, - &banks_to_include, - &banks_to_exclude, - cache - ) - .unwrap(), - ( - vec![ - bonk_bank_wrapper.address, - bonk_bank_wrapper.bank.config.oracle_keys[0], - sol_bank_wrapper.address, - sol_bank_wrapper.bank.config.oracle_keys[0], - usdc_bank_wrapper.address, - usdc_bank_wrapper.bank.config.oracle_keys[0], - ], - vec![bonk_bank_wrapper.bank.config.oracle_keys[0]], // Bonk oracle is the only switchboard oracle - vec![ - bonk_bank_wrapper.address, - sol_bank_wrapper.address, - usdc_bank_wrapper.address - ], - HashSet::new(), - HashSet::new(), - HashSet::new() - ) - ); - } - - #[test] - fn test_get_observation_accounts_with_banks_to_exclude_and_gaps() { - let sol_bank_wrapper = test_sol(); - let usdc_bank_wrapper = test_usdc(); - let bonk_bank_wrapper = test_bonk(); - let cache = create_test_cache(&vec![ - sol_bank_wrapper.clone(), - usdc_bank_wrapper.clone(), - bonk_bank_wrapper.clone(), - ]); - let cache = Arc::new(cache); - - let mut healthy_wrapper = - MarginfiAccountWrapper::test_healthy(&sol_bank_wrapper, &usdc_bank_wrapper); - healthy_wrapper.lending_account.balances.swap(1, 2); // swap the elements to create a "gap" at index 1 - - let banks_to_include = vec![bonk_bank_wrapper.address]; - let banks_to_exclude = vec![sol_bank_wrapper.address]; - assert_eq!( - MarginfiAccountWrapper::get_observation_accounts::( - &healthy_wrapper.lending_account, - &banks_to_include, - &banks_to_exclude, - cache - ) - .unwrap(), - ( - vec![ - // SOL bank was excluded - // sol_bank_wrapper.address, - // sol_bank_wrapper.oracle_adapter.address, - bonk_bank_wrapper.address, // bonk bank took the place of a "gap" - bonk_bank_wrapper.bank.config.oracle_keys[0], - usdc_bank_wrapper.address, - usdc_bank_wrapper.bank.config.oracle_keys[0], - ], - vec![bonk_bank_wrapper.bank.config.oracle_keys[0]], // Bonk oracle is the only switchboard oracle - vec![bonk_bank_wrapper.address, usdc_bank_wrapper.address], - HashSet::new(), - HashSet::new(), - HashSet::new() - ) - ); + }) } } diff --git a/src/wrappers/oracle.rs b/src/wrappers/oracle.rs index 1be46796..df16fa49 100644 --- a/src/wrappers/oracle.rs +++ b/src/wrappers/oracle.rs @@ -1,15 +1,15 @@ use anyhow::{anyhow, Result}; use fixed::types::I80F48; use log::error; -use marginfi::state::price::{OraclePriceFeedAdapter, OraclePriceType, PriceAdapter, PriceBias}; -use marginfi_type_crate::types::OracleSetup; +use marginfi::state::price::{OraclePriceFeedAdapter, PriceAdapter}; +use marginfi_type_crate::types::{OraclePriceType, OracleSetup, PriceBias}; use solana_program::pubkey::Pubkey; -use solana_sdk::account_info::IntoAccountInfo; +use solana_sdk::{account_info::IntoAccountInfo, clock::Clock}; -use crate::{cache::Cache, clock_manager, utils::find_oracle_keys}; +use crate::{cache::Cache, utils::find_oracle_keys}; pub trait OracleWrapperTrait { - fn build(cache: &Cache, bank_address: &Pubkey) -> Result + fn build(cache: &Cache, clock: &Clock, bank_address: &Pubkey) -> Result where Self: Sized; fn get_price_of_type( @@ -43,7 +43,7 @@ impl OracleWrapperTrait for OracleWrapper { *self.addresses.first().unwrap_or(&Pubkey::default()) } - fn build(cache: &Cache, bank_address: &Pubkey) -> Result { + fn build(cache: &Cache, clock: &Clock, bank_address: &Pubkey) -> Result { let bank_wrapper = cache.banks.try_get_bank(bank_address)?; let oracle_addresses = find_oracle_keys(&bank_wrapper.bank.config); @@ -65,7 +65,7 @@ impl OracleWrapperTrait for OracleWrapper { let price_adapter = OraclePriceFeedAdapter::try_from_bank( &bank_wrapper.bank, &[bank_oracle_account_info], - &clock_manager::get_clock(&cache.clock)?, + clock, )?; let oracle_wrapper = Self { @@ -105,7 +105,7 @@ impl OracleWrapperTrait for OracleWrapper { mint_oracle_account_info, sol_pool_account_info, ], - &clock_manager::get_clock(&cache.clock)?, + clock, )?; let oracle_wrapper = Self { addresses: vec![ @@ -118,11 +118,8 @@ impl OracleWrapperTrait for OracleWrapper { result = Some(oracle_wrapper); } OracleSetup::Fixed => { - let price_adapter = OraclePriceFeedAdapter::try_from_bank( - &bank_wrapper.bank, - &[], - &clock_manager::get_clock(&cache.clock)?, - )?; + let price_adapter = + OraclePriceFeedAdapter::try_from_bank(&bank_wrapper.bank, &[], clock)?; let oracle_wrapper = Self { addresses: vec![], @@ -152,13 +149,12 @@ impl OracleWrapperTrait for OracleWrapper { let mut integration_oracle = cache.oracles.try_get_account(&integration_oracle_address)?; - let clock = clock_manager::get_clock(&cache.clock)?; let integration_oracle_account_info = (&integration_oracle_address, &mut integration_oracle).into_account_info(); let price_adapter = OraclePriceFeedAdapter::try_from_bank( &bank_wrapper.bank, &[bank_oracle_account_info, integration_oracle_account_info], - &clock, + clock, )?; let oracle_wrapper = Self { @@ -184,7 +180,7 @@ impl OracleWrapperTrait for OracleWrapper { let price_adapter = OraclePriceFeedAdapter::try_from_bank( &bank_wrapper.bank, &[integration_oracle_account_info], - &clock_manager::get_clock(&cache.clock)?, + clock, )?; let oracle_wrapper = Self { @@ -210,183 +206,3 @@ impl OracleWrapperTrait for OracleWrapper { } } } - -#[cfg(test)] -pub mod test_utils { - use std::str::FromStr; - - use solana_sdk::account::Account; - use switchboard_on_demand::PullFeedAccountData; - - use super::*; - - #[derive(Clone)] - pub struct TestOracleWrapper { - pub price: f64, - pub bias: f64, - pub address: Pubkey, - } - - const SOL_ORACLE_ADDRESS: &str = "11111119rSGfPZLcyCGzY4uYEL1fkzJr6fke9qKxb"; - const USDC_ORACLE_ADDRESS: &str = "1111111Af7Udc9v3L82dQM5b4zee1Xt77Be4czzbH"; - const BONK_ORACLE_ADDRESS: &str = "8ihFLu5FimgTQ1Unh4dVyEHUGodJ5gJQCrQf4KUVB9bN"; - - impl Default for TestOracleWrapper { - fn default() -> Self { - TestOracleWrapper { - price: 42.0, - bias: 5.0, - address: Pubkey::new_unique(), - } - } - } - - impl TestOracleWrapper { - pub fn new(address: Pubkey, price: f64, bias: f64) -> Self { - Self { - price, - bias, - address, - } - } - - pub fn test_sol() -> Self { - Self { - price: 200.0, - bias: 10.0, - address: Pubkey::from_str(SOL_ORACLE_ADDRESS).unwrap(), - } - } - - pub fn test_usdc() -> Self { - Self { - price: 1.0, - bias: 0.1, - address: Pubkey::from_str(USDC_ORACLE_ADDRESS).unwrap(), - } - } - - pub fn test_bonk() -> Self { - Self { - price: 1000.0, - bias: 1.0, - address: Pubkey::from_str(BONK_ORACLE_ADDRESS).unwrap(), - } - } - } - - impl OracleWrapperTrait for TestOracleWrapper { - fn build(cache: &Cache, bank_address: &Pubkey) -> Result { - let bank_wrapper = cache.banks.try_get_bank(bank_address)?; - if matches!( - bank_wrapper.bank.config.oracle_setup, - OracleSetup::SwitchboardPull - | OracleSetup::PythPushOracle - | OracleSetup::StakedWithPythPush - ) { - Ok(TestOracleWrapper::new( - bank_wrapper.bank.config.oracle_keys[0], - 100.0, - 5.0, - )) - } else { - panic!( - "Unsupported Oracle type {:?}", - bank_wrapper.bank.config.oracle_setup - ) - } - } - - fn get_price_of_type( - &self, - _: OraclePriceType, - price_bias: Option, - _: u32, - ) -> anyhow::Result { - match price_bias { - Some(PriceBias::Low) => Ok(I80F48::from_num(self.price - self.bias)), - Some(PriceBias::High) => Ok(I80F48::from_num(self.price + self.bias)), - None => Ok(I80F48::from_num(self.price)), - } - } - - fn get_address(&self) -> Pubkey { - self.address - } - } - - pub fn create_empty_oracle_account() -> Account { - let buffer = vec![0u8; std::mem::size_of::()]; - let pull_feed_data = bytemuck::try_from_bytes::(&buffer).unwrap(); - let bytes: &[u8] = bytemuck::bytes_of(pull_feed_data); - - let mut oracle_account = Account::default(); - oracle_account - .data - .resize(std::mem::size_of::() + 8, 0); - oracle_account.data[8..].copy_from_slice(bytes); - - oracle_account - } -} - -#[cfg(test)] -mod tests { - use crate::cache::test_utils::create_test_cache; - use crate::wrappers::bank::test_utils::test_usdc; - - use super::test_utils::*; - use super::*; - - #[test] - fn test_oracle() { - let oracle = TestOracleWrapper::default(); - - assert_eq!( - oracle - .get_price_of_type(OraclePriceType::RealTime, None, 0) - .unwrap(), - I80F48::from_num(42.0) - ); - assert_eq!( - oracle - .get_price_of_type(OraclePriceType::TimeWeighted, Some(PriceBias::Low), 1) - .unwrap(), - I80F48::from_num(37.0) - ); - assert_eq!( - oracle - .get_price_of_type(OraclePriceType::RealTime, Some(PriceBias::High), 2) - .unwrap(), - I80F48::from_num(47.0) - ); - } - - /// Create test for try_build_oracle_wrapper - #[test] - fn test_try_build_oracle_wrapper() { - let oracle_key = Pubkey::new_unique(); - - let mut usdc_bank_wrapper = test_usdc(); - usdc_bank_wrapper.bank.config.oracle_setup = OracleSetup::SwitchboardPull; - usdc_bank_wrapper.bank.config.oracle_keys[0] = oracle_key.clone(); - - let mut cache = create_test_cache(&vec![usdc_bank_wrapper.clone()]); - cache - .banks - .insert(usdc_bank_wrapper.address, usdc_bank_wrapper.bank); - - let oracle_account = create_empty_oracle_account(); - - // Mock oracles in the cache - cache - .oracles - .try_insert(oracle_key, oracle_account) - .unwrap(); - - let oracle_wrapper: TestOracleWrapper = - TestOracleWrapper::build(&cache, &usdc_bank_wrapper.address).unwrap(); - - assert_eq!(oracle_wrapper.get_address(), oracle_key); - } -} diff --git a/src/wrappers/token_account.rs b/src/wrappers/token_account.rs index 5b8d8575..be2e85f8 100644 --- a/src/wrappers/token_account.rs +++ b/src/wrappers/token_account.rs @@ -2,7 +2,7 @@ use crate::wrappers::oracle::OracleWrapperTrait; use super::bank::BankWrapper; use fixed::types::I80F48; -use marginfi_type_crate::constants::EXP_10_I80F48; +use marginfi_type_crate::{constants::EXP_10_I80F48, types::OraclePriceType}; #[derive(Clone)] pub struct TokenAccountWrapper { @@ -26,7 +26,7 @@ impl TokenAccountWrapper { }; let price = self.oracle_wrapper.get_price_of_type( - marginfi::state::price::OraclePriceType::RealTime, + OraclePriceType::RealTime, None, self.bank_wrapper.bank.config.oracle_max_confidence, )?; @@ -36,7 +36,7 @@ impl TokenAccountWrapper { pub fn get_amount_from_value(&self, value: I80F48) -> anyhow::Result { let price = self.oracle_wrapper.get_price_of_type( - marginfi::state::price::OraclePriceType::RealTime, + OraclePriceType::RealTime, None, self.bank_wrapper.bank.config.oracle_max_confidence, )?; @@ -50,48 +50,3 @@ impl TokenAccountWrapper { Ok(amount * decimal_scale) } } - -#[cfg(test)] -pub mod test_utils { - use crate::wrappers::{ - bank::test_utils::{test_sol, test_usdc}, - oracle::test_utils::TestOracleWrapper, - }; - - use super::*; - - pub type TestTokenAccountWrapper = TokenAccountWrapper; - - impl TestTokenAccountWrapper { - pub fn test_sol() -> Self { - Self { - balance: 10000000, - bank_wrapper: test_sol(), - oracle_wrapper: TestOracleWrapper::test_sol(), - } - } - - pub fn test_usdc() -> Self { - Self { - balance: 100000, - bank_wrapper: test_usdc(), - oracle_wrapper: TestOracleWrapper::test_usdc(), - } - } - } -} - -#[cfg(test)] -mod tests { - use super::test_utils::*; - use super::*; - - #[test] - fn test_token_account() { - let sol_acc = TestTokenAccountWrapper::test_sol(); - assert_eq!(sol_acc.get_value().unwrap(), I80F48::from_num(2000.0)); - - let usdc_acc: TestTokenAccountWrapper = TestTokenAccountWrapper::test_usdc(); - assert_eq!(usdc_acc.get_value().unwrap(), I80F48::from_num(1000.0)); - } -}