Skip to content

Commit e2e44ad

Browse files
committed
Two Prices PR (#129)
- 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)
1 parent be92df7 commit e2e44ad

15 files changed

+1378
-149
lines changed

token-lending/program/src/processor.rs

Lines changed: 53 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,8 @@ fn process_init_reserve(
329329
validate_pyth_keys(&lending_market, pyth_product_info, pyth_price_info)?;
330330
validate_switchboard_keys(&lending_market, switchboard_feed_info)?;
331331

332-
let market_price = get_price(Some(switchboard_feed_info), pyth_price_info, clock)?;
332+
let (market_price, smoothed_market_price) =
333+
get_price(Some(switchboard_feed_info), pyth_price_info, clock)?;
333334

334335
let authority_signer_seeds = &[
335336
lending_market_info.key.as_ref(),
@@ -360,6 +361,7 @@ fn process_init_reserve(
360361
pyth_oracle_pubkey: *pyth_price_info.key,
361362
switchboard_oracle_pubkey: *switchboard_feed_info.key,
362363
market_price,
364+
smoothed_market_price: smoothed_market_price.unwrap_or(market_price),
363365
}),
364366
collateral: ReserveCollateral::new(NewReserveCollateralParams {
365367
mint_pubkey: *reserve_collateral_mint_info.key,
@@ -482,7 +484,21 @@ fn _refresh_reserve<'a>(
482484
return Err(LendingError::InvalidOracleConfig.into());
483485
}
484486

485-
reserve.liquidity.market_price = get_price(switchboard_feed_info, pyth_price_info, clock)?;
487+
let (market_price, smoothed_market_price) =
488+
get_price(switchboard_feed_info, pyth_price_info, clock)?;
489+
490+
reserve.liquidity.market_price = market_price;
491+
492+
if let Some(smoothed_market_price) = smoothed_market_price {
493+
reserve.liquidity.smoothed_market_price = smoothed_market_price;
494+
}
495+
496+
// currently there's no way to support two prices without a pyth oracle. So if a reserve
497+
// only supports switchboard, reserve.smoothed_market_price == reserve.market_price
498+
if reserve.liquidity.pyth_oracle_pubkey == solend_program::NULL_PUBKEY {
499+
reserve.liquidity.smoothed_market_price = market_price;
500+
}
501+
486502
Reserve::pack(reserve, &mut reserve_info.data.borrow_mut())?;
487503

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

882898
let mut deposited_value = Decimal::zero();
883899
let mut borrowed_value = Decimal::zero();
900+
let mut borrowed_value_upper_bound = Decimal::zero();
884901
let mut allowed_borrow_value = Decimal::zero();
885902
let mut unhealthy_borrow_value = Decimal::zero();
886903

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

913-
// @TODO: add lookup table https://git.io/JOCYq
914-
let decimals = 10u64
915-
.checked_pow(deposit_reserve.liquidity.mint_decimals as u32)
916-
.ok_or(LendingError::MathOverflow)?;
917-
918-
let market_value = deposit_reserve
930+
let liquidity_amount = deposit_reserve
919931
.collateral_exchange_rate()?
920-
.decimal_collateral_to_liquidity(collateral.deposited_amount.into())?
921-
.try_mul(deposit_reserve.liquidity.market_price)?
922-
.try_div(decimals)?;
923-
collateral.market_value = market_value;
932+
.decimal_collateral_to_liquidity(collateral.deposited_amount.into())?;
933+
934+
let market_value = deposit_reserve.market_value(liquidity_amount)?;
935+
let market_value_lower_bound =
936+
deposit_reserve.market_value_lower_bound(liquidity_amount)?;
924937

925938
let loan_to_value_rate = Rate::from_percent(deposit_reserve.config.loan_to_value_ratio);
926939
let liquidation_threshold_rate =
927940
Rate::from_percent(deposit_reserve.config.liquidation_threshold);
928941

942+
collateral.market_value = market_value;
929943
deposited_value = deposited_value.try_add(market_value)?;
930944
allowed_borrow_value =
931-
allowed_borrow_value.try_add(market_value.try_mul(loan_to_value_rate)?)?;
945+
allowed_borrow_value.try_add(market_value_lower_bound.try_mul(loan_to_value_rate)?)?;
932946
unhealthy_borrow_value =
933947
unhealthy_borrow_value.try_add(market_value.try_mul(liquidation_threshold_rate)?)?;
934948
}
@@ -962,10 +976,14 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) ->
962976
liquidity.accrue_interest(borrow_reserve.liquidity.cumulative_borrow_rate_wads)?;
963977

964978
let market_value = borrow_reserve.market_value(liquidity.borrowed_amount_wads)?;
979+
let market_value_upper_bound =
980+
borrow_reserve.market_value_upper_bound(liquidity.borrowed_amount_wads)?;
965981
liquidity.market_value = market_value;
966982

967983
borrowed_value =
968984
borrowed_value.try_add(market_value.try_mul(borrow_reserve.borrow_weight())?)?;
985+
borrowed_value_upper_bound = borrowed_value_upper_bound
986+
.try_add(market_value_upper_bound.try_mul(borrow_reserve.borrow_weight())?)?;
969987
}
970988

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

976994
obligation.deposited_value = deposited_value;
977995
obligation.borrowed_value = borrowed_value;
996+
obligation.borrowed_value_upper_bound = borrowed_value_upper_bound;
978997

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

1313-
let withdraw_amount = if obligation.borrows.is_empty() {
1314-
if collateral_amount == u64::MAX {
1315-
collateral.deposited_amount
1316-
} else {
1317-
collateral.deposited_amount.min(collateral_amount)
1318-
}
1319-
} else if obligation.deposited_value == Decimal::zero() {
1320-
msg!("Obligation deposited value is zero");
1321-
return Err(LendingError::ObligationDepositsZero.into());
1322-
} else {
1323-
let max_withdraw_value = obligation.max_withdraw_value(Rate::from_percent(
1324-
withdraw_reserve.config.loan_to_value_ratio,
1325-
))?;
1332+
let max_withdraw_amount = obligation.max_withdraw_amount(collateral, &withdraw_reserve)?;
13261333

1327-
if max_withdraw_value == Decimal::zero() {
1328-
msg!("Maximum withdraw value is zero");
1329-
return Err(LendingError::WithdrawTooLarge.into());
1330-
}
1334+
if max_withdraw_amount == 0 {
1335+
msg!("Maximum withdraw value is zero");
1336+
return Err(LendingError::WithdrawTooLarge.into());
1337+
}
13311338

1332-
let withdraw_amount = if collateral_amount == u64::MAX {
1333-
let withdraw_value = max_withdraw_value.min(collateral.market_value);
1334-
let withdraw_pct = withdraw_value.try_div(collateral.market_value)?;
1335-
withdraw_pct
1336-
.try_mul(collateral.deposited_amount)?
1337-
.try_floor_u64()?
1338-
.min(collateral.deposited_amount)
1339-
} else {
1340-
let withdraw_amount = collateral_amount.min(collateral.deposited_amount);
1341-
let withdraw_pct =
1342-
Decimal::from(withdraw_amount).try_div(collateral.deposited_amount)?;
1343-
let withdraw_value = collateral.market_value.try_mul(withdraw_pct)?;
1344-
if withdraw_value > max_withdraw_value {
1345-
msg!("Withdraw value cannot exceed maximum withdraw value");
1346-
return Err(LendingError::WithdrawTooLarge.into());
1347-
}
1348-
withdraw_amount
1349-
};
1350-
if withdraw_amount == 0 {
1351-
msg!("Withdraw amount is too small to transfer collateral");
1352-
return Err(LendingError::WithdrawTooSmall.into());
1353-
}
1354-
withdraw_amount
1355-
};
1339+
let withdraw_amount = std::cmp::min(collateral_amount, max_withdraw_amount);
13561340

13571341
obligation.withdraw(withdraw_amount, collateral_index)?;
13581342
obligation.last_update.mark_stale();
@@ -1486,7 +1470,9 @@ fn process_borrow_obligation_liquidity(
14861470
return Err(LendingError::InvalidMarketAuthority.into());
14871471
}
14881472

1489-
let remaining_borrow_value = obligation.remaining_borrow_value()?;
1473+
let remaining_borrow_value = obligation
1474+
.remaining_borrow_value()
1475+
.unwrap_or_else(|_| Decimal::zero());
14901476
if remaining_borrow_value == Decimal::zero() {
14911477
msg!("Remaining borrow value is zero");
14921478
return Err(LendingError::BorrowTooLarge.into());
@@ -2606,19 +2592,26 @@ fn get_pyth_product_quote_currency(
26062592
})
26072593
}
26082594

2595+
/// get_price tries to load the oracle price from pyth, and if it fails, uses switchboard.
2596+
/// The first element in the returned tuple is the market price, and the second is the optional
2597+
/// smoothed price (eg ema, twap).
26092598
fn get_price(
26102599
switchboard_feed_info: Option<&AccountInfo>,
26112600
pyth_price_account_info: &AccountInfo,
26122601
clock: &Clock,
2613-
) -> Result<Decimal, ProgramError> {
2614-
let pyth_price = get_pyth_price(pyth_price_account_info, clock).unwrap_or_default();
2615-
if pyth_price != Decimal::zero() {
2616-
return Ok(pyth_price);
2602+
) -> Result<(Decimal, Option<Decimal>), ProgramError> {
2603+
if let Ok(prices) = get_pyth_price(pyth_price_account_info, clock) {
2604+
return Ok((prices.0, Some(prices.1)));
26172605
}
26182606

26192607
// if switchboard was not passed in don't try to grab the price
26202608
if let Some(switchboard_feed_info_unwrapped) = switchboard_feed_info {
2621-
return get_switchboard_price(switchboard_feed_info_unwrapped, clock);
2609+
// TODO: add support for switchboard smoothed prices. Probably need to add a new
2610+
// switchboard account per reserve.
2611+
return match get_switchboard_price(switchboard_feed_info_unwrapped, clock) {
2612+
Ok(price) => Ok((price, None)),
2613+
Err(e) => Err(e),
2614+
};
26222615
}
26232616

26242617
Err(LendingError::InvalidOracleConfig.into())

token-lending/program/tests/helpers/mock_pyth.rs

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use pyth_sdk_solana::state::{
2-
AccountType, PriceAccount, PriceStatus, ProductAccount, MAGIC, PROD_ACCT_SIZE, PROD_ATTR_SIZE,
3-
VERSION_2,
2+
AccountType, PriceAccount, PriceStatus, ProductAccount, Rational, MAGIC, PROD_ACCT_SIZE,
3+
PROD_ATTR_SIZE, VERSION_2,
44
};
55
/// mock oracle prices in tests with this program.
66
use solana_program::{
@@ -34,7 +34,13 @@ pub enum MockPythInstruction {
3434

3535
/// Accounts:
3636
/// 0: PriceAccount
37-
SetPrice { price: i64, conf: u64, expo: i32 },
37+
SetPrice {
38+
price: i64,
39+
conf: u64,
40+
expo: i32,
41+
ema_price: i64,
42+
ema_conf: u64,
43+
},
3844

3945
/// Accounts:
4046
/// 0: AggregatorAccount
@@ -111,7 +117,13 @@ impl Processor {
111117

112118
Ok(())
113119
}
114-
MockPythInstruction::SetPrice { price, conf, expo } => {
120+
MockPythInstruction::SetPrice {
121+
price,
122+
conf,
123+
expo,
124+
ema_price,
125+
ema_conf,
126+
} => {
115127
msg!("Mock Pyth: Set price");
116128
let price_account_info = next_account_info(account_info_iter)?;
117129
let data = &mut price_account_info.try_borrow_mut_data()?;
@@ -121,6 +133,19 @@ impl Processor {
121133
price_account.agg.conf = conf;
122134
price_account.expo = expo;
123135

136+
price_account.ema_price = Rational {
137+
val: ema_price,
138+
// these fields don't matter
139+
numer: 1,
140+
denom: 1,
141+
};
142+
143+
price_account.ema_conf = Rational {
144+
val: ema_conf as i64,
145+
numer: 1,
146+
denom: 1,
147+
};
148+
124149
price_account.last_slot = Clock::get()?.slot;
125150
price_account.agg.pub_slot = Clock::get()?.slot;
126151
price_account.agg.status = PriceStatus::Trading;
@@ -201,10 +226,18 @@ pub fn set_price(
201226
price: i64,
202227
conf: u64,
203228
expo: i32,
229+
ema_price: i64,
230+
ema_conf: u64,
204231
) -> Instruction {
205-
let data = MockPythInstruction::SetPrice { price, conf, expo }
206-
.try_to_vec()
207-
.unwrap();
232+
let data = MockPythInstruction::SetPrice {
233+
price,
234+
conf,
235+
expo,
236+
ema_price,
237+
ema_conf,
238+
}
239+
.try_to_vec()
240+
.unwrap();
208241
Instruction {
209242
program_id,
210243
accounts: vec![AccountMeta::new(price_account_pubkey, false)],

token-lending/program/tests/helpers/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ pub mod usdc_mint {
6161
solana_program::declare_id!("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");
6262
}
6363

64+
pub mod usdt_mint {
65+
solana_program::declare_id!("Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB");
66+
}
67+
6468
pub mod wsol_mint {
6569
// fake mint, not the real wsol bc i can't mint wsol programmatically
6670
solana_program::declare_id!("So1m5eppzgokXLBt9Cg8KCMPWhHfTzVaGh26Y415MRG");

0 commit comments

Comments
 (0)