Skip to content

Commit

Permalink
Merge pull request #130 from mrgnlabs/j/liquidation-pricing-fix
Browse files Browse the repository at this point in the history
  • Loading branch information
jkbpvsc authored Nov 2, 2023
2 parents 74737e0 + d9eeec9 commit 5be684b
Show file tree
Hide file tree
Showing 4 changed files with 87 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use crate::state::marginfi_account::{
calc_asset_amount, calc_asset_value, RiskEngine, RiskRequirementType,
};
use crate::state::marginfi_group::{Bank, BankVaultType};
use crate::state::price::{OraclePriceFeedAdapter, PriceAdapter};
use crate::state::price::{OraclePriceFeedAdapter, PriceAdapter, PriceBias};
use crate::{
bank_signer,
constants::{LIQUIDITY_VAULT_AUTHORITY_SEED, LIQUIDITY_VAULT_SEED},
Expand Down Expand Up @@ -118,7 +118,7 @@ pub fn lending_account_liquidate(
current_timestamp,
MAX_PRICE_AGE_SEC,
)?;
asset_pf.get_price()?
asset_pf.get_price_non_weighted(Some(PriceBias::Low))?
};

let mut liab_bank = ctx.accounts.liab_bank.load_mut()?;
Expand All @@ -131,7 +131,7 @@ pub fn lending_account_liquidate(
MAX_PRICE_AGE_SEC,
)?;

liab_pf.get_price()?
liab_pf.get_price_non_weighted(Some(PriceBias::High))?
};

let final_discount = I80F48::ONE - (LIQUIDATION_INSURANCE_FEE + LIQUIDATION_LIQUIDATOR_FEE);
Expand Down
84 changes: 75 additions & 9 deletions programs/marginfi/src/state/price.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,22 @@ pub enum OracleSetup {
SwitchboardV2,
}

#[derive(Copy, Clone, Debug)]
pub enum PriceBias {
Low,
High,
}

#[enum_dispatch]
pub trait PriceAdapter {
fn get_price(&self) -> MarginfiResult<I80F48>;
fn get_confidence_interval(&self) -> MarginfiResult<I80F48>;
/// Get a normalized price range for the given price feed.
/// The range is the price +/- the CONF_INTERVAL_MULTIPLE * confidence interval.
fn get_price_range(&self) -> MarginfiResult<(I80F48, I80F48)>;
/// Get the price without any weighting applied.
/// This is the price that is used for liquidation.
fn get_price_non_weighted(&self, bias: Option<PriceBias>) -> MarginfiResult<I80F48>;
}

#[enum_dispatch(PriceAdapter)]
Expand Down Expand Up @@ -108,17 +117,23 @@ impl OraclePriceFeedAdapter {
}

pub struct PythEmaPriceFeed {
ema_price: Box<Price>,
price: Box<Price>,
}

impl PythEmaPriceFeed {
pub fn load_checked(ai: &AccountInfo, current_time: i64, max_age: u64) -> MarginfiResult<Self> {
let price_feed = load_pyth_price_feed(ai)?;
let price = price_feed
let ema_price = price_feed
.get_ema_price_no_older_than(current_time, max_age)
.ok_or(MarginfiError::StaleOracle)?;

let price = price_feed
.get_price_no_older_than(current_time, max_age)
.ok_or(MarginfiError::StaleOracle)?;

Ok(Self {
ema_price: Box::new(ema_price),
price: Box::new(price),
})
}
Expand All @@ -127,16 +142,16 @@ impl PythEmaPriceFeed {
load_pyth_price_feed(ai)?;
Ok(())
}
}

impl PriceAdapter for PythEmaPriceFeed {
fn get_price(&self) -> MarginfiResult<I80F48> {
pyth_price_components_to_i80f48(I80F48::from_num(self.price.price), self.price.expo)
}
fn get_confidence_interval(&self, use_ema: bool) -> MarginfiResult<I80F48> {
let price = if use_ema {
&self.ema_price
} else {
&self.price
};

fn get_confidence_interval(&self) -> MarginfiResult<I80F48> {
let conf_interval =
pyth_price_components_to_i80f48(I80F48::from_num(self.price.conf), self.price.expo)?
pyth_price_components_to_i80f48(I80F48::from_num(price.conf), price.expo)?
.checked_mul(CONF_INTERVAL_MULTIPLE)
.ok_or_else(math_error!())?;

Expand All @@ -147,10 +162,20 @@ impl PriceAdapter for PythEmaPriceFeed {

Ok(conf_interval)
}
}

impl PriceAdapter for PythEmaPriceFeed {
fn get_price(&self) -> MarginfiResult<I80F48> {
pyth_price_components_to_i80f48(I80F48::from_num(self.ema_price.price), self.ema_price.expo)
}

fn get_confidence_interval(&self) -> MarginfiResult<I80F48> {
self.get_confidence_interval(true)
}

fn get_price_range(&self) -> MarginfiResult<(I80F48, I80F48)> {
let base_price = self.get_price()?;
let price_range = self.get_confidence_interval()?;
let price_range = self.get_confidence_interval(true)?;

let lowest_price = base_price
.checked_sub(price_range)
Expand All @@ -161,6 +186,27 @@ impl PriceAdapter for PythEmaPriceFeed {

Ok((lowest_price, highest_price))
}

fn get_price_non_weighted(&self, price_bias: Option<PriceBias>) -> MarginfiResult<I80F48> {
let price =
pyth_price_components_to_i80f48(I80F48::from_num(self.price.price), self.price.expo)?;

match price_bias {
None => Ok(price),
Some(price_bias) => {
let confidence_interval = self.get_confidence_interval(false)?;

match price_bias {
PriceBias::Low => Ok(price
.checked_sub(confidence_interval)
.ok_or_else(math_error!())?),
PriceBias::High => Ok(price
.checked_add(confidence_interval)
.ok_or_else(math_error!())?),
}
}
}
}
}

pub struct SwitchboardV2PriceFeed {
Expand Down Expand Up @@ -248,6 +294,26 @@ impl PriceAdapter for SwitchboardV2PriceFeed {

Ok((lowest_price, highest_price))
}

fn get_price_non_weighted(&self, price_bias: Option<PriceBias>) -> MarginfiResult<I80F48> {
let price = self.get_price()?;

match price_bias {
Some(price_bias) => {
let confidence_interval = self.get_confidence_interval()?;

match price_bias {
PriceBias::Low => Ok(price
.checked_sub(confidence_interval)
.ok_or_else(math_error!())?),
PriceBias::High => Ok(price
.checked_add(confidence_interval)
.ok_or_else(math_error!())?),
}
}
None => Ok(price),
}
}
}

/// A slimmed down version of the AggregatorAccountData struct copied from the switchboard-v2/src/aggregator.rs
Expand Down
8 changes: 4 additions & 4 deletions programs/marginfi/tests/marginfi_account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -925,7 +925,7 @@ async fn marginfi_account_liquidation_success_many_balances() -> anyhow::Result<
assert_eq_noise!(
insurance_fund_usdc.balance().await as i64,
native!(0.25, "USDC", f64) as i64,
1
native!(0.001, "USDC", f64) as i64
);

Ok(())
Expand Down Expand Up @@ -1012,7 +1012,7 @@ async fn marginfi_account_liquidation_success_swb() -> anyhow::Result<()> {
.get_asset_amount(depositor_ma.lending_account.balances[0].asset_shares.into())
.unwrap(),
I80F48::from(native!(1990.25, "USDC", f64)),
native!(0.00001, "USDC", f64)
native!(0.01, "USDC", f64)
);

// Borrower should have 99 SOL
Expand All @@ -1033,7 +1033,7 @@ async fn marginfi_account_liquidation_success_swb() -> anyhow::Result<()> {
)
.unwrap(),
I80F48::from(native!(989.50, "USDC", f64)),
native!(0.00001, "USDC", f64)
native!(0.01, "USDC", f64)
);

// Check insurance fund fee
Expand All @@ -1044,7 +1044,7 @@ async fn marginfi_account_liquidation_success_swb() -> anyhow::Result<()> {
assert_eq_noise!(
insurance_fund_usdc.balance().await as i64,
native!(0.25, "USDC", f64) as i64,
1
native!(0.001, "USDC", f64) as i64
);

Ok(())
Expand Down
5 changes: 5 additions & 0 deletions test-utils/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@ pub fn create_pyth_price_account(
denom: 1,
},
prev_timestamp: timestamp.unwrap_or(0),
ema_conf: Rational {
val: 0,
numer: 0,
denom: 1,
},
..Default::default()
})
.to_vec(),
Expand Down

0 comments on commit 5be684b

Please sign in to comment.