Skip to content
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

Liquidation pricing improvements #130

Merged
merged 3 commits into from
Nov 2, 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
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
Loading