diff --git a/Cargo.lock b/Cargo.lock index 9d201e66d65..cddf9e84885 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4305,6 +4305,7 @@ dependencies = [ "rand 0.8.5", "serde", "serde_yaml", + "solana-client", "solana-program", "solana-sdk", "spl-token", diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index e64f223322f..6310fadbf66 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -530,9 +530,9 @@ fn _refresh_reserve<'a>( /// Lite version of refresh_reserve that should be used when the oracle price doesn't need to be updated /// BE CAREFUL WHEN USING THIS -fn _refresh_reserve_interest<'a>( +fn _refresh_reserve_interest( program_id: &Pubkey, - reserve_info: &AccountInfo<'a>, + reserve_info: &AccountInfo<'_>, clock: &Clock, ) -> ProgramResult { let mut reserve = Reserve::unpack(&reserve_info.data.borrow())?; diff --git a/token-lending/program/tests/helpers/mock_pyth.rs b/token-lending/program/tests/helpers/mock_pyth.rs index 0deac090b4a..9d7f3315657 100644 --- a/token-lending/program/tests/helpers/mock_pyth.rs +++ b/token-lending/program/tests/helpers/mock_pyth.rs @@ -127,7 +127,7 @@ impl Processor { msg!("Mock Pyth: Set price"); let price_account_info = next_account_info(account_info_iter)?; let data = &mut price_account_info.try_borrow_mut_data()?; - let mut price_account: &mut PriceAccount = load_mut(data).unwrap(); + let price_account: &mut PriceAccount = load_mut(data).unwrap(); price_account.agg.price = price; price_account.agg.conf = conf; diff --git a/token-lending/sdk/Cargo.toml b/token-lending/sdk/Cargo.toml index 716b46375ad..d0343d09f13 100644 --- a/token-lending/sdk/Cargo.toml +++ b/token-lending/sdk/Cargo.toml @@ -14,6 +14,7 @@ num-derive = "0.3" num-traits = "0.2" pyth-sdk-solana = "0.7.0" solana-program = ">=1.9, < 1.15" +solana-client = ">=1.9, < 1.15" spl-token = { version = "3.2.0", features=["no-entrypoint"] } static_assertions = "1.1.0" thiserror = "1.0" diff --git a/token-lending/sdk/examples/jito.rs b/token-lending/sdk/examples/jito.rs new file mode 100644 index 00000000000..438dfab88f2 --- /dev/null +++ b/token-lending/sdk/examples/jito.rs @@ -0,0 +1,73 @@ +use solana_client::rpc_client::RpcClient; +use solana_sdk::pubkey; +use std::collections::HashMap; + +use solend_sdk::{ + offchain_utils::{ + get_solend_accounts_as_map, offchain_refresh_obligation, offchain_refresh_reserve_interest, + }, + solend_mainnet, +}; + +#[derive(Debug, Clone, Default)] +struct Position { + pub deposit_balance: u64, + pub borrow_balance: u64, +} + +pub fn main() { + let rpc_url = std::env::var("RPC_URL") + .unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".to_string()); + let rpc_client = RpcClient::new(rpc_url); + + let mut accounts = get_solend_accounts_as_map(&solend_mainnet::id(), &rpc_client).unwrap(); + + // update solend-specific interest variables + let slot = rpc_client.get_slot().unwrap(); + for reserve in accounts.reserves.values_mut() { + let _ = offchain_refresh_reserve_interest(reserve, slot); + } + + for obligation in accounts.obligations.values_mut() { + offchain_refresh_obligation(obligation, &accounts.reserves).unwrap(); + } + + // calculate jitosol balances per user across all pools + let jitosol = pubkey!("J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn"); + let mut user_to_position = HashMap::new(); + + for obligation in accounts.obligations.values() { + for deposit in &obligation.deposits { + let deposit_reserve = accounts.reserves.get(&deposit.deposit_reserve).unwrap(); + if deposit_reserve.liquidity.mint_pubkey == jitosol { + let position = user_to_position + .entry(obligation.owner) + .or_insert(Position::default()); + + // convert cJitoSol to JitoSol + let cjitosol_deposited = deposit.deposited_amount; + let jitosol_deposited = deposit_reserve + .collateral_exchange_rate() + .unwrap() + .collateral_to_liquidity(cjitosol_deposited) + .unwrap(); + + position.deposit_balance += jitosol_deposited; + } + } + + for borrow in &obligation.borrows { + let borrow_reserve = accounts.reserves.get(&borrow.borrow_reserve).unwrap(); + if borrow_reserve.liquidity.mint_pubkey == jitosol { + let position = user_to_position + .entry(obligation.owner) + .or_insert(Position::default()); + + position.borrow_balance += borrow.borrowed_amount_wads.try_round_u64().unwrap(); + } + } + } + + println!("Done refreshing"); + println!("{:#?}", user_to_position); +} diff --git a/token-lending/sdk/src/lib.rs b/token-lending/sdk/src/lib.rs index 8fc5cd4c978..940ecafbf8e 100644 --- a/token-lending/sdk/src/lib.rs +++ b/token-lending/sdk/src/lib.rs @@ -5,6 +5,7 @@ pub mod error; pub mod instruction; pub mod math; +pub mod offchain_utils; pub mod oracles; pub mod state; diff --git a/token-lending/sdk/src/offchain_utils.rs b/token-lending/sdk/src/offchain_utils.rs new file mode 100644 index 00000000000..99dfff7e0a5 --- /dev/null +++ b/token-lending/sdk/src/offchain_utils.rs @@ -0,0 +1,178 @@ +#![allow(missing_docs)] + +use solana_client::rpc_client::RpcClient; +use solana_program::slot_history::Slot; +// use pyth_sdk_solana; +use solana_program::program_error::ProgramError; +use std::result::Result; + +use crate::{state::LastUpdate, NULL_PUBKEY}; + +use solana_program::{program_pack::Pack, pubkey::Pubkey}; + +use crate::math::{Decimal, Rate, TryAdd, TryMul}; + +use crate::state::{LendingMarket, Obligation, Reserve}; +use std::{collections::HashMap, error::Error}; + +#[derive(Debug, Clone)] +pub struct SolendAccounts { + pub lending_markets: HashMap, + pub reserves: HashMap, + pub obligations: HashMap, +} + +pub fn get_solend_accounts_as_map( + lending_program_id: &Pubkey, + client: &RpcClient, +) -> Result> { + let accounts = client.get_program_accounts(lending_program_id)?; + + let (lending_markets, reserves, obligations) = accounts.into_iter().fold( + (HashMap::new(), HashMap::new(), HashMap::new()), + |(mut lending_markets, mut reserves, mut obligations), (pubkey, account)| { + match account.data.len() { + Obligation::LEN => { + if let Ok(o) = Obligation::unpack(&account.data) { + obligations.insert(pubkey, o); + } + } + Reserve::LEN => { + if let Ok(r) = Reserve::unpack(&account.data) { + reserves.insert(pubkey, r); + } + } + LendingMarket::LEN => { + if let Ok(l) = LendingMarket::unpack(&account.data) { + lending_markets.insert(pubkey, l); + } + } + _ => (), + }; + (lending_markets, reserves, obligations) + }, + ); + + Ok(SolendAccounts { + lending_markets, + reserves, + obligations, + }) +} + +pub fn offchain_refresh_reserve_interest( + reserve: &mut Reserve, + slot: Slot, +) -> Result<(), Box> { + reserve.accrue_interest(slot)?; + reserve.last_update = LastUpdate { slot, stale: false }; + + Ok(()) +} + +pub fn offchain_refresh_reserve( + _pubkey: &Pubkey, + reserve: &mut Reserve, + slot: Slot, + prices: &HashMap>, +) -> Result<(), Box> { + let pyth_oracle = reserve.liquidity.pyth_oracle_pubkey; + let switchboard_oracle = reserve.liquidity.switchboard_oracle_pubkey; + + let price = if let Some(Some(price)) = prices.get(&pyth_oracle) { + if pyth_oracle != NULL_PUBKEY { + Some(*price) + } else { + None + } + } else if let Some(Some(price)) = prices.get(&switchboard_oracle) { + if switchboard_oracle != NULL_PUBKEY { + Some(*price) + } else { + None + } + } else { + None + }; + + if let Some(price) = price { + reserve.liquidity.market_price = price; + } else { + return Err("No price".into()); + } + + reserve.accrue_interest(slot)?; + reserve.last_update = LastUpdate { slot, stale: false }; + + Ok(()) +} + +pub fn offchain_refresh_obligation( + o: &mut Obligation, + reserves: &HashMap, +) -> Result<(), Box> { + o.deposited_value = Decimal::zero(); + o.super_unhealthy_borrow_value = Decimal::zero(); + o.unhealthy_borrow_value = Decimal::zero(); + o.borrowed_value = Decimal::zero(); + + for collateral in &mut o.deposits { + let deposit_reserve = reserves + .get(&collateral.deposit_reserve) + .ok_or(ProgramError::Custom(35))?; + + let liquidity_amount = deposit_reserve + .collateral_exchange_rate()? + .decimal_collateral_to_liquidity(collateral.deposited_amount.into())?; + + let market_value = deposit_reserve.market_value(liquidity_amount)?; + let liquidation_threshold_rate = + Rate::from_percent(deposit_reserve.config.liquidation_threshold); + let max_liquidation_threshold_rate = + Rate::from_percent(deposit_reserve.config.max_liquidation_threshold); + + collateral.market_value = market_value; + + o.deposited_value = o.deposited_value.try_add(market_value)?; + o.unhealthy_borrow_value = o + .unhealthy_borrow_value + .try_add(market_value.try_mul(liquidation_threshold_rate)?)?; + o.super_unhealthy_borrow_value = o + .super_unhealthy_borrow_value + .try_add(market_value.try_mul(max_liquidation_threshold_rate)?)?; + } + + let mut max_borrow_weight = None; + + for (index, liquidity) in o.borrows.iter_mut().enumerate() { + let borrow_reserve = reserves.get(&liquidity.borrow_reserve).unwrap(); + liquidity.accrue_interest(borrow_reserve.liquidity.cumulative_borrow_rate_wads)?; + + let market_value = borrow_reserve.market_value(liquidity.borrowed_amount_wads)?; + liquidity.market_value = market_value; + + o.borrowed_value = o + .borrowed_value + .try_add(market_value.try_mul(borrow_reserve.borrow_weight())?)?; + + let borrow_weight_and_pubkey = ( + borrow_reserve.config.added_borrow_weight_bps, + borrow_reserve.liquidity.mint_pubkey, + ); + + max_borrow_weight = match max_borrow_weight { + None => Some((borrow_weight_and_pubkey, index)), + Some((max_borrow_weight_and_pubkey, _)) => { + if liquidity.borrowed_amount_wads > Decimal::zero() + && borrow_weight_and_pubkey > max_borrow_weight_and_pubkey + { + Some((borrow_weight_and_pubkey, index)) + } else { + max_borrow_weight + } + } + }; + } + + Ok(()) +}