Skip to content

Commit

Permalink
Two Prices PR (#129)
Browse files Browse the repository at this point in the history
- Add a smoothed_market_price to Reserve that is used to limit borrows and withdraws in cases where smoothed price and spot price diverge.
- allowed_borrow_value now uses the min(smoothed_market_price, current spot price)
- new field on obligation called borrowed_value_upper_bound that uses max(smoothed_market_price, current spot price)
  • Loading branch information
0xripleys committed Mar 7, 2023
1 parent be92df7 commit e2e44ad
Show file tree
Hide file tree
Showing 15 changed files with 1,378 additions and 149 deletions.
113 changes: 53 additions & 60 deletions token-lending/program/src/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,8 @@ fn process_init_reserve(
validate_pyth_keys(&lending_market, pyth_product_info, pyth_price_info)?;
validate_switchboard_keys(&lending_market, switchboard_feed_info)?;

let market_price = get_price(Some(switchboard_feed_info), pyth_price_info, clock)?;
let (market_price, smoothed_market_price) =
get_price(Some(switchboard_feed_info), pyth_price_info, clock)?;

let authority_signer_seeds = &[
lending_market_info.key.as_ref(),
Expand Down Expand Up @@ -360,6 +361,7 @@ fn process_init_reserve(
pyth_oracle_pubkey: *pyth_price_info.key,
switchboard_oracle_pubkey: *switchboard_feed_info.key,
market_price,
smoothed_market_price: smoothed_market_price.unwrap_or(market_price),
}),
collateral: ReserveCollateral::new(NewReserveCollateralParams {
mint_pubkey: *reserve_collateral_mint_info.key,
Expand Down Expand Up @@ -482,7 +484,21 @@ fn _refresh_reserve<'a>(
return Err(LendingError::InvalidOracleConfig.into());
}

reserve.liquidity.market_price = get_price(switchboard_feed_info, pyth_price_info, clock)?;
let (market_price, smoothed_market_price) =
get_price(switchboard_feed_info, pyth_price_info, clock)?;

reserve.liquidity.market_price = market_price;

if let Some(smoothed_market_price) = smoothed_market_price {
reserve.liquidity.smoothed_market_price = smoothed_market_price;
}

// currently there's no way to support two prices without a pyth oracle. So if a reserve
// only supports switchboard, reserve.smoothed_market_price == reserve.market_price
if reserve.liquidity.pyth_oracle_pubkey == solend_program::NULL_PUBKEY {
reserve.liquidity.smoothed_market_price = market_price;
}

Reserve::pack(reserve, &mut reserve_info.data.borrow_mut())?;

_refresh_reserve_interest(program_id, reserve_info, clock)
Expand Down Expand Up @@ -881,6 +897,7 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) ->

let mut deposited_value = Decimal::zero();
let mut borrowed_value = Decimal::zero();
let mut borrowed_value_upper_bound = Decimal::zero();
let mut allowed_borrow_value = Decimal::zero();
let mut unhealthy_borrow_value = Decimal::zero();

Expand Down Expand Up @@ -910,25 +927,22 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) ->
return Err(LendingError::ReserveStale.into());
}

// @TODO: add lookup table https://git.io/JOCYq
let decimals = 10u64
.checked_pow(deposit_reserve.liquidity.mint_decimals as u32)
.ok_or(LendingError::MathOverflow)?;

let market_value = deposit_reserve
let liquidity_amount = deposit_reserve
.collateral_exchange_rate()?
.decimal_collateral_to_liquidity(collateral.deposited_amount.into())?
.try_mul(deposit_reserve.liquidity.market_price)?
.try_div(decimals)?;
collateral.market_value = market_value;
.decimal_collateral_to_liquidity(collateral.deposited_amount.into())?;

let market_value = deposit_reserve.market_value(liquidity_amount)?;
let market_value_lower_bound =
deposit_reserve.market_value_lower_bound(liquidity_amount)?;

let loan_to_value_rate = Rate::from_percent(deposit_reserve.config.loan_to_value_ratio);
let liquidation_threshold_rate =
Rate::from_percent(deposit_reserve.config.liquidation_threshold);

collateral.market_value = market_value;
deposited_value = deposited_value.try_add(market_value)?;
allowed_borrow_value =
allowed_borrow_value.try_add(market_value.try_mul(loan_to_value_rate)?)?;
allowed_borrow_value.try_add(market_value_lower_bound.try_mul(loan_to_value_rate)?)?;
unhealthy_borrow_value =
unhealthy_borrow_value.try_add(market_value.try_mul(liquidation_threshold_rate)?)?;
}
Expand Down Expand Up @@ -962,10 +976,14 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) ->
liquidity.accrue_interest(borrow_reserve.liquidity.cumulative_borrow_rate_wads)?;

let market_value = borrow_reserve.market_value(liquidity.borrowed_amount_wads)?;
let market_value_upper_bound =
borrow_reserve.market_value_upper_bound(liquidity.borrowed_amount_wads)?;
liquidity.market_value = market_value;

borrowed_value =
borrowed_value.try_add(market_value.try_mul(borrow_reserve.borrow_weight())?)?;
borrowed_value_upper_bound = borrowed_value_upper_bound
.try_add(market_value_upper_bound.try_mul(borrow_reserve.borrow_weight())?)?;
}

if account_info_iter.peek().is_some() {
Expand All @@ -975,6 +993,7 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) ->

obligation.deposited_value = deposited_value;
obligation.borrowed_value = borrowed_value;
obligation.borrowed_value_upper_bound = borrowed_value_upper_bound;

let global_unhealthy_borrow_value = Decimal::from(70000000u64);
let global_allowed_borrow_value = Decimal::from(65000000u64);
Expand Down Expand Up @@ -1310,49 +1329,14 @@ fn _withdraw_obligation_collateral<'a>(
return Err(LendingError::InvalidMarketAuthority.into());
}

let withdraw_amount = if obligation.borrows.is_empty() {
if collateral_amount == u64::MAX {
collateral.deposited_amount
} else {
collateral.deposited_amount.min(collateral_amount)
}
} else if obligation.deposited_value == Decimal::zero() {
msg!("Obligation deposited value is zero");
return Err(LendingError::ObligationDepositsZero.into());
} else {
let max_withdraw_value = obligation.max_withdraw_value(Rate::from_percent(
withdraw_reserve.config.loan_to_value_ratio,
))?;
let max_withdraw_amount = obligation.max_withdraw_amount(collateral, &withdraw_reserve)?;

if max_withdraw_value == Decimal::zero() {
msg!("Maximum withdraw value is zero");
return Err(LendingError::WithdrawTooLarge.into());
}
if max_withdraw_amount == 0 {
msg!("Maximum withdraw value is zero");
return Err(LendingError::WithdrawTooLarge.into());
}

let withdraw_amount = if collateral_amount == u64::MAX {
let withdraw_value = max_withdraw_value.min(collateral.market_value);
let withdraw_pct = withdraw_value.try_div(collateral.market_value)?;
withdraw_pct
.try_mul(collateral.deposited_amount)?
.try_floor_u64()?
.min(collateral.deposited_amount)
} else {
let withdraw_amount = collateral_amount.min(collateral.deposited_amount);
let withdraw_pct =
Decimal::from(withdraw_amount).try_div(collateral.deposited_amount)?;
let withdraw_value = collateral.market_value.try_mul(withdraw_pct)?;
if withdraw_value > max_withdraw_value {
msg!("Withdraw value cannot exceed maximum withdraw value");
return Err(LendingError::WithdrawTooLarge.into());
}
withdraw_amount
};
if withdraw_amount == 0 {
msg!("Withdraw amount is too small to transfer collateral");
return Err(LendingError::WithdrawTooSmall.into());
}
withdraw_amount
};
let withdraw_amount = std::cmp::min(collateral_amount, max_withdraw_amount);

obligation.withdraw(withdraw_amount, collateral_index)?;
obligation.last_update.mark_stale();
Expand Down Expand Up @@ -1486,7 +1470,9 @@ fn process_borrow_obligation_liquidity(
return Err(LendingError::InvalidMarketAuthority.into());
}

let remaining_borrow_value = obligation.remaining_borrow_value()?;
let remaining_borrow_value = obligation
.remaining_borrow_value()
.unwrap_or_else(|_| Decimal::zero());
if remaining_borrow_value == Decimal::zero() {
msg!("Remaining borrow value is zero");
return Err(LendingError::BorrowTooLarge.into());
Expand Down Expand Up @@ -2606,19 +2592,26 @@ fn get_pyth_product_quote_currency(
})
}

/// get_price tries to load the oracle price from pyth, and if it fails, uses switchboard.
/// The first element in the returned tuple is the market price, and the second is the optional
/// smoothed price (eg ema, twap).
fn get_price(
switchboard_feed_info: Option<&AccountInfo>,
pyth_price_account_info: &AccountInfo,
clock: &Clock,
) -> Result<Decimal, ProgramError> {
let pyth_price = get_pyth_price(pyth_price_account_info, clock).unwrap_or_default();
if pyth_price != Decimal::zero() {
return Ok(pyth_price);
) -> Result<(Decimal, Option<Decimal>), ProgramError> {
if let Ok(prices) = get_pyth_price(pyth_price_account_info, clock) {
return Ok((prices.0, Some(prices.1)));
}

// if switchboard was not passed in don't try to grab the price
if let Some(switchboard_feed_info_unwrapped) = switchboard_feed_info {
return get_switchboard_price(switchboard_feed_info_unwrapped, clock);
// TODO: add support for switchboard smoothed prices. Probably need to add a new
// switchboard account per reserve.
return match get_switchboard_price(switchboard_feed_info_unwrapped, clock) {
Ok(price) => Ok((price, None)),
Err(e) => Err(e),
};
}

Err(LendingError::InvalidOracleConfig.into())
Expand Down
47 changes: 40 additions & 7 deletions token-lending/program/tests/helpers/mock_pyth.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use pyth_sdk_solana::state::{
AccountType, PriceAccount, PriceStatus, ProductAccount, MAGIC, PROD_ACCT_SIZE, PROD_ATTR_SIZE,
VERSION_2,
AccountType, PriceAccount, PriceStatus, ProductAccount, Rational, MAGIC, PROD_ACCT_SIZE,
PROD_ATTR_SIZE, VERSION_2,
};
/// mock oracle prices in tests with this program.
use solana_program::{
Expand Down Expand Up @@ -34,7 +34,13 @@ pub enum MockPythInstruction {

/// Accounts:
/// 0: PriceAccount
SetPrice { price: i64, conf: u64, expo: i32 },
SetPrice {
price: i64,
conf: u64,
expo: i32,
ema_price: i64,
ema_conf: u64,
},

/// Accounts:
/// 0: AggregatorAccount
Expand Down Expand Up @@ -111,7 +117,13 @@ impl Processor {

Ok(())
}
MockPythInstruction::SetPrice { price, conf, expo } => {
MockPythInstruction::SetPrice {
price,
conf,
expo,
ema_price,
ema_conf,
} => {
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()?;
Expand All @@ -121,6 +133,19 @@ impl Processor {
price_account.agg.conf = conf;
price_account.expo = expo;

price_account.ema_price = Rational {
val: ema_price,
// these fields don't matter
numer: 1,
denom: 1,
};

price_account.ema_conf = Rational {
val: ema_conf as i64,
numer: 1,
denom: 1,
};

price_account.last_slot = Clock::get()?.slot;
price_account.agg.pub_slot = Clock::get()?.slot;
price_account.agg.status = PriceStatus::Trading;
Expand Down Expand Up @@ -201,10 +226,18 @@ pub fn set_price(
price: i64,
conf: u64,
expo: i32,
ema_price: i64,
ema_conf: u64,
) -> Instruction {
let data = MockPythInstruction::SetPrice { price, conf, expo }
.try_to_vec()
.unwrap();
let data = MockPythInstruction::SetPrice {
price,
conf,
expo,
ema_price,
ema_conf,
}
.try_to_vec()
.unwrap();
Instruction {
program_id,
accounts: vec![AccountMeta::new(price_account_pubkey, false)],
Expand Down
4 changes: 4 additions & 0 deletions token-lending/program/tests/helpers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ pub mod usdc_mint {
solana_program::declare_id!("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");
}

pub mod usdt_mint {
solana_program::declare_id!("Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB");
}

pub mod wsol_mint {
// fake mint, not the real wsol bc i can't mint wsol programmatically
solana_program::declare_id!("So1m5eppzgokXLBt9Cg8KCMPWhHfTzVaGh26Y415MRG");
Expand Down
Loading

0 comments on commit e2e44ad

Please sign in to comment.