Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions token-lending/program/src/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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())?;
Expand Down
2 changes: 1 addition & 1 deletion token-lending/program/tests/helpers/mock_pyth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions token-lending/sdk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
73 changes: 73 additions & 0 deletions token-lending/sdk/examples/jito.rs
Original file line number Diff line number Diff line change
@@ -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);
}
1 change: 1 addition & 0 deletions token-lending/sdk/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
pub mod error;
pub mod instruction;
pub mod math;
pub mod offchain_utils;
pub mod oracles;
pub mod state;

Expand Down
178 changes: 178 additions & 0 deletions token-lending/sdk/src/offchain_utils.rs
Original file line number Diff line number Diff line change
@@ -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<Pubkey, LendingMarket>,
pub reserves: HashMap<Pubkey, Reserve>,
pub obligations: HashMap<Pubkey, Obligation>,
}

pub fn get_solend_accounts_as_map(
lending_program_id: &Pubkey,
client: &RpcClient,
) -> Result<SolendAccounts, Box<dyn Error>> {
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<dyn Error>> {
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<Pubkey, Option<Decimal>>,
) -> Result<(), Box<dyn Error>> {
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<Pubkey, Reserve>,
) -> Result<(), Box<dyn Error>> {
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(())
}