Perps: Support trusted markets

This commit is contained in:
Christian Kamm 2022-09-29 14:35:01 +02:00
parent 8ba52f46c2
commit 7e180c7b3a
7 changed files with 64 additions and 31 deletions

View File

@ -66,7 +66,7 @@ pub fn fetch_top(
}
}
// Negative pnl needs to be limited by spot_health.
// Negative pnl needs to be limited by perp_settle_health.
// We're doing it in a second step, because it's pretty expensive and we don't
// want to run this for all accounts.
if direction == Direction::MaxNegative {
@ -78,10 +78,10 @@ pub fn fetch_top(
} else {
I80F48::ZERO
};
let spot_health = crate::health_cache::new(context, account_fetcher, &acc)?
.spot_health(HealthType::Maint);
let settleable_pnl = if spot_health > 0 && !acc.being_liquidated() {
(*pnl).max(-spot_health)
let perp_settle_health =
crate::health_cache::new(context, account_fetcher, &acc)?.perp_settle_health();
let settleable_pnl = if perp_settle_health > 0 && !acc.being_liquidated() {
(*pnl).max(-perp_settle_health)
} else {
I80F48::ZERO
};

View File

@ -226,7 +226,7 @@ impl<'a> LiquidateHelper<'a> {
}
fn perp_settle_pnl(&self) -> anyhow::Result<Option<Signature>> {
let spot_health = self.health_cache.spot_health(HealthType::Maint);
let perp_settle_health = self.health_cache.perp_settle_health();
let mut perp_settleable_pnl = self
.liqee
.active_perp_positions()
@ -237,8 +237,8 @@ impl<'a> LiquidateHelper<'a> {
let pnl = pp.quote_position_native();
let settleable_pnl = if pnl > 0 {
pnl
} else if pnl < 0 && spot_health > 0 {
pnl.max(-spot_health)
} else if pnl < 0 && perp_settle_health > 0 {
pnl.max(-perp_settle_health)
} else {
return None;
};

View File

@ -66,13 +66,12 @@ pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>) -> Result<()> {
let a_init_health;
let a_maint_health;
let b_spot_health;
let b_settle_health;
{
let retriever =
ScanningAccountRetriever::new(ctx.remaining_accounts, &ctx.accounts.group.key())
.context("create account retriever")?;
b_spot_health =
new_health_cache(&account_b.borrow(), &retriever)?.spot_health(HealthType::Maint);
b_settle_health = new_health_cache(&account_b.borrow(), &retriever)?.perp_settle_health();
let a_cache = new_health_cache(&account_a.borrow(), &retriever)?;
a_init_health = a_cache.health(HealthType::Init);
a_maint_health = a_cache.health(HealthType::Maint);
@ -83,7 +82,7 @@ pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>) -> Result<()> {
// Example: With +100 USDC and -2 SOL (-80 USD) and -500 USD PNL the account may still settle
// 100 - 1.1*80 = 12 USD perp pnl, even though the overall health is already negative.
// Further settlement would convert perp-losses into token-losses and isn't allowed.
require!(b_spot_health >= 0, MangoError::HealthMustBePositive);
require!(b_settle_health >= 0, MangoError::HealthMustBePositive);
let mut bank = ctx.accounts.quote_bank.load_mut()?;
let perp_market = ctx.accounts.perp_market.load()?;
@ -117,8 +116,8 @@ pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>) -> Result<()> {
require!(a_pnl.is_positive(), MangoError::ProfitabilityMismatch);
require!(b_pnl.is_negative(), MangoError::ProfitabilityMismatch);
// Settle for the maximum possible capped to b's spot health
let settlement = a_pnl.abs().min(b_pnl.abs()).min(b_spot_health);
// Settle for the maximum possible capped to b's settle health
let settlement = a_pnl.abs().min(b_pnl.abs()).min(b_settle_health);
a_perp_position.change_quote_position(-settlement);
b_perp_position.change_quote_position(settlement);

View File

@ -526,6 +526,7 @@ pub struct PerpInfo {
pub quote: I80F48,
pub oracle_price: I80F48,
pub has_open_orders: bool,
pub trusted_market: bool,
}
impl PerpInfo {
@ -612,13 +613,14 @@ impl PerpInfo {
quote,
oracle_price,
has_open_orders: perp_position.has_open_orders(),
trusted_market: perp_market.trusted_market(),
})
}
/// Total health contribution from perp balances
///
/// Due to isolation of perp markets, users may never borrow against perp
/// positions without settling first: perp health is capped at zero.
/// positions in untrusted without settling first: perp health is capped at zero.
///
/// Users need to settle their perp pnl with other perp market participants
/// in order to realize their gains if they want to use them as collateral.
@ -631,9 +633,11 @@ impl PerpInfo {
fn health_contribution(&self, health_type: HealthType) -> I80F48 {
let c = self.uncapped_health_contribution(health_type);
// FUTURE: Allow v3-style "reliable" markets where we can return
// `self.quote + weight * self.base` here
c.min(I80F48::ZERO)
if self.trusted_market {
c
} else {
c.min(I80F48::ZERO)
}
}
#[inline(always)]
@ -837,7 +841,18 @@ impl HealthCache {
}
}
pub fn spot_health(&self, health_type: HealthType) -> I80F48 {
/// Compute the health when it comes to settling perp pnl
///
/// Examples:
/// - An account may have maint_health < 0, but settling perp pnl could still be allowed.
/// (+100 USDC health, -50 USDT health, -50 perp health -> allow settling 50 health worth)
/// - Positive health from trusted pnl markets counts
/// - If overall health is 0 with two trusted perp pnl < 0, settling may still be possible.
/// (+100 USDC health, -150 perp1 health, -150 perp2 health -> allow settling 100 health worth)
/// - Positive trusted perp pnl can enable settling.
/// (+100 trusted perp1 health, -100 perp2 health -> allow settling of 100 health worth)
pub fn perp_settle_health(&self) -> I80F48 {
let health_type = HealthType::Maint;
let mut health = I80F48::ZERO;
for token_info in self.token_infos.iter() {
let contrib = token_info.health_contribution(health_type);
@ -847,6 +862,12 @@ impl HealthCache {
let contrib = serum3_info.health_contribution(health_type, &self.token_infos);
cm!(health += contrib);
}
for perp_info in self.perp_infos.iter() {
if perp_info.trusted_market {
let positive_contrib = perp_info.health_contribution(health_type).max(I80F48::ZERO);
cm!(health += positive_contrib);
}
}
health
}
@ -1988,6 +2009,7 @@ mod tests {
quote: I80F48::ZERO,
oracle_price: I80F48::from_num(2.0),
has_open_orders: false,
trusted_market: false,
};
let health_cache = HealthCache {

View File

@ -130,6 +130,10 @@ impl PerpMarket {
self.group_insurance_fund = if v { 1 } else { 0 };
}
pub fn trusted_market(&self) -> bool {
self.trusted_market == 1
}
pub fn gen_order_id(&mut self, side: Side, price: i64) -> i128 {
self.seq_num += 1;

View File

@ -526,8 +526,8 @@ async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportEr
.await
.unwrap();
let liqee_spot_health_before = 1000.0 + 1.0 * 2.0 * 0.8;
let remaining_pnl = 20.0 * 100.0 - liq_amount_2 + liqee_spot_health_before;
let liqee_settle_health_before = 1000.0 + 1.0 * 2.0 * 0.8;
let remaining_pnl = 20.0 * 100.0 - liq_amount_2 + liqee_settle_health_before;
assert!(remaining_pnl < 0.0);
let liqee_data = solana.get_account::<MangoAccount>(account_1).await;
assert_eq!(liqee_data.perps[0].base_position_lots(), 0);

View File

@ -4462,6 +4462,10 @@ export type MangoV4 = {
{
"name": "hasOpenOrders",
"type": "bool"
},
{
"name": "trustedMarket",
"type": "bool"
}
]
}
@ -4544,7 +4548,7 @@ export type MangoV4 = {
"type": {
"array": [
"u8",
8
16
]
}
},
@ -4556,11 +4560,11 @@ export type MangoV4 = {
},
{
"name": "cumulativeDepositInterest",
"type": "i64"
"type": "f32"
},
{
"name": "cumulativeBorrowInterest",
"type": "i64"
"type": "f32"
}
]
}
@ -6070,12 +6074,12 @@ export type MangoV4 = {
},
{
"name": "cumulativeDepositInterest",
"type": "i64",
"type": "f32",
"index": false
},
{
"name": "cumulativeBorrowInterest",
"type": "i64",
"type": "f32",
"index": false
}
]
@ -10664,6 +10668,10 @@ export const IDL: MangoV4 = {
{
"name": "hasOpenOrders",
"type": "bool"
},
{
"name": "trustedMarket",
"type": "bool"
}
]
}
@ -10746,7 +10754,7 @@ export const IDL: MangoV4 = {
"type": {
"array": [
"u8",
8
16
]
}
},
@ -10758,11 +10766,11 @@ export const IDL: MangoV4 = {
},
{
"name": "cumulativeDepositInterest",
"type": "i64"
"type": "f32"
},
{
"name": "cumulativeBorrowInterest",
"type": "i64"
"type": "f32"
}
]
}
@ -12272,12 +12280,12 @@ export const IDL: MangoV4 = {
},
{
"name": "cumulativeDepositInterest",
"type": "i64",
"type": "f32",
"index": false
},
{
"name": "cumulativeBorrowInterest",
"type": "i64",
"type": "f32",
"index": false
}
]