From b25a441e47aeab9978688a20350dad28ef4832ac Mon Sep 17 00:00:00 2001 From: 0xripleys <0xripleys@solend.fi> Date: Fri, 27 Oct 2023 18:13:48 +0800 Subject: [PATCH 1/3] example jito position calculation --- Cargo.lock | 1 + token-lending/sdk/Cargo.toml | 1 + token-lending/sdk/examples/jito.rs | 74 ++++++++++ token-lending/sdk/src/lib.rs | 1 + token-lending/sdk/src/offchain_utils.rs | 185 ++++++++++++++++++++++++ 5 files changed, 262 insertions(+) create mode 100644 token-lending/sdk/examples/jito.rs create mode 100644 token-lending/sdk/src/offchain_utils.rs 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/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..d95dddccb05 --- /dev/null +++ b/token-lending/sdk/examples/jito.rs @@ -0,0 +1,74 @@ +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..9cee0e12462 100644 --- a/token-lending/sdk/src/lib.rs +++ b/token-lending/sdk/src/lib.rs @@ -7,6 +7,7 @@ pub mod instruction; pub mod math; pub mod oracles; pub mod state; +pub mod offchain_utils; // Export current sdk types for downstream users building with a different sdk version pub use solana_program; diff --git a/token-lending/sdk/src/offchain_utils.rs b/token-lending/sdk/src/offchain_utils.rs new file mode 100644 index 00000000000..d37e76ba64d --- /dev/null +++ b/token-lending/sdk/src/offchain_utils.rs @@ -0,0 +1,185 @@ +#![allow(missing_docs)] +use crate::{self as solend_program, error::LendingError}; +use pyth_sdk_solana::Price; +use solana_client::rpc_client::RpcClient; +use solana_program::slot_history::Slot; +// use pyth_sdk_solana; +use solana_program::{ + account_info::AccountInfo, msg, program_error::ProgramError, sysvar::clock::Clock, +}; +use std::{convert::TryInto, result::Result}; + +use crate::{state::LastUpdate, NULL_PUBKEY}; +use std::time::Duration; + +use solana_program::{program_pack::Pack, pubkey::Pubkey}; + +use crate::math::{Decimal, Rate, TryAdd, TryMul}; +use std::collections::HashSet; + +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) { + if !o.borrows.is_empty() { + 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(()) +} From d9a888337410e2a3910b9e58f979f665c0d7662a Mon Sep 17 00:00:00 2001 From: 0xripleys <0xripleys@solend.fi> Date: Fri, 27 Oct 2023 18:15:33 +0800 Subject: [PATCH 2/3] clippy + fmt --- token-lending/program/src/processor.rs | 4 ++-- token-lending/program/tests/helpers/mock_pyth.rs | 2 +- token-lending/sdk/examples/jito.rs | 3 +-- token-lending/sdk/src/lib.rs | 2 +- token-lending/sdk/src/offchain_utils.rs | 11 +++-------- 5 files changed, 8 insertions(+), 14 deletions(-) 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/examples/jito.rs b/token-lending/sdk/examples/jito.rs index d95dddccb05..438dfab88f2 100644 --- a/token-lending/sdk/examples/jito.rs +++ b/token-lending/sdk/examples/jito.rs @@ -63,8 +63,7 @@ pub fn main() { .entry(obligation.owner) .or_insert(Position::default()); - position.borrow_balance += - borrow.borrowed_amount_wads.try_round_u64().unwrap(); + position.borrow_balance += borrow.borrowed_amount_wads.try_round_u64().unwrap(); } } } diff --git a/token-lending/sdk/src/lib.rs b/token-lending/sdk/src/lib.rs index 9cee0e12462..940ecafbf8e 100644 --- a/token-lending/sdk/src/lib.rs +++ b/token-lending/sdk/src/lib.rs @@ -5,9 +5,9 @@ pub mod error; pub mod instruction; pub mod math; +pub mod offchain_utils; pub mod oracles; pub mod state; -pub mod offchain_utils; // Export current sdk types for downstream users building with a different sdk version pub use solana_program; diff --git a/token-lending/sdk/src/offchain_utils.rs b/token-lending/sdk/src/offchain_utils.rs index d37e76ba64d..f1f29688f70 100644 --- a/token-lending/sdk/src/offchain_utils.rs +++ b/token-lending/sdk/src/offchain_utils.rs @@ -1,21 +1,16 @@ #![allow(missing_docs)] -use crate::{self as solend_program, error::LendingError}; -use pyth_sdk_solana::Price; + use solana_client::rpc_client::RpcClient; use solana_program::slot_history::Slot; // use pyth_sdk_solana; -use solana_program::{ - account_info::AccountInfo, msg, program_error::ProgramError, sysvar::clock::Clock, -}; -use std::{convert::TryInto, result::Result}; +use solana_program::program_error::ProgramError; +use std::result::Result; use crate::{state::LastUpdate, NULL_PUBKEY}; -use std::time::Duration; use solana_program::{program_pack::Pack, pubkey::Pubkey}; use crate::math::{Decimal, Rate, TryAdd, TryMul}; -use std::collections::HashSet; use crate::state::{LendingMarket, Obligation, Reserve}; use std::{collections::HashMap, error::Error}; From d4ea670ca54bfc0a61d3ac8961d7d483a91fbc83 Mon Sep 17 00:00:00 2001 From: 0xripleys <0xripleys@solend.fi> Date: Mon, 6 Nov 2023 22:00:25 +0100 Subject: [PATCH 3/3] sdk bugfix --- token-lending/sdk/src/offchain_utils.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/token-lending/sdk/src/offchain_utils.rs b/token-lending/sdk/src/offchain_utils.rs index f1f29688f70..99dfff7e0a5 100644 --- a/token-lending/sdk/src/offchain_utils.rs +++ b/token-lending/sdk/src/offchain_utils.rs @@ -34,9 +34,7 @@ pub fn get_solend_accounts_as_map( match account.data.len() { Obligation::LEN => { if let Ok(o) = Obligation::unpack(&account.data) { - if !o.borrows.is_empty() { - obligations.insert(pubkey, o); - } + obligations.insert(pubkey, o); } } Reserve::LEN => {