Skip to content

Two Prices PR #129

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Feb 24, 2023
Merged
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
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());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this wil just error on the next line

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