Perp: liq with token instruction (#368)
The new instruction allows the liqor to take over negative pnl (limited by liqee settle health and settle limits) before applying the bankruptcy logic.
This commit is contained in:
parent
c5d875e04d
commit
93d33edb74
|
@ -879,7 +879,7 @@ impl MangoClient {
|
||||||
self.send_and_confirm_owner_tx(vec![ix]).await
|
self.send_and_confirm_owner_tx(vec![ix]).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn perp_liq_bankruptcy(
|
pub async fn perp_liq_quote_and_bankruptcy(
|
||||||
&self,
|
&self,
|
||||||
liqee: (&Pubkey, &MangoAccountValue),
|
liqee: (&Pubkey, &MangoAccountValue),
|
||||||
market_index: PerpMarketIndex,
|
market_index: PerpMarketIndex,
|
||||||
|
@ -903,9 +903,10 @@ impl MangoClient {
|
||||||
program_id: mango_v4::id(),
|
program_id: mango_v4::id(),
|
||||||
accounts: {
|
accounts: {
|
||||||
let mut ams = anchor_lang::ToAccountMetas::to_account_metas(
|
let mut ams = anchor_lang::ToAccountMetas::to_account_metas(
|
||||||
&mango_v4::accounts::PerpLiqBankruptcy {
|
&mango_v4::accounts::PerpLiqQuoteAndBankruptcy {
|
||||||
group: self.group(),
|
group: self.group(),
|
||||||
perp_market: perp.address,
|
perp_market: perp.address,
|
||||||
|
oracle: perp.market.oracle,
|
||||||
liqor: self.mango_account_address,
|
liqor: self.mango_account_address,
|
||||||
liqor_owner: self.owner(),
|
liqor_owner: self.owner(),
|
||||||
liqee: *liqee.0,
|
liqee: *liqee.0,
|
||||||
|
@ -920,9 +921,9 @@ impl MangoClient {
|
||||||
ams.extend(health_remaining_ams.into_iter());
|
ams.extend(health_remaining_ams.into_iter());
|
||||||
ams
|
ams
|
||||||
},
|
},
|
||||||
data: anchor_lang::InstructionData::data(&mango_v4::instruction::PerpLiqBankruptcy {
|
data: anchor_lang::InstructionData::data(
|
||||||
max_liab_transfer,
|
&mango_v4::instruction::PerpLiqQuoteAndBankruptcy { max_liab_transfer },
|
||||||
}),
|
),
|
||||||
};
|
};
|
||||||
self.send_and_confirm_owner_tx(vec![ix]).await
|
self.send_and_confirm_owner_tx(vec![ix]).await
|
||||||
}
|
}
|
||||||
|
|
|
@ -247,6 +247,7 @@ impl<'a> LiquidateHelper<'a> {
|
||||||
Ok(Some(sig))
|
Ok(Some(sig))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
async fn perp_settle_pnl(&self) -> anyhow::Result<Option<Signature>> {
|
async fn perp_settle_pnl(&self) -> anyhow::Result<Option<Signature>> {
|
||||||
let perp_settle_health = self.health_cache.perp_settle_health();
|
let perp_settle_health = self.health_cache.perp_settle_health();
|
||||||
let mut perp_settleable_pnl = self
|
let mut perp_settleable_pnl = self
|
||||||
|
@ -257,6 +258,7 @@ impl<'a> LiquidateHelper<'a> {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let pnl = pp.quote_position_native();
|
let pnl = pp.quote_position_native();
|
||||||
|
// TODO: outdated: must account for perp settle limit
|
||||||
let settleable_pnl = if pnl > 0 {
|
let settleable_pnl = if pnl > 0 {
|
||||||
pnl
|
pnl
|
||||||
} else if pnl < 0 && perp_settle_health > 0 {
|
} else if pnl < 0 && perp_settle_health > 0 {
|
||||||
|
@ -320,12 +322,13 @@ impl<'a> LiquidateHelper<'a> {
|
||||||
}
|
}
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
async fn perp_liq_bankruptcy(&self) -> anyhow::Result<Option<Signature>> {
|
async fn perp_liq_quote_and_bankruptcy(&self) -> anyhow::Result<Option<Signature>> {
|
||||||
if self.health_cache.has_liquidatable_assets() {
|
if !self.health_cache.in_phase3_liquidation() {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
let mut perp_bankruptcies = self
|
let mut perp_negative_pnl = self
|
||||||
.liqee
|
.liqee
|
||||||
.active_perp_positions()
|
.active_perp_positions()
|
||||||
.filter_map(|pp| {
|
.filter_map(|pp| {
|
||||||
|
@ -336,24 +339,24 @@ impl<'a> LiquidateHelper<'a> {
|
||||||
Some((pp.market_index, quote))
|
Some((pp.market_index, quote))
|
||||||
})
|
})
|
||||||
.collect::<Vec<(PerpMarketIndex, I80F48)>>();
|
.collect::<Vec<(PerpMarketIndex, I80F48)>>();
|
||||||
perp_bankruptcies.sort_by(|a, b| a.1.cmp(&b.1));
|
perp_negative_pnl.sort_by(|a, b| a.1.cmp(&b.1));
|
||||||
|
|
||||||
if perp_bankruptcies.is_empty() {
|
if perp_negative_pnl.is_empty() {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
let (perp_market_index, _) = perp_bankruptcies.first().unwrap();
|
let (perp_market_index, _) = perp_negative_pnl.first().unwrap();
|
||||||
|
|
||||||
let sig = self
|
let sig = self
|
||||||
.client
|
.client
|
||||||
.perp_liq_bankruptcy(
|
.perp_liq_quote_and_bankruptcy(
|
||||||
(self.pubkey, &self.liqee),
|
(self.pubkey, &self.liqee),
|
||||||
*perp_market_index,
|
*perp_market_index,
|
||||||
// Always use the max amount, since the health effect is always positive
|
// Always use the max amount, since the health effect is >= 0
|
||||||
u64::MAX,
|
u64::MAX,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
log::info!(
|
log::info!(
|
||||||
"Liquidated bankruptcy for perp market on account {}, market index {}, maint_health was {}, tx sig {:?}",
|
"Liquidated negative perp pnl on account {}, market index {}, maint_health was {}, tx sig {:?}",
|
||||||
self.pubkey,
|
self.pubkey,
|
||||||
perp_market_index,
|
perp_market_index,
|
||||||
self.maint_health,
|
self.maint_health,
|
||||||
|
@ -564,7 +567,10 @@ impl<'a> LiquidateHelper<'a> {
|
||||||
// return Ok(txsig);
|
// return Ok(txsig);
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// Try to close orders before touching the user's positions
|
//
|
||||||
|
// Phase 1: Try to close orders before touching the user's positions
|
||||||
|
//
|
||||||
|
// TODO: All these close ix could be in one transaction.
|
||||||
if let Some(txsig) = self.perp_close_orders().await? {
|
if let Some(txsig) = self.perp_close_orders().await? {
|
||||||
return Ok(txsig);
|
return Ok(txsig);
|
||||||
}
|
}
|
||||||
|
@ -572,6 +578,10 @@ impl<'a> LiquidateHelper<'a> {
|
||||||
return Ok(txsig);
|
return Ok(txsig);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Phase 2: token, perp base, TODO: perp positive trusted pnl
|
||||||
|
//
|
||||||
|
|
||||||
if let Some(txsig) = self.perp_liq_base_position().await? {
|
if let Some(txsig) = self.perp_liq_base_position().await? {
|
||||||
return Ok(txsig);
|
return Ok(txsig);
|
||||||
}
|
}
|
||||||
|
@ -581,16 +591,20 @@ impl<'a> LiquidateHelper<'a> {
|
||||||
// It's possible that some positive pnl can't be settled (if there's
|
// It's possible that some positive pnl can't be settled (if there's
|
||||||
// no liquid counterparty) and that some negative pnl can't be settled
|
// no liquid counterparty) and that some negative pnl can't be settled
|
||||||
// (if the liqee isn't liquid enough).
|
// (if the liqee isn't liquid enough).
|
||||||
if let Some(txsig) = self.perp_settle_pnl().await? {
|
// if let Some(txsig) = self.perp_settle_pnl().await? {
|
||||||
return Ok(txsig);
|
// return Ok(txsig);
|
||||||
}
|
// }
|
||||||
|
|
||||||
if let Some(txsig) = self.token_liq().await? {
|
if let Some(txsig) = self.token_liq().await? {
|
||||||
return Ok(txsig);
|
return Ok(txsig);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Socialize/insurance fund unsettleable negative pnl
|
//
|
||||||
if let Some(txsig) = self.perp_liq_bankruptcy().await? {
|
// Phase 3: perp and token bankruptcy
|
||||||
|
//
|
||||||
|
|
||||||
|
// Negative pnl: take over (paid by liqee or insurance) or socialize the loss
|
||||||
|
if let Some(txsig) = self.perp_liq_quote_and_bankruptcy().await? {
|
||||||
return Ok(txsig);
|
return Ok(txsig);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -69,6 +69,16 @@ pub enum MangoError {
|
||||||
TokenInReduceOnlyMode,
|
TokenInReduceOnlyMode,
|
||||||
#[msg("market is in reduce only mode")]
|
#[msg("market is in reduce only mode")]
|
||||||
MarketInReduceOnlyMode,
|
MarketInReduceOnlyMode,
|
||||||
|
#[msg("the perp position has non-zero base lots")]
|
||||||
|
PerpHasBaseLots,
|
||||||
|
#[msg("there are open or unsettled serum3 orders")]
|
||||||
|
HasOpenOrUnsettledSerum3Orders,
|
||||||
|
#[msg("has liquidatable token position")]
|
||||||
|
HasLiquidatableTokenPosition,
|
||||||
|
#[msg("has liquidatable perp base position")]
|
||||||
|
HasLiquidatablePerpBasePosition,
|
||||||
|
#[msg("has liquidatable trusted perp pnl")]
|
||||||
|
HasLiquidatableTrustedPerpPnl,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MangoError {
|
impl MangoError {
|
||||||
|
|
|
@ -4,8 +4,7 @@ use fixed::types::I80F48;
|
||||||
|
|
||||||
use crate::error::*;
|
use crate::error::*;
|
||||||
use crate::state::{
|
use crate::state::{
|
||||||
Bank, MangoAccountFixed, MangoAccountRef, PerpMarket, PerpMarketIndex, PerpPosition,
|
Bank, MangoAccountRef, PerpMarket, PerpMarketIndex, PerpPosition, Serum3MarketIndex, TokenIndex,
|
||||||
Serum3MarketIndex, TokenIndex,
|
|
||||||
};
|
};
|
||||||
use crate::util::checked_math as cm;
|
use crate::util::checked_math as cm;
|
||||||
|
|
||||||
|
@ -135,6 +134,8 @@ pub struct Serum3Info {
|
||||||
pub base_index: usize,
|
pub base_index: usize,
|
||||||
pub quote_index: usize,
|
pub quote_index: usize,
|
||||||
pub market_index: Serum3MarketIndex,
|
pub market_index: Serum3MarketIndex,
|
||||||
|
/// The open orders account has no free or reserved funds
|
||||||
|
pub has_zero_funds: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Serum3Info {
|
impl Serum3Info {
|
||||||
|
@ -328,29 +329,6 @@ impl HealthCache {
|
||||||
health
|
health
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn check_health_pre(&self, account: &mut MangoAccountFixed) -> Result<I80F48> {
|
|
||||||
let pre_health = self.health(HealthType::Init);
|
|
||||||
msg!("pre_health: {}", pre_health);
|
|
||||||
account.maybe_recover_from_being_liquidated(pre_health);
|
|
||||||
require!(!account.being_liquidated(), MangoError::BeingLiquidated);
|
|
||||||
Ok(pre_health)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn check_health_post(
|
|
||||||
&self,
|
|
||||||
account: &mut MangoAccountFixed,
|
|
||||||
pre_health: I80F48,
|
|
||||||
) -> Result<()> {
|
|
||||||
let post_health = self.health(HealthType::Init);
|
|
||||||
msg!("post_health: {}", post_health);
|
|
||||||
require!(
|
|
||||||
post_health >= 0 || post_health > pre_health,
|
|
||||||
MangoError::HealthMustBePositiveOrIncrease
|
|
||||||
);
|
|
||||||
account.maybe_recover_from_being_liquidated(post_health);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn token_info(&self, token_index: TokenIndex) -> Result<&TokenInfo> {
|
pub fn token_info(&self, token_index: TokenIndex) -> Result<&TokenInfo> {
|
||||||
Ok(&self.token_infos[self.token_info_index(token_index)?])
|
Ok(&self.token_infos[self.token_info_index(token_index)?])
|
||||||
}
|
}
|
||||||
|
@ -446,13 +424,94 @@ impl HealthCache {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn has_serum3_open_orders_funds(&self) -> bool {
|
||||||
|
self.serum3_infos.iter().any(|si| !si.has_zero_funds)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_perp_open_orders(&self) -> bool {
|
||||||
|
self.perp_infos.iter().any(|p| p.has_open_orders)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_perp_base_positions(&self) -> bool {
|
||||||
|
self.perp_infos.iter().any(|p| p.base_lots != 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_perp_positive_trusted_pnl_without_base_position(&self) -> bool {
|
||||||
|
self.perp_infos
|
||||||
|
.iter()
|
||||||
|
.any(|p| p.trusted_market && p.base_lots == 0 && p.quote > 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_perp_negative_pnl(&self) -> bool {
|
||||||
|
self.perp_infos.iter().any(|p| p.quote < 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Phase1 is spot/perp order cancellation and spot settlement since
|
||||||
|
/// neither of these come at a cost to the liqee
|
||||||
|
pub fn has_phase1_liquidatable(&self) -> bool {
|
||||||
|
self.has_serum3_open_orders_funds() || self.has_perp_open_orders()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn require_after_phase1_liquidation(&self) -> Result<()> {
|
||||||
|
require!(
|
||||||
|
!self.has_serum3_open_orders_funds(),
|
||||||
|
MangoError::HasOpenOrUnsettledSerum3Orders
|
||||||
|
);
|
||||||
|
require!(!self.has_perp_open_orders(), MangoError::HasOpenPerpOrders);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn in_phase1_liquidation(&self) -> bool {
|
||||||
|
self.has_phase1_liquidatable()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Phase2 is for:
|
||||||
|
/// - token-token liquidation
|
||||||
|
/// - liquidation of perp base positions
|
||||||
|
/// - bringing positive trusted perp pnl into the spot realm
|
||||||
|
pub fn has_phase2_liquidatable(&self) -> bool {
|
||||||
|
self.has_spot_assets() && self.has_spot_borrows()
|
||||||
|
|| self.has_perp_base_positions()
|
||||||
|
|| self.has_perp_positive_trusted_pnl_without_base_position()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn require_after_phase2_liquidation(&self) -> Result<()> {
|
||||||
|
self.require_after_phase1_liquidation()?;
|
||||||
|
require!(
|
||||||
|
!self.has_spot_assets() || !self.has_spot_borrows(),
|
||||||
|
MangoError::HasLiquidatableTokenPosition
|
||||||
|
);
|
||||||
|
require!(
|
||||||
|
!self.has_perp_base_positions(),
|
||||||
|
MangoError::HasLiquidatablePerpBasePosition
|
||||||
|
);
|
||||||
|
require!(
|
||||||
|
!self.has_perp_positive_trusted_pnl_without_base_position(),
|
||||||
|
MangoError::HasLiquidatableTrustedPerpPnl
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn in_phase2_liquidation(&self) -> bool {
|
||||||
|
!self.has_phase1_liquidatable() && self.has_phase2_liquidatable()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Phase3 is bankruptcy:
|
||||||
|
/// - token bankruptcy
|
||||||
|
/// - perp bankruptcy
|
||||||
|
pub fn has_phase3_liquidatable(&self) -> bool {
|
||||||
|
self.has_spot_borrows() || self.has_perp_negative_pnl()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn in_phase3_liquidation(&self) -> bool {
|
||||||
|
!self.has_phase1_liquidatable()
|
||||||
|
&& !self.has_phase2_liquidatable()
|
||||||
|
&& self.has_phase3_liquidatable()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn has_liquidatable_assets(&self) -> bool {
|
pub fn has_liquidatable_assets(&self) -> bool {
|
||||||
let spot_liquidatable = self.has_spot_assets();
|
let spot_liquidatable = self.has_spot_assets();
|
||||||
// can use serum3_liq_force_cancel_orders
|
let serum3_cancelable = self.has_serum3_open_orders_funds();
|
||||||
let serum3_cancelable = self
|
|
||||||
.serum3_infos
|
|
||||||
.iter()
|
|
||||||
.any(|si| si.reserved_base != 0 || si.reserved_quote != 0);
|
|
||||||
let perp_liquidatable = self.perp_infos.iter().any(|p| {
|
let perp_liquidatable = self.perp_infos.iter().any(|p| {
|
||||||
// can use perp_liq_base_position
|
// can use perp_liq_base_position
|
||||||
p.base_lots != 0
|
p.base_lots != 0
|
||||||
|
@ -477,6 +536,21 @@ impl HealthCache {
|
||||||
self.has_spot_borrows() || perp_borrows
|
self.has_spot_borrows() || perp_borrows
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn has_liquidatable_spot_or_perp_base(&self) -> bool {
|
||||||
|
let spot_liquidatable = self.has_spot_assets();
|
||||||
|
let serum3_cancelable = self.has_serum3_open_orders_funds();
|
||||||
|
let perp_liquidatable = self.perp_infos.iter().any(|p| {
|
||||||
|
// can use perp_liq_base_position
|
||||||
|
p.base_lots != 0
|
||||||
|
// can use perp_liq_force_cancel_orders
|
||||||
|
|| p.has_open_orders
|
||||||
|
// A remaining quote position can be reduced with perp_settle_pnl and that can improve health.
|
||||||
|
// However, since it's not guaranteed that there is a counterparty, a positive perp quote position
|
||||||
|
// does not prevent bankruptcy.
|
||||||
|
});
|
||||||
|
spot_liquidatable || serum3_cancelable || perp_liquidatable
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn compute_serum3_reservations(
|
pub(crate) fn compute_serum3_reservations(
|
||||||
&self,
|
&self,
|
||||||
health_type: HealthType,
|
health_type: HealthType,
|
||||||
|
@ -671,6 +745,9 @@ pub fn new_health_cache(
|
||||||
base_index,
|
base_index,
|
||||||
quote_index,
|
quote_index,
|
||||||
market_index: serum_account.market_index,
|
market_index: serum_account.market_index,
|
||||||
|
has_zero_funds: oo.native_coin_total == 0
|
||||||
|
&& oo.native_pc_total == 0
|
||||||
|
&& oo.referrer_rebates_accrued == 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -846,6 +846,7 @@ mod tests {
|
||||||
market_index: 0,
|
market_index: 0,
|
||||||
reserved_base: I80F48::from(30 / 3),
|
reserved_base: I80F48::from(30 / 3),
|
||||||
reserved_quote: I80F48::from(30 / 2),
|
reserved_quote: I80F48::from(30 / 2),
|
||||||
|
has_zero_funds: false,
|
||||||
}];
|
}];
|
||||||
adjust_by_usdc(&mut health_cache, 0, -20.0);
|
adjust_by_usdc(&mut health_cache, 0, -20.0);
|
||||||
adjust_by_usdc(&mut health_cache, 1, -40.0);
|
adjust_by_usdc(&mut health_cache, 1, -40.0);
|
||||||
|
|
|
@ -20,9 +20,9 @@ pub use perp_consume_events::*;
|
||||||
pub use perp_create_market::*;
|
pub use perp_create_market::*;
|
||||||
pub use perp_deactivate_position::*;
|
pub use perp_deactivate_position::*;
|
||||||
pub use perp_edit_market::*;
|
pub use perp_edit_market::*;
|
||||||
pub use perp_liq_bankruptcy::*;
|
|
||||||
pub use perp_liq_base_position::*;
|
pub use perp_liq_base_position::*;
|
||||||
pub use perp_liq_force_cancel_orders::*;
|
pub use perp_liq_force_cancel_orders::*;
|
||||||
|
pub use perp_liq_quote_and_bankruptcy::*;
|
||||||
pub use perp_place_order::*;
|
pub use perp_place_order::*;
|
||||||
pub use perp_settle_fees::*;
|
pub use perp_settle_fees::*;
|
||||||
pub use perp_settle_pnl::*;
|
pub use perp_settle_pnl::*;
|
||||||
|
@ -73,9 +73,9 @@ mod perp_consume_events;
|
||||||
mod perp_create_market;
|
mod perp_create_market;
|
||||||
mod perp_deactivate_position;
|
mod perp_deactivate_position;
|
||||||
mod perp_edit_market;
|
mod perp_edit_market;
|
||||||
mod perp_liq_bankruptcy;
|
|
||||||
mod perp_liq_base_position;
|
mod perp_liq_base_position;
|
||||||
mod perp_liq_force_cancel_orders;
|
mod perp_liq_force_cancel_orders;
|
||||||
|
mod perp_liq_quote_and_bankruptcy;
|
||||||
mod perp_place_order;
|
mod perp_place_order;
|
||||||
mod perp_settle_fees;
|
mod perp_settle_fees;
|
||||||
mod perp_settle_pnl;
|
mod perp_settle_pnl;
|
||||||
|
|
|
@ -71,7 +71,6 @@ pub fn perp_consume_events(ctx: Context<PerpConsumeEvents>, limit: usize) -> Res
|
||||||
emit_perp_balances(
|
emit_perp_balances(
|
||||||
ctx.accounts.group.key(),
|
ctx.accounts.group.key(),
|
||||||
fill.maker,
|
fill.maker,
|
||||||
perp_market.perp_market_index,
|
|
||||||
ma.perp_position(perp_market.perp_market_index).unwrap(),
|
ma.perp_position(perp_market.perp_market_index).unwrap(),
|
||||||
&perp_market,
|
&perp_market,
|
||||||
);
|
);
|
||||||
|
@ -123,14 +122,12 @@ pub fn perp_consume_events(ctx: Context<PerpConsumeEvents>, limit: usize) -> Res
|
||||||
emit_perp_balances(
|
emit_perp_balances(
|
||||||
ctx.accounts.group.key(),
|
ctx.accounts.group.key(),
|
||||||
fill.maker,
|
fill.maker,
|
||||||
perp_market.perp_market_index,
|
|
||||||
maker.perp_position(perp_market.perp_market_index).unwrap(),
|
maker.perp_position(perp_market.perp_market_index).unwrap(),
|
||||||
&perp_market,
|
&perp_market,
|
||||||
);
|
);
|
||||||
emit_perp_balances(
|
emit_perp_balances(
|
||||||
ctx.accounts.group.key(),
|
ctx.accounts.group.key(),
|
||||||
fill.taker,
|
fill.taker,
|
||||||
perp_market.perp_market_index,
|
|
||||||
taker.perp_position(perp_market.perp_market_index).unwrap(),
|
taker.perp_position(perp_market.perp_market_index).unwrap(),
|
||||||
&perp_market,
|
&perp_market,
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,238 +0,0 @@
|
||||||
use anchor_lang::prelude::*;
|
|
||||||
use anchor_spl::token;
|
|
||||||
use anchor_spl::token::Token;
|
|
||||||
use anchor_spl::token::TokenAccount;
|
|
||||||
use fixed::types::I80F48;
|
|
||||||
|
|
||||||
use crate::error::*;
|
|
||||||
use crate::health::*;
|
|
||||||
use crate::state::*;
|
|
||||||
use crate::util::checked_math as cm;
|
|
||||||
|
|
||||||
use crate::logs::{emit_perp_balances, PerpLiqBankruptcyLog, TokenBalanceLog};
|
|
||||||
|
|
||||||
// Remaining accounts:
|
|
||||||
// - merged health accounts for liqor+liqee
|
|
||||||
#[derive(Accounts)]
|
|
||||||
pub struct PerpLiqBankruptcy<'info> {
|
|
||||||
#[account(
|
|
||||||
has_one = insurance_vault,
|
|
||||||
)]
|
|
||||||
pub group: AccountLoader<'info, Group>,
|
|
||||||
|
|
||||||
#[account(mut, has_one = group)]
|
|
||||||
pub perp_market: AccountLoader<'info, PerpMarket>,
|
|
||||||
|
|
||||||
#[account(
|
|
||||||
mut,
|
|
||||||
has_one = group
|
|
||||||
// liqor_owner is checked at #1
|
|
||||||
)]
|
|
||||||
pub liqor: AccountLoader<'info, MangoAccountFixed>,
|
|
||||||
pub liqor_owner: Signer<'info>,
|
|
||||||
|
|
||||||
#[account(
|
|
||||||
mut,
|
|
||||||
has_one = group
|
|
||||||
)]
|
|
||||||
pub liqee: AccountLoader<'info, MangoAccountFixed>,
|
|
||||||
|
|
||||||
#[account(
|
|
||||||
mut,
|
|
||||||
has_one = group,
|
|
||||||
// address is checked at #2
|
|
||||||
)]
|
|
||||||
pub settle_bank: AccountLoader<'info, Bank>,
|
|
||||||
|
|
||||||
#[account(
|
|
||||||
mut,
|
|
||||||
address = settle_bank.load()?.vault
|
|
||||||
)]
|
|
||||||
pub settle_vault: Account<'info, TokenAccount>,
|
|
||||||
|
|
||||||
/// CHECK: Oracle can have different account types
|
|
||||||
#[account(address = settle_bank.load()?.oracle)]
|
|
||||||
pub settle_oracle: UncheckedAccount<'info>,
|
|
||||||
|
|
||||||
// future: this would be an insurance fund vault specific to a
|
|
||||||
// trustless token, separate from the shared one on the group
|
|
||||||
#[account(mut)]
|
|
||||||
pub insurance_vault: Account<'info, TokenAccount>,
|
|
||||||
|
|
||||||
pub token_program: Program<'info, Token>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'info> PerpLiqBankruptcy<'info> {
|
|
||||||
pub fn transfer_ctx(&self) -> CpiContext<'_, '_, '_, 'info, token::Transfer<'info>> {
|
|
||||||
let program = self.token_program.to_account_info();
|
|
||||||
let accounts = token::Transfer {
|
|
||||||
from: self.insurance_vault.to_account_info(),
|
|
||||||
to: self.settle_vault.to_account_info(),
|
|
||||||
authority: self.group.to_account_info(),
|
|
||||||
};
|
|
||||||
CpiContext::new(program, accounts)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn perp_liq_bankruptcy(ctx: Context<PerpLiqBankruptcy>, max_liab_transfer: u64) -> Result<()> {
|
|
||||||
let group = ctx.accounts.group.load()?;
|
|
||||||
let group_pk = &ctx.accounts.group.key();
|
|
||||||
|
|
||||||
let mut liqor = ctx.accounts.liqor.load_full_mut()?;
|
|
||||||
// account constraint #1
|
|
||||||
require!(
|
|
||||||
liqor
|
|
||||||
.fixed
|
|
||||||
.is_owner_or_delegate(ctx.accounts.liqor_owner.key()),
|
|
||||||
MangoError::SomeError
|
|
||||||
);
|
|
||||||
require!(!liqor.fixed.being_liquidated(), MangoError::BeingLiquidated);
|
|
||||||
|
|
||||||
let mut liqee = ctx.accounts.liqee.load_full_mut()?;
|
|
||||||
let mut liqee_health_cache = {
|
|
||||||
let account_retriever = ScanningAccountRetriever::new(ctx.remaining_accounts, group_pk)?;
|
|
||||||
new_health_cache(&liqee.borrow(), &account_retriever)
|
|
||||||
.context("create liqee health cache")?
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check if liqee is bankrupt
|
|
||||||
require!(
|
|
||||||
!liqee_health_cache.has_liquidatable_assets(),
|
|
||||||
MangoError::IsNotBankrupt
|
|
||||||
);
|
|
||||||
liqee.fixed.set_being_liquidated(true);
|
|
||||||
|
|
||||||
// Find bankrupt liab amount
|
|
||||||
let mut perp_market = ctx.accounts.perp_market.load_mut()?;
|
|
||||||
let settle_token_index = perp_market.settle_token_index;
|
|
||||||
let liqee_perp_position = liqee.perp_position_mut(perp_market.perp_market_index)?;
|
|
||||||
require_msg!(
|
|
||||||
liqee_perp_position.base_position_lots() == 0,
|
|
||||||
"liqee must have zero base position"
|
|
||||||
);
|
|
||||||
require!(
|
|
||||||
!liqee_perp_position.has_open_orders(),
|
|
||||||
MangoError::HasOpenPerpOrders
|
|
||||||
);
|
|
||||||
|
|
||||||
let liqee_pnl = liqee_perp_position.quote_position_native();
|
|
||||||
require_msg!(
|
|
||||||
liqee_pnl.is_negative(),
|
|
||||||
"liqee pnl must be negative, was {}",
|
|
||||||
liqee_pnl
|
|
||||||
);
|
|
||||||
let liab_transfer = (-liqee_pnl).min(I80F48::from(max_liab_transfer));
|
|
||||||
|
|
||||||
// Preparation for covering it with the insurance fund
|
|
||||||
let insurance_vault_amount = if perp_market.elligible_for_group_insurance_fund() {
|
|
||||||
ctx.accounts.insurance_vault.amount
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
};
|
|
||||||
|
|
||||||
let liquidation_fee_factor = cm!(I80F48::ONE + perp_market.liquidation_fee);
|
|
||||||
|
|
||||||
let insurance_transfer = cm!(liab_transfer * liquidation_fee_factor)
|
|
||||||
.checked_ceil()
|
|
||||||
.unwrap()
|
|
||||||
.checked_to_num::<u64>()
|
|
||||||
.unwrap()
|
|
||||||
.min(insurance_vault_amount);
|
|
||||||
|
|
||||||
let insurance_transfer_i80f48 = I80F48::from(insurance_transfer);
|
|
||||||
let insurance_fund_exhausted = insurance_transfer == insurance_vault_amount;
|
|
||||||
let insurance_liab_transfer =
|
|
||||||
cm!(insurance_transfer_i80f48 / liquidation_fee_factor).min(liab_transfer);
|
|
||||||
|
|
||||||
// Try using the insurance fund if possible
|
|
||||||
if insurance_transfer > 0 {
|
|
||||||
let mut settle_bank = ctx.accounts.settle_bank.load_mut()?;
|
|
||||||
require_eq!(settle_bank.token_index, settle_token_index);
|
|
||||||
require_keys_eq!(settle_bank.mint, ctx.accounts.insurance_vault.mint);
|
|
||||||
|
|
||||||
// move insurance assets into quote bank
|
|
||||||
let group_seeds = group_seeds!(group);
|
|
||||||
token::transfer(
|
|
||||||
ctx.accounts.transfer_ctx().with_signer(&[group_seeds]),
|
|
||||||
insurance_transfer,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// credit the liqor with quote tokens
|
|
||||||
let (liqor_quote, _, _) = liqor.ensure_token_position(settle_token_index)?;
|
|
||||||
settle_bank.deposit(
|
|
||||||
liqor_quote,
|
|
||||||
insurance_transfer_i80f48,
|
|
||||||
Clock::get()?.unix_timestamp.try_into().unwrap(),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
emit!(TokenBalanceLog {
|
|
||||||
mango_group: ctx.accounts.group.key(),
|
|
||||||
mango_account: ctx.accounts.liqor.key(),
|
|
||||||
token_index: settle_token_index,
|
|
||||||
indexed_position: liqor_quote.indexed_position.to_bits(),
|
|
||||||
deposit_index: settle_bank.deposit_index.to_bits(),
|
|
||||||
borrow_index: settle_bank.borrow_index.to_bits(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// transfer perp quote loss from the liqee to the liqor
|
|
||||||
let liqor_perp_position = liqor
|
|
||||||
.ensure_perp_position(perp_market.perp_market_index, settle_token_index)?
|
|
||||||
.0;
|
|
||||||
liqee_perp_position.record_settle(-insurance_liab_transfer);
|
|
||||||
liqor_perp_position.record_liquidation_quote_change(-insurance_liab_transfer);
|
|
||||||
|
|
||||||
emit_perp_balances(
|
|
||||||
ctx.accounts.group.key(),
|
|
||||||
ctx.accounts.liqor.key(),
|
|
||||||
perp_market.perp_market_index,
|
|
||||||
liqor_perp_position,
|
|
||||||
&perp_market,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Socialize loss if the insurance fund is exhausted
|
|
||||||
let remaining_liab = liab_transfer - insurance_liab_transfer;
|
|
||||||
let mut socialized_loss = I80F48::ZERO;
|
|
||||||
if insurance_fund_exhausted && remaining_liab.is_positive() {
|
|
||||||
perp_market.socialize_loss(-remaining_liab)?;
|
|
||||||
liqee_perp_position.record_settle(-remaining_liab);
|
|
||||||
require_eq!(liqee_perp_position.quote_position_native(), 0);
|
|
||||||
socialized_loss = remaining_liab;
|
|
||||||
}
|
|
||||||
|
|
||||||
emit_perp_balances(
|
|
||||||
ctx.accounts.group.key(),
|
|
||||||
ctx.accounts.liqee.key(),
|
|
||||||
perp_market.perp_market_index,
|
|
||||||
liqee_perp_position,
|
|
||||||
&perp_market,
|
|
||||||
);
|
|
||||||
|
|
||||||
emit!(PerpLiqBankruptcyLog {
|
|
||||||
mango_group: ctx.accounts.group.key(),
|
|
||||||
liqee: ctx.accounts.liqee.key(),
|
|
||||||
liqor: ctx.accounts.liqor.key(),
|
|
||||||
perp_market_index: perp_market.perp_market_index,
|
|
||||||
insurance_transfer: insurance_transfer_i80f48.to_bits(),
|
|
||||||
socialized_loss: socialized_loss.to_bits()
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check liqee health again
|
|
||||||
liqee_health_cache.recompute_perp_info(liqee_perp_position, &perp_market)?;
|
|
||||||
let liqee_init_health = liqee_health_cache.health(HealthType::Init);
|
|
||||||
liqee
|
|
||||||
.fixed
|
|
||||||
.maybe_recover_from_being_liquidated(liqee_init_health);
|
|
||||||
|
|
||||||
drop(perp_market);
|
|
||||||
|
|
||||||
// Check liqor's health
|
|
||||||
if !liqor.fixed.is_in_health_region() {
|
|
||||||
let account_retriever = ScanningAccountRetriever::new(ctx.remaining_accounts, group_pk)?;
|
|
||||||
let liqor_health = compute_health(&liqor.borrow(), HealthType::Init, &account_retriever)
|
|
||||||
.context("compute liqor health")?;
|
|
||||||
require!(liqor_health >= 0, MangoError::HealthMustBePositive);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
|
@ -45,7 +45,11 @@ pub fn perp_liq_base_position(
|
||||||
.is_owner_or_delegate(ctx.accounts.liqor_owner.key()),
|
.is_owner_or_delegate(ctx.accounts.liqor_owner.key()),
|
||||||
MangoError::SomeError
|
MangoError::SomeError
|
||||||
);
|
);
|
||||||
require!(!liqor.fixed.being_liquidated(), MangoError::BeingLiquidated);
|
require_msg_typed!(
|
||||||
|
!liqor.fixed.being_liquidated(),
|
||||||
|
MangoError::BeingLiquidated,
|
||||||
|
"liqor account"
|
||||||
|
);
|
||||||
|
|
||||||
let mut liqee = ctx.accounts.liqee.load_full_mut()?;
|
let mut liqee = ctx.accounts.liqee.load_full_mut()?;
|
||||||
|
|
||||||
|
@ -57,6 +61,7 @@ pub fn perp_liq_base_position(
|
||||||
.context("create liqee health cache")?
|
.context("create liqee health cache")?
|
||||||
};
|
};
|
||||||
let liqee_init_health = liqee_health_cache.health(HealthType::Init);
|
let liqee_init_health = liqee_health_cache.health(HealthType::Init);
|
||||||
|
liqee_health_cache.require_after_phase1_liquidation()?;
|
||||||
|
|
||||||
// Once maint_health falls below 0, we want to start liquidating,
|
// Once maint_health falls below 0, we want to start liquidating,
|
||||||
// we want to allow liquidation to continue until init_health is positive,
|
// we want to allow liquidation to continue until init_health is positive,
|
||||||
|
@ -96,11 +101,6 @@ pub fn perp_liq_base_position(
|
||||||
.0;
|
.0;
|
||||||
let liqee_base_lots = liqee_perp_position.base_position_lots();
|
let liqee_base_lots = liqee_perp_position.base_position_lots();
|
||||||
|
|
||||||
require!(
|
|
||||||
!liqee_perp_position.has_open_orders(),
|
|
||||||
MangoError::HasOpenPerpOrders
|
|
||||||
);
|
|
||||||
|
|
||||||
// Settle funding
|
// Settle funding
|
||||||
liqee_perp_position.settle_funding(&perp_market);
|
liqee_perp_position.settle_funding(&perp_market);
|
||||||
liqor_perp_position.settle_funding(&perp_market);
|
liqor_perp_position.settle_funding(&perp_market);
|
||||||
|
@ -173,7 +173,6 @@ pub fn perp_liq_base_position(
|
||||||
emit_perp_balances(
|
emit_perp_balances(
|
||||||
ctx.accounts.group.key(),
|
ctx.accounts.group.key(),
|
||||||
ctx.accounts.liqor.key(),
|
ctx.accounts.liqor.key(),
|
||||||
perp_market.perp_market_index,
|
|
||||||
liqor_perp_position,
|
liqor_perp_position,
|
||||||
&perp_market,
|
&perp_market,
|
||||||
);
|
);
|
||||||
|
@ -181,7 +180,6 @@ pub fn perp_liq_base_position(
|
||||||
emit_perp_balances(
|
emit_perp_balances(
|
||||||
ctx.accounts.group.key(),
|
ctx.accounts.group.key(),
|
||||||
ctx.accounts.liqee.key(),
|
ctx.accounts.liqee.key(),
|
||||||
perp_market.perp_market_index,
|
|
||||||
liqee_perp_position,
|
liqee_perp_position,
|
||||||
&perp_market,
|
&perp_market,
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,380 @@
|
||||||
|
use anchor_lang::prelude::*;
|
||||||
|
use anchor_spl::token;
|
||||||
|
use anchor_spl::token::Token;
|
||||||
|
use anchor_spl::token::TokenAccount;
|
||||||
|
use checked_math as cm;
|
||||||
|
use fixed::types::I80F48;
|
||||||
|
|
||||||
|
use crate::accounts_zerocopy::*;
|
||||||
|
use crate::error::*;
|
||||||
|
use crate::health::{compute_health, new_health_cache, HealthType, ScanningAccountRetriever};
|
||||||
|
use crate::logs::{
|
||||||
|
emit_perp_balances, PerpLiqBankruptcyLog, PerpLiqQuoteAndBankruptcyLog, TokenBalanceLog,
|
||||||
|
};
|
||||||
|
use crate::state::*;
|
||||||
|
|
||||||
|
#[derive(Accounts)]
|
||||||
|
pub struct PerpLiqQuoteAndBankruptcy<'info> {
|
||||||
|
pub group: AccountLoader<'info, Group>,
|
||||||
|
|
||||||
|
#[account(
|
||||||
|
mut,
|
||||||
|
has_one = group,
|
||||||
|
// liqor_owner is checked at #1
|
||||||
|
)]
|
||||||
|
pub liqor: AccountLoader<'info, MangoAccountFixed>,
|
||||||
|
pub liqor_owner: Signer<'info>,
|
||||||
|
|
||||||
|
// This account MUST have a loss
|
||||||
|
#[account(mut, has_one = group)]
|
||||||
|
pub liqee: AccountLoader<'info, MangoAccountFixed>,
|
||||||
|
|
||||||
|
#[account(mut, has_one = group, has_one = oracle)]
|
||||||
|
pub perp_market: AccountLoader<'info, PerpMarket>,
|
||||||
|
|
||||||
|
/// CHECK: Oracle can have different account types, constrained by address in perp_market
|
||||||
|
pub oracle: UncheckedAccount<'info>,
|
||||||
|
|
||||||
|
// bank correctness is checked at #2
|
||||||
|
#[account(mut, has_one = group)]
|
||||||
|
pub settle_bank: AccountLoader<'info, Bank>,
|
||||||
|
|
||||||
|
#[account(
|
||||||
|
mut,
|
||||||
|
address = settle_bank.load()?.vault
|
||||||
|
)]
|
||||||
|
pub settle_vault: Account<'info, TokenAccount>,
|
||||||
|
|
||||||
|
/// CHECK: Oracle can have different account types
|
||||||
|
#[account(address = settle_bank.load()?.oracle)]
|
||||||
|
pub settle_oracle: UncheckedAccount<'info>,
|
||||||
|
|
||||||
|
// future: this would be an insurance fund vault specific to a
|
||||||
|
// trustless token, separate from the shared one on the group
|
||||||
|
#[account(mut)]
|
||||||
|
pub insurance_vault: Account<'info, TokenAccount>,
|
||||||
|
|
||||||
|
pub token_program: Program<'info, Token>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'info> PerpLiqQuoteAndBankruptcy<'info> {
|
||||||
|
pub fn transfer_ctx(&self) -> CpiContext<'_, '_, '_, 'info, token::Transfer<'info>> {
|
||||||
|
let program = self.token_program.to_account_info();
|
||||||
|
let accounts = token::Transfer {
|
||||||
|
from: self.insurance_vault.to_account_info(),
|
||||||
|
to: self.settle_vault.to_account_info(),
|
||||||
|
authority: self.group.to_account_info(),
|
||||||
|
};
|
||||||
|
CpiContext::new(program, accounts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn perp_liq_quote_and_bankruptcy(
|
||||||
|
ctx: Context<PerpLiqQuoteAndBankruptcy>,
|
||||||
|
max_liab_transfer: u64,
|
||||||
|
) -> Result<()> {
|
||||||
|
let mango_group = ctx.accounts.group.key();
|
||||||
|
|
||||||
|
// Cannot settle with yourself
|
||||||
|
require!(
|
||||||
|
ctx.accounts.liqor.key() != ctx.accounts.liqee.key(),
|
||||||
|
MangoError::SomeError
|
||||||
|
);
|
||||||
|
|
||||||
|
let (perp_market_index, settle_token_index) = {
|
||||||
|
let perp_market = ctx.accounts.perp_market.load()?;
|
||||||
|
(
|
||||||
|
perp_market.perp_market_index,
|
||||||
|
perp_market.settle_token_index,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut liqee = ctx.accounts.liqee.load_full_mut()?;
|
||||||
|
let mut liqor = ctx.accounts.liqor.load_full_mut()?;
|
||||||
|
// account constraint #1
|
||||||
|
require!(
|
||||||
|
liqor
|
||||||
|
.fixed
|
||||||
|
.is_owner_or_delegate(ctx.accounts.liqor_owner.key()),
|
||||||
|
MangoError::SomeError
|
||||||
|
);
|
||||||
|
require_msg_typed!(
|
||||||
|
!liqor.fixed.being_liquidated(),
|
||||||
|
MangoError::BeingLiquidated,
|
||||||
|
"liqor account"
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut liqee_health_cache = {
|
||||||
|
let retriever = ScanningAccountRetriever::new(ctx.remaining_accounts, &mango_group)
|
||||||
|
.context("create account retriever")?;
|
||||||
|
new_health_cache(&liqee.borrow(), &retriever)?
|
||||||
|
};
|
||||||
|
let liqee_init_health = liqee_health_cache.health(HealthType::Init);
|
||||||
|
let liqee_settle_health = liqee_health_cache.perp_settle_health();
|
||||||
|
liqee_health_cache.require_after_phase2_liquidation()?;
|
||||||
|
|
||||||
|
// Once maint_health falls below 0, we want to start liquidating,
|
||||||
|
// we want to allow liquidation to continue until init_health is positive,
|
||||||
|
// to prevent constant oscillation between the two states
|
||||||
|
if liqee.being_liquidated() {
|
||||||
|
if liqee
|
||||||
|
.fixed
|
||||||
|
.maybe_recover_from_being_liquidated(liqee_init_health)
|
||||||
|
{
|
||||||
|
msg!("Liqee init_health above zero");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let maint_health = liqee_health_cache.health(HealthType::Maint);
|
||||||
|
require!(
|
||||||
|
maint_health < I80F48::ZERO,
|
||||||
|
MangoError::HealthMustBeNegative
|
||||||
|
);
|
||||||
|
liqee.fixed.set_being_liquidated(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// check positions exist/create them, done early for nicer error messages
|
||||||
|
{
|
||||||
|
liqee.perp_position(perp_market_index)?;
|
||||||
|
liqee.token_position(settle_token_index)?;
|
||||||
|
liqor.ensure_perp_position(perp_market_index, settle_token_index)?;
|
||||||
|
liqor.ensure_token_position(settle_token_index)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut settle_bank = ctx.accounts.settle_bank.load_mut()?;
|
||||||
|
// account constraint #2
|
||||||
|
require!(
|
||||||
|
settle_bank.token_index == settle_token_index,
|
||||||
|
MangoError::InvalidBank
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get oracle price for market. Price is validated inside
|
||||||
|
let mut perp_market = ctx.accounts.perp_market.load_mut()?;
|
||||||
|
let oracle_price = perp_market.oracle_price(
|
||||||
|
&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?,
|
||||||
|
None, // staleness checked in health
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
|
||||||
|
|
||||||
|
//
|
||||||
|
// Step 1: Allow the liqor to take over ("settle") negative liqee pnl.
|
||||||
|
//
|
||||||
|
// The only limitation is the liqee's perp_settle_health and its perp pnl settle limit.
|
||||||
|
//
|
||||||
|
let settlement;
|
||||||
|
let max_settlement_liqee;
|
||||||
|
{
|
||||||
|
let liqee_perp_position = liqee.perp_position_mut(perp_market_index)?;
|
||||||
|
let liqor_perp_position = liqor.perp_position_mut(perp_market_index)?;
|
||||||
|
liqee_perp_position.settle_funding(&perp_market);
|
||||||
|
liqor_perp_position.settle_funding(&perp_market);
|
||||||
|
|
||||||
|
let liqee_pnl = liqee_perp_position.pnl_for_price(&perp_market, oracle_price)?;
|
||||||
|
// TODO: deal with positive liqee pnl! Maybe another instruction?
|
||||||
|
require!(liqee_pnl < 0, MangoError::ProfitabilityMismatch);
|
||||||
|
|
||||||
|
// Get settleable pnl on the liqee
|
||||||
|
liqee_perp_position.update_settle_limit(&perp_market, now_ts);
|
||||||
|
let liqee_settleable_pnl =
|
||||||
|
liqee_perp_position.apply_pnl_settle_limit(&perp_market, liqee_pnl);
|
||||||
|
|
||||||
|
max_settlement_liqee = liqee_settle_health
|
||||||
|
.min(-liqee_settleable_pnl)
|
||||||
|
.max(I80F48::ZERO);
|
||||||
|
settlement = max_settlement_liqee
|
||||||
|
.min(I80F48::from(max_liab_transfer))
|
||||||
|
.max(I80F48::ZERO);
|
||||||
|
if settlement > 0 {
|
||||||
|
liqor_perp_position.record_liquidation_quote_change(-settlement);
|
||||||
|
liqee_perp_position.record_settle(-settlement);
|
||||||
|
|
||||||
|
// Update the accounts' perp_spot_transfer statistics.
|
||||||
|
let settlement_i64 = settlement.round_to_zero().checked_to_num::<i64>().unwrap();
|
||||||
|
cm!(liqor_perp_position.perp_spot_transfers += settlement_i64);
|
||||||
|
cm!(liqee_perp_position.perp_spot_transfers -= settlement_i64);
|
||||||
|
cm!(liqor.fixed.perp_spot_transfers += settlement_i64);
|
||||||
|
cm!(liqee.fixed.perp_spot_transfers -= settlement_i64);
|
||||||
|
|
||||||
|
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
|
||||||
|
|
||||||
|
// Transfer token balance
|
||||||
|
let liqor_token_position = liqor.token_position_mut(settle_token_index)?.0;
|
||||||
|
let liqee_token_position = liqee.token_position_mut(settle_token_index)?.0;
|
||||||
|
settle_bank.deposit(liqor_token_position, settlement, now_ts)?;
|
||||||
|
settle_bank.withdraw_without_fee(
|
||||||
|
liqee_token_position,
|
||||||
|
settlement,
|
||||||
|
now_ts,
|
||||||
|
oracle_price,
|
||||||
|
)?;
|
||||||
|
liqee_health_cache.adjust_token_balance(&settle_bank, -settlement)?;
|
||||||
|
|
||||||
|
emit!(PerpLiqQuoteAndBankruptcyLog {
|
||||||
|
mango_group,
|
||||||
|
liqee: ctx.accounts.liqee.key(),
|
||||||
|
liqor: ctx.accounts.liqor.key(),
|
||||||
|
perp_market_index: perp_market_index,
|
||||||
|
settlement: settlement.to_bits(),
|
||||||
|
});
|
||||||
|
|
||||||
|
msg!("liquidated pnl = {}", settlement);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let max_liab_transfer = cm!(I80F48::from(max_liab_transfer) - settlement);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Step 2: bankruptcy
|
||||||
|
//
|
||||||
|
// Remaining pnl that brings the account into negative init health is either:
|
||||||
|
// - taken by the liqor in exchange for spot from the insurance fund, or
|
||||||
|
// - wiped away and socialized among all perp participants
|
||||||
|
//
|
||||||
|
let insurance_transfer = if settlement == max_settlement_liqee {
|
||||||
|
let liqee_perp_position = liqee.perp_position_mut(perp_market_index)?;
|
||||||
|
let liqee_pnl = liqee_perp_position.pnl_for_price(&perp_market, oracle_price)?;
|
||||||
|
|
||||||
|
let max_liab_transfer_from_liqee = (-liqee_pnl).min(-liqee_init_health).max(I80F48::ZERO);
|
||||||
|
let liab_transfer = max_liab_transfer_from_liqee
|
||||||
|
.min(max_liab_transfer)
|
||||||
|
.max(I80F48::ZERO);
|
||||||
|
|
||||||
|
// Available insurance fund coverage
|
||||||
|
let insurance_vault_amount = if perp_market.elligible_for_group_insurance_fund() {
|
||||||
|
ctx.accounts.insurance_vault.amount
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
|
||||||
|
let liquidation_fee_factor = cm!(I80F48::ONE + perp_market.liquidation_fee);
|
||||||
|
|
||||||
|
// Amount given to the liqor from the insurance fund
|
||||||
|
let insurance_transfer = cm!(liab_transfer * liquidation_fee_factor)
|
||||||
|
.checked_ceil()
|
||||||
|
.unwrap()
|
||||||
|
.checked_to_num::<u64>()
|
||||||
|
.unwrap()
|
||||||
|
.min(insurance_vault_amount);
|
||||||
|
|
||||||
|
let insurance_transfer_i80f48 = I80F48::from(insurance_transfer);
|
||||||
|
let insurance_fund_exhausted = insurance_transfer == insurance_vault_amount;
|
||||||
|
|
||||||
|
// Amount of negative perp pnl transfered to the liqor
|
||||||
|
let insurance_liab_transfer =
|
||||||
|
cm!(insurance_transfer_i80f48 / liquidation_fee_factor).min(liab_transfer);
|
||||||
|
|
||||||
|
// Try using the insurance fund if possible
|
||||||
|
if insurance_transfer > 0 {
|
||||||
|
require_keys_eq!(settle_bank.mint, ctx.accounts.insurance_vault.mint);
|
||||||
|
|
||||||
|
// move insurance assets into quote bank
|
||||||
|
let group = ctx.accounts.group.load()?;
|
||||||
|
let group_seeds = group_seeds!(group);
|
||||||
|
token::transfer(
|
||||||
|
ctx.accounts.transfer_ctx().with_signer(&[group_seeds]),
|
||||||
|
insurance_transfer,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// credit the liqor with quote tokens
|
||||||
|
let (liqor_quote, _, _) = liqor.ensure_token_position(settle_token_index)?;
|
||||||
|
settle_bank.deposit(liqor_quote, insurance_transfer_i80f48, now_ts)?;
|
||||||
|
|
||||||
|
// transfer perp quote loss from the liqee to the liqor
|
||||||
|
let liqor_perp_position = liqor.perp_position_mut(perp_market_index)?;
|
||||||
|
liqee_perp_position.record_settle(-insurance_liab_transfer);
|
||||||
|
liqor_perp_position.record_liquidation_quote_change(-insurance_liab_transfer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Socialize loss if the insurance fund is exhausted
|
||||||
|
// At this point, we don't care about the liqor's requested max_liab_tranfer
|
||||||
|
let remaining_liab = max_liab_transfer_from_liqee - insurance_liab_transfer;
|
||||||
|
let mut socialized_loss = I80F48::ZERO;
|
||||||
|
let (starting_long_funding, starting_short_funding) =
|
||||||
|
(perp_market.long_funding, perp_market.short_funding);
|
||||||
|
if insurance_fund_exhausted && remaining_liab > 0 {
|
||||||
|
perp_market.socialize_loss(-remaining_liab)?;
|
||||||
|
liqee_perp_position.record_settle(-remaining_liab);
|
||||||
|
socialized_loss = remaining_liab;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit!(PerpLiqBankruptcyLog {
|
||||||
|
mango_group,
|
||||||
|
liqee: ctx.accounts.liqee.key(),
|
||||||
|
liqor: ctx.accounts.liqor.key(),
|
||||||
|
perp_market_index: perp_market.perp_market_index,
|
||||||
|
insurance_transfer: insurance_transfer_i80f48.to_bits(),
|
||||||
|
socialized_loss: socialized_loss.to_bits(),
|
||||||
|
starting_long_funding: starting_long_funding.to_bits(),
|
||||||
|
starting_short_funding: starting_short_funding.to_bits(),
|
||||||
|
ending_long_funding: perp_market.long_funding.to_bits(),
|
||||||
|
ending_short_funding: perp_market.short_funding.to_bits(),
|
||||||
|
});
|
||||||
|
|
||||||
|
insurance_transfer
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Log positions aftewards
|
||||||
|
//
|
||||||
|
if settlement > 0 || insurance_transfer > 0 {
|
||||||
|
let liqor_token_position = liqor.token_position(settle_token_index)?;
|
||||||
|
emit!(TokenBalanceLog {
|
||||||
|
mango_group,
|
||||||
|
mango_account: ctx.accounts.liqor.key(),
|
||||||
|
token_index: settle_token_index,
|
||||||
|
indexed_position: liqor_token_position.indexed_position.to_bits(),
|
||||||
|
deposit_index: settle_bank.deposit_index.to_bits(),
|
||||||
|
borrow_index: settle_bank.borrow_index.to_bits(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if settlement > 0 {
|
||||||
|
let liqee_token_position = liqee.token_position(settle_token_index)?;
|
||||||
|
emit!(TokenBalanceLog {
|
||||||
|
mango_group,
|
||||||
|
mango_account: ctx.accounts.liqee.key(),
|
||||||
|
token_index: settle_token_index,
|
||||||
|
indexed_position: liqee_token_position.indexed_position.to_bits(),
|
||||||
|
deposit_index: settle_bank.deposit_index.to_bits(),
|
||||||
|
borrow_index: settle_bank.borrow_index.to_bits(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let liqee_perp_position = liqee.perp_position(perp_market_index)?;
|
||||||
|
let liqor_perp_position = liqor.perp_position(perp_market_index)?;
|
||||||
|
emit_perp_balances(
|
||||||
|
mango_group,
|
||||||
|
ctx.accounts.liqor.key(),
|
||||||
|
liqor_perp_position,
|
||||||
|
&perp_market,
|
||||||
|
);
|
||||||
|
emit_perp_balances(
|
||||||
|
mango_group,
|
||||||
|
ctx.accounts.liqee.key(),
|
||||||
|
liqee_perp_position,
|
||||||
|
&perp_market,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check liqee health again: bankruptcy would improve health
|
||||||
|
liqee_health_cache.recompute_perp_info(liqee_perp_position, &perp_market)?;
|
||||||
|
let liqee_init_health = liqee_health_cache.health(HealthType::Init);
|
||||||
|
liqee
|
||||||
|
.fixed
|
||||||
|
.maybe_recover_from_being_liquidated(liqee_init_health);
|
||||||
|
|
||||||
|
drop(perp_market);
|
||||||
|
drop(settle_bank);
|
||||||
|
|
||||||
|
// Check liqor's health
|
||||||
|
if !liqor.fixed.is_in_health_region() {
|
||||||
|
let account_retriever =
|
||||||
|
ScanningAccountRetriever::new(ctx.remaining_accounts, &mango_group)?;
|
||||||
|
let liqor_health = compute_health(&liqor.borrow(), HealthType::Init, &account_retriever)
|
||||||
|
.context("compute liqor health")?;
|
||||||
|
require!(liqor_health >= 0, MangoError::HealthMustBePositive);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -91,7 +91,6 @@ pub fn perp_settle_fees(ctx: Context<PerpSettleFees>, max_settle_amount: u64) ->
|
||||||
emit_perp_balances(
|
emit_perp_balances(
|
||||||
ctx.accounts.group.key(),
|
ctx.accounts.group.key(),
|
||||||
ctx.accounts.account.key(),
|
ctx.accounts.account.key(),
|
||||||
perp_market.perp_market_index,
|
|
||||||
perp_position,
|
perp_position,
|
||||||
&perp_market,
|
&perp_market,
|
||||||
);
|
);
|
||||||
|
|
|
@ -81,19 +81,12 @@ pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>) -> Result<()> {
|
||||||
a_maint_health = a_cache.health(HealthType::Maint);
|
a_maint_health = a_cache.health(HealthType::Maint);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Account B is the one that must have negative pnl. Check how much of that may be actualized
|
let mut settle_bank = ctx.accounts.settle_bank.load_mut()?;
|
||||||
// given the account's health. In that, we only care about the health of spot assets on the account.
|
|
||||||
// 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_settle_health >= 0, MangoError::HealthMustBePositive);
|
|
||||||
|
|
||||||
let mut bank = ctx.accounts.settle_bank.load_mut()?;
|
|
||||||
let perp_market = ctx.accounts.perp_market.load()?;
|
let perp_market = ctx.accounts.perp_market.load()?;
|
||||||
|
|
||||||
// Verify that the bank is the quote currency bank
|
// Verify that the bank is the quote currency bank
|
||||||
require!(
|
require!(
|
||||||
bank.token_index == settle_token_index,
|
settle_bank.token_index == settle_token_index,
|
||||||
MangoError::InvalidBank
|
MangoError::InvalidBank
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -103,20 +96,16 @@ pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>) -> Result<()> {
|
||||||
None, // staleness checked in health
|
None, // staleness checked in health
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// Fetch perp positions for accounts
|
// Fetch perp position and pnl
|
||||||
let a_perp_position = account_a.perp_position_mut(perp_market_index)?;
|
let a_perp_position = account_a.perp_position_mut(perp_market_index)?;
|
||||||
let b_perp_position = account_b.perp_position_mut(perp_market_index)?;
|
let b_perp_position = account_b.perp_position_mut(perp_market_index)?;
|
||||||
|
|
||||||
// Settle funding before settling any PnL
|
|
||||||
a_perp_position.settle_funding(&perp_market);
|
a_perp_position.settle_funding(&perp_market);
|
||||||
b_perp_position.settle_funding(&perp_market);
|
b_perp_position.settle_funding(&perp_market);
|
||||||
|
|
||||||
// Calculate PnL for each account
|
|
||||||
let a_pnl = a_perp_position.pnl_for_price(&perp_market, oracle_price)?;
|
let a_pnl = a_perp_position.pnl_for_price(&perp_market, oracle_price)?;
|
||||||
let b_pnl = b_perp_position.pnl_for_price(&perp_market, oracle_price)?;
|
let b_pnl = b_perp_position.pnl_for_price(&perp_market, oracle_price)?;
|
||||||
|
|
||||||
// Account A must be profitable, and B must be unprofitable
|
// PnL must have opposite signs for there to be a settlement:
|
||||||
// PnL must be opposite signs for there to be a settlement
|
// Account A must be profitable, and B must be unprofitable.
|
||||||
require_msg_typed!(
|
require_msg_typed!(
|
||||||
a_pnl.is_positive(),
|
a_pnl.is_positive(),
|
||||||
MangoError::ProfitabilityMismatch,
|
MangoError::ProfitabilityMismatch,
|
||||||
|
@ -130,12 +119,11 @@ pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>) -> Result<()> {
|
||||||
b_pnl
|
b_pnl
|
||||||
);
|
);
|
||||||
|
|
||||||
// Cap settlement of unrealized pnl
|
// Apply pnl settle limits
|
||||||
// Settles at most x100% each hour
|
|
||||||
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
|
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
|
||||||
a_perp_position.update_settle_limit(&perp_market, now_ts);
|
a_perp_position.update_settle_limit(&perp_market, now_ts);
|
||||||
b_perp_position.update_settle_limit(&perp_market, now_ts);
|
|
||||||
let a_settleable_pnl = a_perp_position.apply_pnl_settle_limit(&perp_market, a_pnl);
|
let a_settleable_pnl = a_perp_position.apply_pnl_settle_limit(&perp_market, a_pnl);
|
||||||
|
b_perp_position.update_settle_limit(&perp_market, now_ts);
|
||||||
let b_settleable_pnl = b_perp_position.apply_pnl_settle_limit(&perp_market, b_pnl);
|
let b_settleable_pnl = b_perp_position.apply_pnl_settle_limit(&perp_market, b_pnl);
|
||||||
|
|
||||||
require_msg_typed!(
|
require_msg_typed!(
|
||||||
|
@ -153,11 +141,23 @@ pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>) -> Result<()> {
|
||||||
b_pnl
|
b_pnl
|
||||||
);
|
);
|
||||||
|
|
||||||
// Settle for the maximum possible capped to b's settle health
|
// Check how much of account b's negative pnl may be actualized given the health.
|
||||||
|
// In that, we only care about the health of spot assets on the account.
|
||||||
|
// 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 unbacked token-losses and isn't allowed.
|
||||||
|
require_msg_typed!(
|
||||||
|
b_settle_health >= 0,
|
||||||
|
MangoError::HealthMustBePositive,
|
||||||
|
"account b settle health is negative: {}",
|
||||||
|
b_settle_health
|
||||||
|
);
|
||||||
|
|
||||||
|
// Settle for the maximum possible capped to target's settle health
|
||||||
let settlement = a_settleable_pnl
|
let settlement = a_settleable_pnl
|
||||||
.abs()
|
.min(-b_settleable_pnl)
|
||||||
.min(b_settleable_pnl.abs())
|
.min(b_settle_health)
|
||||||
.min(b_settle_health);
|
.max(I80F48::ZERO);
|
||||||
require_msg_typed!(
|
require_msg_typed!(
|
||||||
settlement >= 0,
|
settlement >= 0,
|
||||||
MangoError::SettlementAmountMustBePositive,
|
MangoError::SettlementAmountMustBePositive,
|
||||||
|
@ -167,54 +167,28 @@ pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>) -> Result<()> {
|
||||||
b_settle_health,
|
b_settle_health,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Settle
|
let fee = compute_settle_fee(&perp_market, a_init_health, a_maint_health, settlement)?;
|
||||||
|
|
||||||
a_perp_position.record_settle(settlement);
|
a_perp_position.record_settle(settlement);
|
||||||
b_perp_position.record_settle(-settlement);
|
b_perp_position.record_settle(-settlement);
|
||||||
|
|
||||||
emit_perp_balances(
|
emit_perp_balances(
|
||||||
ctx.accounts.group.key(),
|
ctx.accounts.group.key(),
|
||||||
ctx.accounts.account_a.key(),
|
ctx.accounts.account_a.key(),
|
||||||
perp_market.perp_market_index,
|
|
||||||
a_perp_position,
|
a_perp_position,
|
||||||
&perp_market,
|
&perp_market,
|
||||||
);
|
);
|
||||||
|
|
||||||
emit_perp_balances(
|
emit_perp_balances(
|
||||||
ctx.accounts.group.key(),
|
ctx.accounts.group.key(),
|
||||||
ctx.accounts.account_b.key(),
|
ctx.accounts.account_b.key(),
|
||||||
perp_market.perp_market_index,
|
|
||||||
b_perp_position,
|
b_perp_position,
|
||||||
&perp_market,
|
&perp_market,
|
||||||
);
|
);
|
||||||
|
|
||||||
// A percentage fee is paid to the settler when account_a's health is low.
|
// Update the accounts' perp_spot_transfer statistics.
|
||||||
// That's because the settlement could avoid it getting liquidated.
|
//
|
||||||
let low_health_fee = if a_init_health < 0 {
|
|
||||||
let fee_fraction = I80F48::from_num(perp_market.settle_fee_fraction_low_health);
|
|
||||||
if a_maint_health < 0 {
|
|
||||||
cm!(settlement * fee_fraction)
|
|
||||||
} else {
|
|
||||||
cm!(settlement * fee_fraction * (-a_init_health / (a_maint_health - a_init_health)))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
I80F48::ZERO
|
|
||||||
};
|
|
||||||
|
|
||||||
// The settler receives a flat fee
|
|
||||||
let flat_fee = I80F48::from_num(perp_market.settle_fee_flat);
|
|
||||||
|
|
||||||
// Fees only apply when the settlement is large enough
|
|
||||||
let fee = if settlement >= perp_market.settle_fee_amount_threshold {
|
|
||||||
cm!(low_health_fee + flat_fee).min(settlement)
|
|
||||||
} else {
|
|
||||||
I80F48::ZERO
|
|
||||||
};
|
|
||||||
|
|
||||||
// Safety check to prevent any accidental negative transfer
|
|
||||||
require!(fee >= 0, MangoError::SettlementAmountMustBePositive);
|
|
||||||
|
|
||||||
// Update the account's net_settled with the new PnL.
|
|
||||||
// Applying the fee here means that it decreases the displayed perp pnl.
|
// Applying the fee here means that it decreases the displayed perp pnl.
|
||||||
|
// Think about it like this: a's pnl reduces by `settlement` and spot increases by `settlement - fee`.
|
||||||
|
// That means that it managed to extract `settlement - fee` from perp interactions.
|
||||||
let settlement_i64 = settlement.round_to_zero().checked_to_num::<i64>().unwrap();
|
let settlement_i64 = settlement.round_to_zero().checked_to_num::<i64>().unwrap();
|
||||||
let fee_i64 = fee.round_to_zero().checked_to_num::<i64>().unwrap();
|
let fee_i64 = fee.round_to_zero().checked_to_num::<i64>().unwrap();
|
||||||
cm!(a_perp_position.perp_spot_transfers += settlement_i64 - fee_i64);
|
cm!(a_perp_position.perp_spot_transfers += settlement_i64 - fee_i64);
|
||||||
|
@ -222,34 +196,32 @@ pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>) -> Result<()> {
|
||||||
cm!(account_a.fixed.perp_spot_transfers += settlement_i64 - fee_i64);
|
cm!(account_a.fixed.perp_spot_transfers += settlement_i64 - fee_i64);
|
||||||
cm!(account_b.fixed.perp_spot_transfers -= settlement_i64);
|
cm!(account_b.fixed.perp_spot_transfers -= settlement_i64);
|
||||||
|
|
||||||
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
|
|
||||||
|
|
||||||
// Transfer token balances
|
// Transfer token balances
|
||||||
// The fee is paid by the account with positive unsettled pnl
|
// The fee is paid by the account with positive unsettled pnl
|
||||||
let a_token_position = account_a.token_position_mut(settle_token_index)?.0;
|
let a_token_position = account_a.token_position_mut(settle_token_index)?.0;
|
||||||
let b_token_position = account_b.token_position_mut(settle_token_index)?.0;
|
let b_token_position = account_b.token_position_mut(settle_token_index)?.0;
|
||||||
bank.deposit(a_token_position, cm!(settlement - fee), now_ts)?;
|
settle_bank.deposit(a_token_position, cm!(settlement - fee), now_ts)?;
|
||||||
// Don't charge loan origination fees on borrows created via settling:
|
// Don't charge loan origination fees on borrows created via settling:
|
||||||
// Even small loan origination fees could accumulate if a perp position is
|
// Even small loan origination fees could accumulate if a perp position is
|
||||||
// settled back and forth repeatedly.
|
// settled back and forth repeatedly.
|
||||||
bank.withdraw_without_fee(b_token_position, settlement, now_ts, oracle_price)?;
|
settle_bank.withdraw_without_fee(b_token_position, settlement, now_ts, oracle_price)?;
|
||||||
|
|
||||||
emit!(TokenBalanceLog {
|
emit!(TokenBalanceLog {
|
||||||
mango_group: ctx.accounts.group.key(),
|
mango_group: ctx.accounts.group.key(),
|
||||||
mango_account: ctx.accounts.settler.key(),
|
mango_account: ctx.accounts.account_a.key(),
|
||||||
token_index: settle_token_index,
|
token_index: settle_token_index,
|
||||||
indexed_position: a_token_position.indexed_position.to_bits(),
|
indexed_position: a_token_position.indexed_position.to_bits(),
|
||||||
deposit_index: bank.deposit_index.to_bits(),
|
deposit_index: settle_bank.deposit_index.to_bits(),
|
||||||
borrow_index: bank.borrow_index.to_bits(),
|
borrow_index: settle_bank.borrow_index.to_bits(),
|
||||||
});
|
});
|
||||||
|
|
||||||
emit!(TokenBalanceLog {
|
emit!(TokenBalanceLog {
|
||||||
mango_group: ctx.accounts.group.key(),
|
mango_group: ctx.accounts.group.key(),
|
||||||
mango_account: ctx.accounts.settler.key(),
|
mango_account: ctx.accounts.account_b.key(),
|
||||||
token_index: settle_token_index,
|
token_index: settle_token_index,
|
||||||
indexed_position: b_token_position.indexed_position.to_bits(),
|
indexed_position: b_token_position.indexed_position.to_bits(),
|
||||||
deposit_index: bank.deposit_index.to_bits(),
|
deposit_index: settle_bank.deposit_index.to_bits(),
|
||||||
borrow_index: bank.borrow_index.to_bits(),
|
borrow_index: settle_bank.borrow_index.to_bits(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// settler might be the same as account a or b
|
// settler might be the same as account a or b
|
||||||
|
@ -267,15 +239,15 @@ pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>) -> Result<()> {
|
||||||
|
|
||||||
let (settler_token_position, settler_token_raw_index, _) =
|
let (settler_token_position, settler_token_raw_index, _) =
|
||||||
settler.ensure_token_position(settle_token_index)?;
|
settler.ensure_token_position(settle_token_index)?;
|
||||||
let settler_token_position_active = bank.deposit(settler_token_position, fee, now_ts)?;
|
let settler_token_position_active = settle_bank.deposit(settler_token_position, fee, now_ts)?;
|
||||||
|
|
||||||
emit!(TokenBalanceLog {
|
emit!(TokenBalanceLog {
|
||||||
mango_group: ctx.accounts.group.key(),
|
mango_group: ctx.accounts.group.key(),
|
||||||
mango_account: ctx.accounts.settler.key(),
|
mango_account: ctx.accounts.settler.key(),
|
||||||
token_index: settler_token_position.token_index,
|
token_index: settler_token_position.token_index,
|
||||||
indexed_position: settler_token_position.indexed_position.to_bits(),
|
indexed_position: settler_token_position.indexed_position.to_bits(),
|
||||||
deposit_index: bank.deposit_index.to_bits(),
|
deposit_index: settle_bank.deposit_index.to_bits(),
|
||||||
borrow_index: bank.borrow_index.to_bits(),
|
borrow_index: settle_bank.borrow_index.to_bits(),
|
||||||
});
|
});
|
||||||
|
|
||||||
if !settler_token_position_active {
|
if !settler_token_position_active {
|
||||||
|
@ -296,3 +268,41 @@ pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>) -> Result<()> {
|
||||||
msg!("settled pnl = {}, fee = {}", settlement, fee);
|
msg!("settled pnl = {}, fee = {}", settlement, fee);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn compute_settle_fee(
|
||||||
|
perp_market: &PerpMarket,
|
||||||
|
source_init_health: I80F48,
|
||||||
|
source_maint_health: I80F48,
|
||||||
|
settlement: I80F48,
|
||||||
|
) -> Result<I80F48> {
|
||||||
|
// A percentage fee is paid to the settler when the source account's health is low.
|
||||||
|
// That's because the settlement could avoid it getting liquidated: settling will
|
||||||
|
// increase its health by actualizing positive perp pnl.
|
||||||
|
let low_health_fee = if source_init_health < 0 {
|
||||||
|
let fee_fraction = I80F48::from_num(perp_market.settle_fee_fraction_low_health);
|
||||||
|
if source_maint_health < 0 {
|
||||||
|
cm!(settlement * fee_fraction)
|
||||||
|
} else {
|
||||||
|
cm!(settlement
|
||||||
|
* fee_fraction
|
||||||
|
* (-source_init_health / (source_maint_health - source_init_health)))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
I80F48::ZERO
|
||||||
|
};
|
||||||
|
|
||||||
|
// The settler receives a flat fee
|
||||||
|
let flat_fee = I80F48::from_num(perp_market.settle_fee_flat);
|
||||||
|
|
||||||
|
// Fees only apply when the settlement is large enough
|
||||||
|
let fee = if settlement >= perp_market.settle_fee_amount_threshold {
|
||||||
|
cm!(low_health_fee + flat_fee).min(settlement)
|
||||||
|
} else {
|
||||||
|
I80F48::ZERO
|
||||||
|
};
|
||||||
|
|
||||||
|
// Safety check to prevent any accidental negative transfer
|
||||||
|
require!(fee >= 0, MangoError::SettlementAmountMustBePositive);
|
||||||
|
|
||||||
|
Ok(fee)
|
||||||
|
}
|
||||||
|
|
|
@ -89,17 +89,18 @@ pub fn token_liq_bankruptcy(
|
||||||
.is_owner_or_delegate(ctx.accounts.liqor_owner.key()),
|
.is_owner_or_delegate(ctx.accounts.liqor_owner.key()),
|
||||||
MangoError::SomeError
|
MangoError::SomeError
|
||||||
);
|
);
|
||||||
require!(!liqor.fixed.being_liquidated(), MangoError::BeingLiquidated);
|
require_msg_typed!(
|
||||||
|
!liqor.fixed.being_liquidated(),
|
||||||
|
MangoError::BeingLiquidated,
|
||||||
|
"liqor account"
|
||||||
|
);
|
||||||
|
|
||||||
let mut account_retriever = ScanningAccountRetriever::new(health_ais, group_pk)?;
|
let mut account_retriever = ScanningAccountRetriever::new(health_ais, group_pk)?;
|
||||||
|
|
||||||
let mut liqee = ctx.accounts.liqee.load_full_mut()?;
|
let mut liqee = ctx.accounts.liqee.load_full_mut()?;
|
||||||
let mut liqee_health_cache = new_health_cache(&liqee.borrow(), &account_retriever)
|
let mut liqee_health_cache = new_health_cache(&liqee.borrow(), &account_retriever)
|
||||||
.context("create liqee health cache")?;
|
.context("create liqee health cache")?;
|
||||||
require!(
|
liqee_health_cache.require_after_phase2_liquidation()?;
|
||||||
!liqee_health_cache.has_liquidatable_assets(),
|
|
||||||
MangoError::IsNotBankrupt
|
|
||||||
);
|
|
||||||
liqee.fixed.set_being_liquidated(true);
|
liqee.fixed.set_being_liquidated(true);
|
||||||
|
|
||||||
let (liab_bank, liab_price, opt_quote_bank_and_price) =
|
let (liab_bank, liab_price, opt_quote_bank_and_price) =
|
||||||
|
@ -244,6 +245,7 @@ pub fn token_liq_bankruptcy(
|
||||||
// Socialize loss if there's more loss and noone else could use the
|
// Socialize loss if there's more loss and noone else could use the
|
||||||
// insurance fund to cover it.
|
// insurance fund to cover it.
|
||||||
let mut socialized_loss = I80F48::ZERO;
|
let mut socialized_loss = I80F48::ZERO;
|
||||||
|
let starting_deposit_index = liab_deposit_index;
|
||||||
if insurance_fund_exhausted && remaining_liab_loss.is_positive() {
|
if insurance_fund_exhausted && remaining_liab_loss.is_positive() {
|
||||||
// find the total deposits
|
// find the total deposits
|
||||||
let mut indexed_total_deposits = I80F48::ZERO;
|
let mut indexed_total_deposits = I80F48::ZERO;
|
||||||
|
@ -318,7 +320,9 @@ pub fn token_liq_bankruptcy(
|
||||||
liab_price: liab_price.to_bits(),
|
liab_price: liab_price.to_bits(),
|
||||||
insurance_token_index: QUOTE_TOKEN_INDEX,
|
insurance_token_index: QUOTE_TOKEN_INDEX,
|
||||||
insurance_transfer: insurance_transfer_i80f48.to_bits(),
|
insurance_transfer: insurance_transfer_i80f48.to_bits(),
|
||||||
socialized_loss: socialized_loss.to_bits()
|
socialized_loss: socialized_loss.to_bits(),
|
||||||
|
starting_liab_deposit_index: starting_deposit_index.to_bits(),
|
||||||
|
ending_liab_deposit_index: liab_deposit_index.to_bits()
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -50,7 +50,11 @@ pub fn token_liq_with_token(
|
||||||
.is_owner_or_delegate(ctx.accounts.liqor_owner.key()),
|
.is_owner_or_delegate(ctx.accounts.liqor_owner.key()),
|
||||||
MangoError::SomeError
|
MangoError::SomeError
|
||||||
);
|
);
|
||||||
require!(!liqor.fixed.being_liquidated(), MangoError::BeingLiquidated);
|
require_msg_typed!(
|
||||||
|
!liqor.fixed.being_liquidated(),
|
||||||
|
MangoError::BeingLiquidated,
|
||||||
|
"liqor account"
|
||||||
|
);
|
||||||
|
|
||||||
let mut liqee = ctx.accounts.liqee.load_full_mut()?;
|
let mut liqee = ctx.accounts.liqee.load_full_mut()?;
|
||||||
|
|
||||||
|
@ -58,6 +62,7 @@ pub fn token_liq_with_token(
|
||||||
let mut liqee_health_cache = new_health_cache(&liqee.borrow(), &account_retriever)
|
let mut liqee_health_cache = new_health_cache(&liqee.borrow(), &account_retriever)
|
||||||
.context("create liqee health cache")?;
|
.context("create liqee health cache")?;
|
||||||
let init_health = liqee_health_cache.health(HealthType::Init);
|
let init_health = liqee_health_cache.health(HealthType::Init);
|
||||||
|
liqee_health_cache.require_after_phase1_liquidation()?;
|
||||||
|
|
||||||
// Once maint_health falls below 0, we want to start liquidating,
|
// Once maint_health falls below 0, we want to start liquidating,
|
||||||
// we want to allow liquidation to continue until init_health is positive,
|
// we want to allow liquidation to continue until init_health is positive,
|
||||||
|
|
|
@ -720,11 +720,11 @@ pub mod mango_v4 {
|
||||||
instructions::perp_liq_force_cancel_orders(ctx, limit)
|
instructions::perp_liq_force_cancel_orders(ctx, limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn perp_liq_bankruptcy(
|
pub fn perp_liq_quote_and_bankruptcy(
|
||||||
ctx: Context<PerpLiqBankruptcy>,
|
ctx: Context<PerpLiqQuoteAndBankruptcy>,
|
||||||
max_liab_transfer: u64,
|
max_liab_transfer: u64,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
instructions::perp_liq_bankruptcy(ctx, max_liab_transfer)
|
instructions::perp_liq_quote_and_bankruptcy(ctx, max_liab_transfer)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn alt_set(ctx: Context<AltSet>, index: u8) -> Result<()> {
|
pub fn alt_set(ctx: Context<AltSet>, index: u8) -> Result<()> {
|
||||||
|
|
|
@ -8,14 +8,13 @@ use borsh::BorshSerialize;
|
||||||
pub fn emit_perp_balances(
|
pub fn emit_perp_balances(
|
||||||
mango_group: Pubkey,
|
mango_group: Pubkey,
|
||||||
mango_account: Pubkey,
|
mango_account: Pubkey,
|
||||||
market_index: u16,
|
|
||||||
pp: &PerpPosition,
|
pp: &PerpPosition,
|
||||||
pm: &PerpMarket,
|
pm: &PerpMarket,
|
||||||
) {
|
) {
|
||||||
emit!(PerpBalanceLog {
|
emit!(PerpBalanceLog {
|
||||||
mango_group,
|
mango_group,
|
||||||
mango_account,
|
mango_account,
|
||||||
market_index,
|
market_index: pm.perp_market_index,
|
||||||
base_position: pp.base_position_lots(),
|
base_position: pp.base_position_lots(),
|
||||||
quote_position: pp.quote_position_native().to_bits(),
|
quote_position: pp.quote_position_native().to_bits(),
|
||||||
long_settled_funding: pp.long_settled_funding.to_bits(),
|
long_settled_funding: pp.long_settled_funding.to_bits(),
|
||||||
|
@ -212,6 +211,8 @@ pub struct TokenLiqBankruptcyLog {
|
||||||
pub insurance_token_index: u16,
|
pub insurance_token_index: u16,
|
||||||
pub insurance_transfer: i128,
|
pub insurance_transfer: i128,
|
||||||
pub socialized_loss: i128,
|
pub socialized_loss: i128,
|
||||||
|
pub starting_liab_deposit_index: i128,
|
||||||
|
pub ending_liab_deposit_index: i128,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[event]
|
#[event]
|
||||||
|
@ -286,6 +287,19 @@ pub struct PerpLiqBankruptcyLog {
|
||||||
pub perp_market_index: u16,
|
pub perp_market_index: u16,
|
||||||
pub insurance_transfer: i128,
|
pub insurance_transfer: i128,
|
||||||
pub socialized_loss: i128,
|
pub socialized_loss: i128,
|
||||||
|
pub starting_long_funding: i128,
|
||||||
|
pub starting_short_funding: i128,
|
||||||
|
pub ending_long_funding: i128,
|
||||||
|
pub ending_short_funding: i128,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[event]
|
||||||
|
pub struct PerpLiqQuoteAndBankruptcyLog {
|
||||||
|
pub mango_group: Pubkey,
|
||||||
|
pub liqee: Pubkey,
|
||||||
|
pub liqor: Pubkey,
|
||||||
|
pub perp_market_index: u16,
|
||||||
|
pub settlement: i128,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[event]
|
#[event]
|
||||||
|
|
|
@ -269,7 +269,7 @@ async fn derive_liquidation_remaining_account_metas(
|
||||||
|
|
||||||
let perp_markets: Vec<Pubkey> = liqee
|
let perp_markets: Vec<Pubkey> = liqee
|
||||||
.active_perp_positions()
|
.active_perp_positions()
|
||||||
.chain(liqee.active_perp_positions())
|
.chain(liqor.active_perp_positions())
|
||||||
.map(|perp| get_perp_market_address_by_index(liqee.fixed.group, perp.market_index))
|
.map(|perp| get_perp_market_address_by_index(liqee.fixed.group, perp.market_index))
|
||||||
.unique()
|
.unique()
|
||||||
.collect();
|
.collect();
|
||||||
|
@ -339,6 +339,17 @@ pub async fn account_position_f64(solana: &SolanaCookie, account: Pubkey, bank:
|
||||||
native.to_num::<f64>()
|
native.to_num::<f64>()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn account_init_health(solana: &SolanaCookie, account: Pubkey) -> f64 {
|
||||||
|
send_tx(solana, ComputeAccountDataInstruction { account })
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let health_data = solana
|
||||||
|
.program_log_events::<mango_v4::events::MangoAccountData>()
|
||||||
|
.pop()
|
||||||
|
.unwrap();
|
||||||
|
health_data.init_health.to_num::<f64>()
|
||||||
|
}
|
||||||
|
|
||||||
// Verifies that the "post_health: ..." log emitted by the previous instruction
|
// Verifies that the "post_health: ..." log emitted by the previous instruction
|
||||||
// matches the init health of the account.
|
// matches the init health of the account.
|
||||||
pub async fn check_prev_instruction_post_health(solana: &SolanaCookie, account: Pubkey) {
|
pub async fn check_prev_instruction_post_health(solana: &SolanaCookie, account: Pubkey) {
|
||||||
|
@ -1036,6 +1047,96 @@ impl ClientInstruction for TokenDeregisterInstruction {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn token_edit_instruction_default() -> mango_v4::instruction::TokenEdit {
|
||||||
|
mango_v4::instruction::TokenEdit {
|
||||||
|
oracle_opt: None,
|
||||||
|
oracle_config_opt: None,
|
||||||
|
group_insurance_fund_opt: None,
|
||||||
|
interest_rate_params_opt: None,
|
||||||
|
loan_fee_rate_opt: None,
|
||||||
|
loan_origination_fee_rate_opt: None,
|
||||||
|
maint_asset_weight_opt: None,
|
||||||
|
init_asset_weight_opt: None,
|
||||||
|
maint_liab_weight_opt: None,
|
||||||
|
init_liab_weight_opt: None,
|
||||||
|
liquidation_fee_opt: None,
|
||||||
|
stable_price_delay_interval_seconds_opt: None,
|
||||||
|
stable_price_delay_growth_limit_opt: None,
|
||||||
|
stable_price_growth_limit_opt: None,
|
||||||
|
min_vault_to_deposits_ratio_opt: None,
|
||||||
|
net_borrow_limit_per_window_quote_opt: None,
|
||||||
|
net_borrow_limit_window_size_ts_opt: None,
|
||||||
|
borrow_weight_scale_start_quote_opt: None,
|
||||||
|
deposit_weight_scale_start_quote_opt: None,
|
||||||
|
reset_stable_price: false,
|
||||||
|
reset_net_borrow_limit: false,
|
||||||
|
reduce_only_opt: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TokenEditWeights {
|
||||||
|
pub group: Pubkey,
|
||||||
|
pub admin: TestKeypair,
|
||||||
|
pub mint: Pubkey,
|
||||||
|
|
||||||
|
pub maint_asset_weight: f32,
|
||||||
|
pub maint_liab_weight: f32,
|
||||||
|
pub init_asset_weight: f32,
|
||||||
|
pub init_liab_weight: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
impl ClientInstruction for TokenEditWeights {
|
||||||
|
type Accounts = mango_v4::accounts::TokenEdit;
|
||||||
|
type Instruction = mango_v4::instruction::TokenEdit;
|
||||||
|
async fn to_instruction(
|
||||||
|
&self,
|
||||||
|
account_loader: impl ClientAccountLoader + 'async_trait,
|
||||||
|
) -> (Self::Accounts, instruction::Instruction) {
|
||||||
|
let program_id = mango_v4::id();
|
||||||
|
|
||||||
|
let mint_info_key = Pubkey::find_program_address(
|
||||||
|
&[
|
||||||
|
b"MintInfo".as_ref(),
|
||||||
|
self.group.as_ref(),
|
||||||
|
self.mint.as_ref(),
|
||||||
|
],
|
||||||
|
&program_id,
|
||||||
|
)
|
||||||
|
.0;
|
||||||
|
let mint_info: MintInfo = account_loader.load(&mint_info_key).await.unwrap();
|
||||||
|
|
||||||
|
let instruction = Self::Instruction {
|
||||||
|
init_asset_weight_opt: Some(self.init_asset_weight),
|
||||||
|
init_liab_weight_opt: Some(self.init_liab_weight),
|
||||||
|
maint_asset_weight_opt: Some(self.maint_asset_weight),
|
||||||
|
maint_liab_weight_opt: Some(self.maint_liab_weight),
|
||||||
|
..token_edit_instruction_default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let accounts = Self::Accounts {
|
||||||
|
group: self.group,
|
||||||
|
admin: self.admin.pubkey(),
|
||||||
|
mint_info: mint_info_key,
|
||||||
|
oracle: mint_info.oracle,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut instruction = make_instruction(program_id, &accounts, instruction);
|
||||||
|
instruction
|
||||||
|
.accounts
|
||||||
|
.extend(mint_info.banks().iter().map(|&k| AccountMeta {
|
||||||
|
pubkey: k,
|
||||||
|
is_signer: false,
|
||||||
|
is_writable: true,
|
||||||
|
}));
|
||||||
|
(accounts, instruction)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn signers(&self) -> Vec<TestKeypair> {
|
||||||
|
vec![self.admin]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct TokenResetStablePriceModel {
|
pub struct TokenResetStablePriceModel {
|
||||||
pub group: Pubkey,
|
pub group: Pubkey,
|
||||||
pub admin: TestKeypair,
|
pub admin: TestKeypair,
|
||||||
|
@ -1064,28 +1165,9 @@ impl ClientInstruction for TokenResetStablePriceModel {
|
||||||
let mint_info: MintInfo = account_loader.load(&mint_info_key).await.unwrap();
|
let mint_info: MintInfo = account_loader.load(&mint_info_key).await.unwrap();
|
||||||
|
|
||||||
let instruction = Self::Instruction {
|
let instruction = Self::Instruction {
|
||||||
oracle_opt: None,
|
|
||||||
oracle_config_opt: None,
|
|
||||||
group_insurance_fund_opt: None,
|
|
||||||
interest_rate_params_opt: None,
|
|
||||||
loan_fee_rate_opt: None,
|
|
||||||
loan_origination_fee_rate_opt: None,
|
|
||||||
maint_asset_weight_opt: None,
|
|
||||||
init_asset_weight_opt: None,
|
|
||||||
maint_liab_weight_opt: None,
|
|
||||||
init_liab_weight_opt: None,
|
|
||||||
liquidation_fee_opt: None,
|
|
||||||
stable_price_delay_interval_seconds_opt: None,
|
|
||||||
stable_price_delay_growth_limit_opt: None,
|
|
||||||
stable_price_growth_limit_opt: None,
|
|
||||||
min_vault_to_deposits_ratio_opt: None,
|
|
||||||
net_borrow_limit_per_window_quote_opt: None,
|
|
||||||
net_borrow_limit_window_size_ts_opt: None,
|
|
||||||
borrow_weight_scale_start_quote_opt: None,
|
|
||||||
deposit_weight_scale_start_quote_opt: None,
|
|
||||||
reset_stable_price: true,
|
reset_stable_price: true,
|
||||||
reset_net_borrow_limit: false,
|
reset_net_borrow_limit: false,
|
||||||
reduce_only_opt: None,
|
..token_edit_instruction_default()
|
||||||
};
|
};
|
||||||
|
|
||||||
let accounts = Self::Accounts {
|
let accounts = Self::Accounts {
|
||||||
|
@ -1142,28 +1224,11 @@ impl ClientInstruction for TokenResetNetBorrows {
|
||||||
let mint_info: MintInfo = account_loader.load(&mint_info_key).await.unwrap();
|
let mint_info: MintInfo = account_loader.load(&mint_info_key).await.unwrap();
|
||||||
|
|
||||||
let instruction = Self::Instruction {
|
let instruction = Self::Instruction {
|
||||||
oracle_opt: None,
|
|
||||||
oracle_config_opt: None,
|
|
||||||
group_insurance_fund_opt: None,
|
|
||||||
interest_rate_params_opt: None,
|
|
||||||
loan_fee_rate_opt: None,
|
|
||||||
loan_origination_fee_rate_opt: None,
|
|
||||||
maint_asset_weight_opt: None,
|
|
||||||
init_asset_weight_opt: None,
|
|
||||||
maint_liab_weight_opt: None,
|
|
||||||
init_liab_weight_opt: None,
|
|
||||||
liquidation_fee_opt: None,
|
|
||||||
stable_price_delay_interval_seconds_opt: None,
|
|
||||||
stable_price_delay_growth_limit_opt: None,
|
|
||||||
stable_price_growth_limit_opt: None,
|
|
||||||
min_vault_to_deposits_ratio_opt: self.min_vault_to_deposits_ratio_opt,
|
min_vault_to_deposits_ratio_opt: self.min_vault_to_deposits_ratio_opt,
|
||||||
net_borrow_limit_per_window_quote_opt: self.net_borrow_limit_per_window_quote_opt,
|
net_borrow_limit_per_window_quote_opt: self.net_borrow_limit_per_window_quote_opt,
|
||||||
net_borrow_limit_window_size_ts_opt: self.net_borrow_limit_window_size_ts_opt,
|
net_borrow_limit_window_size_ts_opt: self.net_borrow_limit_window_size_ts_opt,
|
||||||
borrow_weight_scale_start_quote_opt: None,
|
|
||||||
deposit_weight_scale_start_quote_opt: None,
|
|
||||||
reset_stable_price: false,
|
|
||||||
reset_net_borrow_limit: true,
|
reset_net_borrow_limit: true,
|
||||||
reduce_only_opt: None,
|
..token_edit_instruction_default()
|
||||||
};
|
};
|
||||||
|
|
||||||
let accounts = Self::Accounts {
|
let accounts = Self::Accounts {
|
||||||
|
@ -1217,28 +1282,8 @@ impl ClientInstruction for TokenMakeReduceOnly {
|
||||||
let mint_info: MintInfo = account_loader.load(&mint_info_key).await.unwrap();
|
let mint_info: MintInfo = account_loader.load(&mint_info_key).await.unwrap();
|
||||||
|
|
||||||
let instruction = Self::Instruction {
|
let instruction = Self::Instruction {
|
||||||
oracle_opt: None,
|
|
||||||
oracle_config_opt: None,
|
|
||||||
group_insurance_fund_opt: None,
|
|
||||||
interest_rate_params_opt: None,
|
|
||||||
loan_fee_rate_opt: None,
|
|
||||||
loan_origination_fee_rate_opt: None,
|
|
||||||
maint_asset_weight_opt: None,
|
|
||||||
init_asset_weight_opt: None,
|
|
||||||
maint_liab_weight_opt: None,
|
|
||||||
init_liab_weight_opt: None,
|
|
||||||
liquidation_fee_opt: None,
|
|
||||||
stable_price_delay_interval_seconds_opt: None,
|
|
||||||
stable_price_delay_growth_limit_opt: None,
|
|
||||||
stable_price_growth_limit_opt: None,
|
|
||||||
min_vault_to_deposits_ratio_opt: None,
|
|
||||||
net_borrow_limit_per_window_quote_opt: None,
|
|
||||||
net_borrow_limit_window_size_ts_opt: None,
|
|
||||||
borrow_weight_scale_start_quote_opt: None,
|
|
||||||
deposit_weight_scale_start_quote_opt: None,
|
|
||||||
reset_stable_price: false,
|
|
||||||
reset_net_borrow_limit: false,
|
|
||||||
reduce_only_opt: Some(true),
|
reduce_only_opt: Some(true),
|
||||||
|
..token_edit_instruction_default()
|
||||||
};
|
};
|
||||||
|
|
||||||
let accounts = Self::Accounts {
|
let accounts = Self::Accounts {
|
||||||
|
@ -3361,7 +3406,7 @@ impl ClientInstruction for PerpLiqBasePositionInstruction {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct PerpLiqBankruptcyInstruction {
|
pub struct PerpLiqQuoteAndBankruptcyInstruction {
|
||||||
pub liqor: Pubkey,
|
pub liqor: Pubkey,
|
||||||
pub liqor_owner: TestKeypair,
|
pub liqor_owner: TestKeypair,
|
||||||
pub liqee: Pubkey,
|
pub liqee: Pubkey,
|
||||||
|
@ -3369,9 +3414,9 @@ pub struct PerpLiqBankruptcyInstruction {
|
||||||
pub max_liab_transfer: u64,
|
pub max_liab_transfer: u64,
|
||||||
}
|
}
|
||||||
#[async_trait::async_trait(?Send)]
|
#[async_trait::async_trait(?Send)]
|
||||||
impl ClientInstruction for PerpLiqBankruptcyInstruction {
|
impl ClientInstruction for PerpLiqQuoteAndBankruptcyInstruction {
|
||||||
type Accounts = mango_v4::accounts::PerpLiqBankruptcy;
|
type Accounts = mango_v4::accounts::PerpLiqQuoteAndBankruptcy;
|
||||||
type Instruction = mango_v4::instruction::PerpLiqBankruptcy;
|
type Instruction = mango_v4::instruction::PerpLiqQuoteAndBankruptcy;
|
||||||
async fn to_instruction(
|
async fn to_instruction(
|
||||||
&self,
|
&self,
|
||||||
account_loader: impl ClientAccountLoader + 'async_trait,
|
account_loader: impl ClientAccountLoader + 'async_trait,
|
||||||
|
@ -3416,10 +3461,11 @@ impl ClientInstruction for PerpLiqBankruptcyInstruction {
|
||||||
|
|
||||||
let accounts = Self::Accounts {
|
let accounts = Self::Accounts {
|
||||||
group: group_key,
|
group: group_key,
|
||||||
perp_market: self.perp_market,
|
|
||||||
liqor: self.liqor,
|
liqor: self.liqor,
|
||||||
liqor_owner: self.liqor_owner.pubkey(),
|
liqor_owner: self.liqor_owner.pubkey(),
|
||||||
liqee: self.liqee,
|
liqee: self.liqee,
|
||||||
|
perp_market: self.perp_market,
|
||||||
|
oracle: perp_market.oracle,
|
||||||
settle_bank: quote_mint_info.first_bank(),
|
settle_bank: quote_mint_info.first_bank(),
|
||||||
settle_vault: quote_mint_info.first_vault(),
|
settle_vault: quote_mint_info.first_vault(),
|
||||||
settle_oracle: quote_mint_info.oracle,
|
settle_oracle: quote_mint_info.oracle,
|
||||||
|
|
|
@ -136,14 +136,7 @@ async fn test_basic() -> Result<(), TransportError> {
|
||||||
//
|
//
|
||||||
// TEST: Compute the account health
|
// TEST: Compute the account health
|
||||||
//
|
//
|
||||||
send_tx(solana, ComputeAccountDataInstruction { account })
|
assert_eq!(account_init_health(solana, account).await.round(), 60.0);
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let health_data = solana
|
|
||||||
.program_log_events::<mango_v4::events::MangoAccountData>()
|
|
||||||
.pop()
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(health_data.init_health.to_num::<i64>(), 60);
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// TEST: Withdraw funds
|
// TEST: Withdraw funds
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
#![cfg(feature = "test-bpf")]
|
#![cfg(feature = "test-bpf")]
|
||||||
|
|
||||||
|
use anchor_lang::prelude::Pubkey;
|
||||||
use fixed::types::I80F48;
|
use fixed::types::I80F48;
|
||||||
use solana_program_test::*;
|
use solana_program_test::*;
|
||||||
use solana_sdk::transport::TransportError;
|
use solana_sdk::transport::TransportError;
|
||||||
|
|
||||||
use mango_v4::state::*;
|
use mango_v4::state::{PerpMarketIndex, *};
|
||||||
use program_test::*;
|
use program_test::*;
|
||||||
|
|
||||||
use mango_setup::*;
|
use mango_setup::*;
|
||||||
|
@ -180,7 +181,8 @@ async fn test_liq_perps_force_cancel() -> Result<(), TransportError> {
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportError> {
|
async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportError> {
|
||||||
let test_builder = TestContextBuilder::new();
|
let mut test_builder = TestContextBuilder::new();
|
||||||
|
test_builder.test().set_compute_max_units(100_000); // PerpLiqQuoteAndBankruptcy takes a lot of CU
|
||||||
let context = test_builder.start_default().await;
|
let context = test_builder.start_default().await;
|
||||||
let solana = &context.solana.clone();
|
let solana = &context.solana.clone();
|
||||||
|
|
||||||
|
@ -306,10 +308,6 @@ async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportEr
|
||||||
//
|
//
|
||||||
// SETUP: Trade perps between accounts
|
// SETUP: Trade perps between accounts
|
||||||
//
|
//
|
||||||
// health was 1000 + 1 * 0.8 = 1000.8 before
|
|
||||||
// after this order it is changed by -20*100*(1.4-1) = -800 for the short
|
|
||||||
// and 20*100*(0.6-1) = -800 for the long
|
|
||||||
//
|
|
||||||
send_tx(
|
send_tx(
|
||||||
solana,
|
solana,
|
||||||
PerpPlaceOrderInstruction {
|
PerpPlaceOrderInstruction {
|
||||||
|
@ -352,25 +350,29 @@ async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportEr
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
// health was 1000 before;
|
||||||
|
// after this order exchange it is changed by
|
||||||
|
// 20*100*(0.6-1) = -800 for the long account0
|
||||||
|
// 20*100*(1-1.4) = -800 for the short account1
|
||||||
|
// (100 is base lot size)
|
||||||
|
assert_eq!(
|
||||||
|
account_init_health(solana, account_0).await.round(),
|
||||||
|
1000.0 - 800.0
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
account_init_health(solana, account_1).await.round(),
|
||||||
|
1000.0 - 800.0
|
||||||
|
);
|
||||||
|
|
||||||
//
|
//
|
||||||
// SETUP: Change the oracle to make health go negative for account_0
|
// SETUP: Change the oracle to make health go negative for account_0
|
||||||
|
// perp base value decreases from 2000 * 0.6 to 2000 * 0.6 * 0.6, i.e. -480
|
||||||
//
|
//
|
||||||
set_bank_stub_oracle_price(solana, group, base_token, admin, 0.6).await;
|
set_bank_stub_oracle_price(solana, group, base_token, admin, 0.6).await;
|
||||||
|
assert_eq!(
|
||||||
// verify health is bad: can't withdraw
|
account_init_health(solana, account_0).await.round(),
|
||||||
assert!(send_tx(
|
200.0 - 480.0
|
||||||
solana,
|
);
|
||||||
TokenWithdrawInstruction {
|
|
||||||
amount: 1,
|
|
||||||
allow_borrow: false,
|
|
||||||
account: account_0,
|
|
||||||
owner,
|
|
||||||
token_account: payer_mint_accounts[0],
|
|
||||||
bank_index: 0,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.is_err());
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// TEST: Liquidate base position with limit
|
// TEST: Liquidate base position with limit
|
||||||
|
@ -403,6 +405,13 @@ async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportEr
|
||||||
-20.0 * 100.0 + liq_amount,
|
-20.0 * 100.0 + liq_amount,
|
||||||
0.1
|
0.1
|
||||||
));
|
));
|
||||||
|
assert!(assert_equal(
|
||||||
|
liqee_data.perps[0].realized_trade_pnl_native,
|
||||||
|
liq_amount - 1000.0,
|
||||||
|
0.1
|
||||||
|
));
|
||||||
|
// stable price is 1.0, so 0.2 * 1000
|
||||||
|
assert_eq!(liqee_data.perps[0].settle_pnl_limit_realized_trade, -201);
|
||||||
|
|
||||||
//
|
//
|
||||||
// TEST: Liquidate base position max
|
// TEST: Liquidate base position max
|
||||||
|
@ -587,22 +596,6 @@ async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportEr
|
||||||
0.1
|
0.1
|
||||||
));
|
));
|
||||||
|
|
||||||
//
|
|
||||||
// TEST: Can't trigger perp bankruptcy yet, account_1 isn't bankrupt
|
|
||||||
//
|
|
||||||
assert!(send_tx(
|
|
||||||
solana,
|
|
||||||
PerpLiqBankruptcyInstruction {
|
|
||||||
liqor,
|
|
||||||
liqor_owner: owner,
|
|
||||||
liqee: account_1,
|
|
||||||
perp_market,
|
|
||||||
max_liab_transfer: u64::MAX,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.is_err());
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// SETUP: We want pnl settling to cause a negative quote position,
|
// SETUP: We want pnl settling to cause a negative quote position,
|
||||||
// thus we deposit some base token collateral. To be able to do that,
|
// thus we deposit some base token collateral. To be able to do that,
|
||||||
|
@ -671,56 +664,17 @@ async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportEr
|
||||||
1
|
1
|
||||||
);
|
);
|
||||||
|
|
||||||
/*
|
|
||||||
Perp liquidation / bankruptcy tests temporarily disabled until further PRs have gone in.
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// TEST: Still can't trigger perp bankruptcy, account_1 has token collateral left
|
// TEST: Can liquidate/bankruptcy away remaining negative pnl
|
||||||
//
|
|
||||||
assert!(send_tx(
|
|
||||||
solana,
|
|
||||||
PerpLiqBankruptcyInstruction {
|
|
||||||
liqor,
|
|
||||||
liqor_owner: owner,
|
|
||||||
liqee: account_1,
|
|
||||||
perp_market,
|
|
||||||
max_liab_transfer: u64::MAX,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.is_err());
|
|
||||||
|
|
||||||
//
|
|
||||||
// SETUP: Liquidate token collateral
|
|
||||||
//
|
//
|
||||||
|
let liqee_before = solana.get_account::<MangoAccount>(account_1).await;
|
||||||
|
let liqor_before = solana.get_account::<MangoAccount>(liqor).await;
|
||||||
|
let liqee_settle_limit_before = liqee_before.perps[0]
|
||||||
|
.available_settle_limit(&perp_market_data)
|
||||||
|
.0;
|
||||||
send_tx(
|
send_tx(
|
||||||
solana,
|
solana,
|
||||||
TokenLiqWithTokenInstruction {
|
PerpLiqQuoteAndBankruptcyInstruction {
|
||||||
liqee: account_1,
|
|
||||||
liqor: liqor,
|
|
||||||
liqor_owner: owner,
|
|
||||||
asset_token_index: base_token.index,
|
|
||||||
liab_token_index: quote_token.index,
|
|
||||||
max_liab_transfer: I80F48::MAX,
|
|
||||||
asset_bank_index: 0,
|
|
||||||
liab_bank_index: 0,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
account_position(solana, account_1, quote_token.bank).await,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
assert!(account_position_closed(solana, account_1, base_token.bank).await);
|
|
||||||
|
|
||||||
//
|
|
||||||
// TEST: Now perp-bankruptcy will work, eat the insurance vault and socialize losses
|
|
||||||
//
|
|
||||||
let liqor_before = account_position_f64(solana, liqor, quote_token.bank).await;
|
|
||||||
send_tx(
|
|
||||||
solana,
|
|
||||||
PerpLiqBankruptcyInstruction {
|
|
||||||
liqor,
|
liqor,
|
||||||
liqor_owner: owner,
|
liqor_owner: owner,
|
||||||
liqee: account_1,
|
liqee: account_1,
|
||||||
|
@ -730,29 +684,47 @@ async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportEr
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
let liqee_after = solana.get_account::<MangoAccount>(account_1).await;
|
||||||
|
let liqor_data = solana.get_account::<MangoAccount>(liqor).await;
|
||||||
|
let quote_bank = solana.get_account::<Bank>(tokens[0].bank).await;
|
||||||
|
|
||||||
|
// the amount of spot the liqor received: full insurance fund, plus what was still settleable
|
||||||
|
let liq_spot_amount = insurance_vault_funding as f64 + (-liqee_settle_limit_before) as f64;
|
||||||
|
// the amount of perp quote transfered
|
||||||
|
let liq_perp_quote_amount =
|
||||||
|
(insurance_vault_funding as f64) / 1.05 + (-liqee_settle_limit_before) as f64;
|
||||||
|
|
||||||
// insurance fund was depleted and the liqor received it
|
// insurance fund was depleted and the liqor received it
|
||||||
assert_eq!(solana.token_account_balance(insurance_vault).await, 0);
|
assert_eq!(solana.token_account_balance(insurance_vault).await, 0);
|
||||||
let liqor_data = solana.get_account::<MangoAccount>(liqor).await;
|
|
||||||
let quote_bank = solana.get_account::<Bank>(tokens[0].bank).await;
|
|
||||||
assert!(assert_equal(
|
assert!(assert_equal(
|
||||||
liqor_data.tokens[0].native("e_bank),
|
liqor_data.tokens[0].native("e_bank),
|
||||||
liqor_before + insurance_vault_funding as f64,
|
liqor_before.tokens[0].native("e_bank).to_num::<f64>() + liq_spot_amount,
|
||||||
0.1
|
0.1
|
||||||
));
|
));
|
||||||
|
|
||||||
// liqee's position is gone
|
// liqor took over the max possible negative pnl
|
||||||
let liqee_data = solana.get_account::<MangoAccount>(account_1).await;
|
|
||||||
assert_eq!(liqee_data.perps[0].base_position_lots(), 0);
|
|
||||||
assert!(assert_equal(
|
assert!(assert_equal(
|
||||||
liqee_data.perps[0].quote_position_native(),
|
liqor_data.perps[0].quote_position_native(),
|
||||||
0.0,
|
liqor_before.perps[0]
|
||||||
|
.quote_position_native()
|
||||||
|
.to_num::<f64>()
|
||||||
|
- liq_perp_quote_amount,
|
||||||
0.1
|
0.1
|
||||||
));
|
));
|
||||||
|
|
||||||
|
// liqee exited liquidation
|
||||||
|
assert!(account_init_health(solana, account_1).await >= 0.0);
|
||||||
|
assert_eq!(liqee_after.being_liquidated, 0);
|
||||||
|
|
||||||
// the remainder got socialized via funding payments
|
// the remainder got socialized via funding payments
|
||||||
let socialized_amount = -remaining_pnl - 100.0 / 1.05;
|
|
||||||
let perp_market = solana.get_account::<PerpMarket>(perp_market).await;
|
let perp_market = solana.get_account::<PerpMarket>(perp_market).await;
|
||||||
|
let pnl_before = liqee_before.perps[0]
|
||||||
|
.pnl_for_price(&perp_market, I80F48::ONE)
|
||||||
|
.unwrap();
|
||||||
|
let pnl_after = liqee_after.perps[0]
|
||||||
|
.pnl_for_price(&perp_market, I80F48::ONE)
|
||||||
|
.unwrap();
|
||||||
|
let socialized_amount = (pnl_after - pnl_before).to_num::<f64>() - liq_perp_quote_amount;
|
||||||
assert!(assert_equal(
|
assert!(assert_equal(
|
||||||
perp_market.long_funding,
|
perp_market.long_funding,
|
||||||
socialized_amount / 20.0,
|
socialized_amount / 20.0,
|
||||||
|
@ -763,7 +735,466 @@ async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportEr
|
||||||
-socialized_amount / 20.0,
|
-socialized_amount / 20.0,
|
||||||
0.1
|
0.1
|
||||||
));
|
));
|
||||||
*/
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_liq_perps_bankruptcy() -> Result<(), TransportError> {
|
||||||
|
let mut test_builder = TestContextBuilder::new();
|
||||||
|
test_builder.test().set_compute_max_units(200_000); // PerpLiqQuoteAndBankruptcy takes a lot of CU
|
||||||
|
let context = test_builder.start_default().await;
|
||||||
|
let solana = &context.solana.clone();
|
||||||
|
|
||||||
|
let admin = TestKeypair::new();
|
||||||
|
let owner = context.users[0].key;
|
||||||
|
let payer = context.users[1].key;
|
||||||
|
let mints = &context.mints[0..3];
|
||||||
|
let payer_mint_accounts = &context.users[1].token_accounts[0..3];
|
||||||
|
|
||||||
|
//
|
||||||
|
// SETUP: Create a group and an account to fill the vaults
|
||||||
|
//
|
||||||
|
|
||||||
|
let GroupWithTokens {
|
||||||
|
group,
|
||||||
|
tokens,
|
||||||
|
insurance_vault,
|
||||||
|
..
|
||||||
|
} = GroupWithTokensConfig {
|
||||||
|
admin,
|
||||||
|
payer,
|
||||||
|
mints: mints.to_vec(),
|
||||||
|
zero_token_is_quote: true,
|
||||||
|
..GroupWithTokensConfig::default()
|
||||||
|
}
|
||||||
|
.create(solana)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
send_tx(
|
||||||
|
solana,
|
||||||
|
TokenEditWeights {
|
||||||
|
group,
|
||||||
|
admin,
|
||||||
|
mint: mints[2].pubkey,
|
||||||
|
maint_liab_weight: 1.0,
|
||||||
|
maint_asset_weight: 1.0,
|
||||||
|
init_liab_weight: 1.0,
|
||||||
|
init_asset_weight: 1.0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let fund_insurance = |amount: u64| async move {
|
||||||
|
let mut tx = ClientTransaction::new(solana);
|
||||||
|
tx.add_instruction_direct(
|
||||||
|
spl_token::instruction::transfer(
|
||||||
|
&spl_token::ID,
|
||||||
|
&payer_mint_accounts[0],
|
||||||
|
&insurance_vault,
|
||||||
|
&payer.pubkey(),
|
||||||
|
&[&payer.pubkey()],
|
||||||
|
amount,
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
tx.add_signer(payer);
|
||||||
|
tx.send().await.unwrap();
|
||||||
|
};
|
||||||
|
|
||||||
|
let quote_token = &tokens[0]; // USDC, 1/1 weights, price 1, never changed
|
||||||
|
let base_token = &tokens[1]; // used for perp market
|
||||||
|
let collateral_token = &tokens[2]; // used for adjusting account health
|
||||||
|
|
||||||
|
// deposit some funds, to the vaults aren't empty
|
||||||
|
let liqor = create_funded_account(
|
||||||
|
&solana,
|
||||||
|
group,
|
||||||
|
owner,
|
||||||
|
250,
|
||||||
|
&context.users[1],
|
||||||
|
mints,
|
||||||
|
10000,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// all perp markets used here default to price = 1.0, base_lot_size = 100
|
||||||
|
let price_lots = 100;
|
||||||
|
|
||||||
|
let context_ref = &context;
|
||||||
|
let mut perp_market_index: PerpMarketIndex = 0;
|
||||||
|
let setup_perp_inner = |perp_market_index: PerpMarketIndex,
|
||||||
|
health: i64,
|
||||||
|
pnl: i64,
|
||||||
|
settle_limit: i64| async move {
|
||||||
|
// price used later to produce negative pnl with a short:
|
||||||
|
// doubling the price leads to -100 pnl
|
||||||
|
let adj_price = 1.0 + pnl as f64 / -100.0;
|
||||||
|
let adj_price_lots = (price_lots as f64 * adj_price) as i64;
|
||||||
|
|
||||||
|
let mango_v4::accounts::PerpCreateMarket { perp_market, .. } = send_tx(
|
||||||
|
solana,
|
||||||
|
PerpCreateMarketInstruction {
|
||||||
|
group,
|
||||||
|
admin,
|
||||||
|
payer,
|
||||||
|
perp_market_index,
|
||||||
|
quote_lot_size: 1,
|
||||||
|
base_lot_size: 100,
|
||||||
|
maint_asset_weight: 0.8,
|
||||||
|
init_asset_weight: 0.6,
|
||||||
|
maint_liab_weight: 1.2,
|
||||||
|
init_liab_weight: 1.4,
|
||||||
|
liquidation_fee: 0.05,
|
||||||
|
maker_fee: 0.0,
|
||||||
|
taker_fee: 0.0,
|
||||||
|
group_insurance_fund: true,
|
||||||
|
// adjust this factur such that we get the desired settle limit in the end
|
||||||
|
settle_pnl_limit_factor: (settle_limit as f32 + 0.1).min(0.0)
|
||||||
|
/ (-1.0 * 100.0 * adj_price) as f32,
|
||||||
|
settle_pnl_limit_window_size_ts: 24 * 60 * 60,
|
||||||
|
..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, base_token).await
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
set_perp_stub_oracle_price(solana, group, perp_market, &base_token, admin, 1.0).await;
|
||||||
|
set_bank_stub_oracle_price(solana, group, &collateral_token, admin, 1.0).await;
|
||||||
|
|
||||||
|
//
|
||||||
|
// SETUP: accounts
|
||||||
|
//
|
||||||
|
let deposit_amount = 1000;
|
||||||
|
let helper_account = create_funded_account(
|
||||||
|
&solana,
|
||||||
|
group,
|
||||||
|
owner,
|
||||||
|
perp_market_index as u32 * 2,
|
||||||
|
&context_ref.users[1],
|
||||||
|
&mints[2..3],
|
||||||
|
deposit_amount,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let account = create_funded_account(
|
||||||
|
&solana,
|
||||||
|
group,
|
||||||
|
owner,
|
||||||
|
perp_market_index as u32 * 2 + 1,
|
||||||
|
&context_ref.users[1],
|
||||||
|
&mints[2..3],
|
||||||
|
deposit_amount,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
//
|
||||||
|
// SETUP: Trade perps between accounts twice to generate pnl, settle_limit
|
||||||
|
//
|
||||||
|
let mut tx = ClientTransaction::new(solana);
|
||||||
|
tx.add_instruction(PerpPlaceOrderInstruction {
|
||||||
|
account: helper_account,
|
||||||
|
perp_market,
|
||||||
|
owner,
|
||||||
|
side: Side::Bid,
|
||||||
|
price_lots,
|
||||||
|
max_base_lots: 1,
|
||||||
|
max_quote_lots: i64::MAX,
|
||||||
|
client_order_id: 0,
|
||||||
|
reduce_only: false,
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
tx.add_instruction(PerpPlaceOrderInstruction {
|
||||||
|
account: account,
|
||||||
|
perp_market,
|
||||||
|
owner,
|
||||||
|
side: Side::Ask,
|
||||||
|
price_lots,
|
||||||
|
max_base_lots: 1,
|
||||||
|
max_quote_lots: i64::MAX,
|
||||||
|
client_order_id: 0,
|
||||||
|
reduce_only: false,
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
tx.add_instruction(PerpConsumeEventsInstruction {
|
||||||
|
perp_market,
|
||||||
|
mango_accounts: vec![account, helper_account],
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
tx.send().await.unwrap();
|
||||||
|
|
||||||
|
set_perp_stub_oracle_price(solana, group, perp_market, &base_token, admin, adj_price).await;
|
||||||
|
let mut tx = ClientTransaction::new(solana);
|
||||||
|
tx.add_instruction(PerpPlaceOrderInstruction {
|
||||||
|
account: helper_account,
|
||||||
|
perp_market,
|
||||||
|
owner,
|
||||||
|
side: Side::Ask,
|
||||||
|
price_lots: adj_price_lots,
|
||||||
|
max_base_lots: 1,
|
||||||
|
max_quote_lots: i64::MAX,
|
||||||
|
client_order_id: 0,
|
||||||
|
reduce_only: false,
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
tx.add_instruction(PerpPlaceOrderInstruction {
|
||||||
|
account: account,
|
||||||
|
perp_market,
|
||||||
|
owner,
|
||||||
|
side: Side::Bid,
|
||||||
|
price_lots: adj_price_lots,
|
||||||
|
max_base_lots: 1,
|
||||||
|
max_quote_lots: i64::MAX,
|
||||||
|
client_order_id: 0,
|
||||||
|
reduce_only: false,
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
tx.add_instruction(PerpConsumeEventsInstruction {
|
||||||
|
perp_market,
|
||||||
|
mango_accounts: vec![account, helper_account],
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
tx.send().await.unwrap();
|
||||||
|
|
||||||
|
set_perp_stub_oracle_price(solana, group, perp_market, &base_token, admin, 1.0).await;
|
||||||
|
|
||||||
|
// Adjust target health:
|
||||||
|
// full health = 1000 * collat price * 1.0 + pnl
|
||||||
|
set_bank_stub_oracle_price(
|
||||||
|
solana,
|
||||||
|
group,
|
||||||
|
&collateral_token,
|
||||||
|
admin,
|
||||||
|
(health - pnl) as f64 / 1000.0,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Verify we got it right
|
||||||
|
let account_data = solana.get_account::<MangoAccount>(account).await;
|
||||||
|
assert_eq!(account_data.perps[0].quote_position_native(), pnl);
|
||||||
|
assert_eq!(
|
||||||
|
account_data.perps[0].settle_pnl_limit_realized_trade,
|
||||||
|
settle_limit
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
account_init_health(solana, account).await.round(),
|
||||||
|
health as f64
|
||||||
|
);
|
||||||
|
|
||||||
|
(perp_market, account)
|
||||||
|
};
|
||||||
|
let mut setup_perp = |health: i64, pnl: i64, settle_limit: i64| {
|
||||||
|
let out = setup_perp_inner(perp_market_index, health, pnl, settle_limit);
|
||||||
|
perp_market_index += 1;
|
||||||
|
out
|
||||||
|
};
|
||||||
|
|
||||||
|
let limit_prec = |f: f64| (f * 1000.0).round() / 1000.0;
|
||||||
|
|
||||||
|
let liq_event_amounts = || {
|
||||||
|
let settlement = solana
|
||||||
|
.program_log_events::<mango_v4::logs::PerpLiqQuoteAndBankruptcyLog>()
|
||||||
|
.pop()
|
||||||
|
.map(|v| limit_prec(I80F48::from_bits(v.settlement).to_num::<f64>()))
|
||||||
|
.unwrap_or(0.0);
|
||||||
|
let (insur, loss) = solana
|
||||||
|
.program_log_events::<mango_v4::logs::PerpLiqBankruptcyLog>()
|
||||||
|
.pop()
|
||||||
|
.map(|v| {
|
||||||
|
(
|
||||||
|
I80F48::from_bits(v.insurance_transfer).to_num::<u64>(),
|
||||||
|
limit_prec(I80F48::from_bits(v.socialized_loss).to_num::<f64>()),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.unwrap_or((0, 0.0));
|
||||||
|
(settlement, insur, loss)
|
||||||
|
};
|
||||||
|
|
||||||
|
let liqor_info = |perp_market: Pubkey| async move {
|
||||||
|
let perp_market = solana.get_account::<PerpMarket>(perp_market).await;
|
||||||
|
let liqor_data = solana.get_account::<MangoAccount>(liqor).await;
|
||||||
|
let liqor_perp = liqor_data
|
||||||
|
.perps
|
||||||
|
.iter()
|
||||||
|
.find(|p| p.market_index == perp_market.perp_market_index)
|
||||||
|
.unwrap()
|
||||||
|
.clone();
|
||||||
|
(liqor_data, liqor_perp)
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
let (perp_market, account) = setup_perp(-28, -50, -10).await;
|
||||||
|
let liqor_quote_before = account_position(solana, liqor, quote_token.bank).await;
|
||||||
|
|
||||||
|
send_tx(
|
||||||
|
solana,
|
||||||
|
PerpLiqQuoteAndBankruptcyInstruction {
|
||||||
|
liqor,
|
||||||
|
liqor_owner: owner,
|
||||||
|
liqee: account,
|
||||||
|
perp_market,
|
||||||
|
max_liab_transfer: 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(liq_event_amounts(), (1.0, 0, 0.0));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
account_position(solana, account, quote_token.bank).await,
|
||||||
|
-1
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
account_position(solana, liqor, quote_token.bank).await,
|
||||||
|
liqor_quote_before + 1
|
||||||
|
);
|
||||||
|
let acc_data = solana.get_account::<MangoAccount>(account).await;
|
||||||
|
assert_eq!(acc_data.perps[0].quote_position_native(), -49);
|
||||||
|
assert_eq!(acc_data.being_liquidated, 1);
|
||||||
|
let (_liqor_data, liqor_perp) = liqor_info(perp_market).await;
|
||||||
|
assert_eq!(liqor_perp.quote_position_native(), -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let (perp_market, account) = setup_perp(-28, -50, -10).await;
|
||||||
|
fund_insurance(2).await;
|
||||||
|
let liqor_quote_before = account_position(solana, liqor, quote_token.bank).await;
|
||||||
|
|
||||||
|
send_tx(
|
||||||
|
solana,
|
||||||
|
PerpLiqQuoteAndBankruptcyInstruction {
|
||||||
|
liqor,
|
||||||
|
liqor_owner: owner,
|
||||||
|
liqee: account,
|
||||||
|
perp_market,
|
||||||
|
max_liab_transfer: 11,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(liq_event_amounts(), (10.0, 2, 27.0));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
account_position(solana, account, quote_token.bank).await,
|
||||||
|
-10
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
account_position(solana, liqor, quote_token.bank).await,
|
||||||
|
liqor_quote_before + 12
|
||||||
|
);
|
||||||
|
let acc_data = solana.get_account::<MangoAccount>(account).await;
|
||||||
|
assert!(assert_equal(
|
||||||
|
acc_data.perps[0].quote_position_native(),
|
||||||
|
-50.0 + 11.0 + 27.0,
|
||||||
|
0.1
|
||||||
|
));
|
||||||
|
assert_eq!(acc_data.being_liquidated, 0);
|
||||||
|
let (_liqor_data, liqor_perp) = liqor_info(perp_market).await;
|
||||||
|
assert_eq!(liqor_perp.quote_position_native(), -11);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let (perp_market, account) = setup_perp(-28, -50, -10).await;
|
||||||
|
fund_insurance(5).await;
|
||||||
|
|
||||||
|
send_tx(
|
||||||
|
solana,
|
||||||
|
PerpLiqQuoteAndBankruptcyInstruction {
|
||||||
|
liqor,
|
||||||
|
liqor_owner: owner,
|
||||||
|
liqee: account,
|
||||||
|
perp_market,
|
||||||
|
max_liab_transfer: 16,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
liq_event_amounts(),
|
||||||
|
(10.0, 5, limit_prec(28.0 - 5.0 / 1.05))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// no insurance
|
||||||
|
{
|
||||||
|
let (perp_market, account) = setup_perp(-28, -50, -10).await;
|
||||||
|
|
||||||
|
send_tx(
|
||||||
|
solana,
|
||||||
|
PerpLiqQuoteAndBankruptcyInstruction {
|
||||||
|
liqor,
|
||||||
|
liqor_owner: owner,
|
||||||
|
liqee: account,
|
||||||
|
perp_market,
|
||||||
|
max_liab_transfer: u64::MAX,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(liq_event_amounts(), (10.0, 0, limit_prec(28.0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// no settlement: no settle health
|
||||||
|
{
|
||||||
|
let (perp_market, account) = setup_perp(-200, -50, -10).await;
|
||||||
|
fund_insurance(5).await;
|
||||||
|
|
||||||
|
send_tx(
|
||||||
|
solana,
|
||||||
|
PerpLiqQuoteAndBankruptcyInstruction {
|
||||||
|
liqor,
|
||||||
|
liqor_owner: owner,
|
||||||
|
liqee: account,
|
||||||
|
perp_market,
|
||||||
|
max_liab_transfer: u64::MAX,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(liq_event_amounts(), (0.0, 5, limit_prec(50.0 - 5.0 / 1.05)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// no settlement: no settle limit
|
||||||
|
{
|
||||||
|
let (perp_market, account) = setup_perp(-40, -50, 0).await;
|
||||||
|
// no insurance
|
||||||
|
|
||||||
|
send_tx(
|
||||||
|
solana,
|
||||||
|
PerpLiqQuoteAndBankruptcyInstruction {
|
||||||
|
liqor,
|
||||||
|
liqor_owner: owner,
|
||||||
|
liqee: account,
|
||||||
|
perp_market,
|
||||||
|
max_liab_transfer: u64::MAX,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(liq_event_amounts(), (0.0, 0, limit_prec(40.0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// no socialized loss: fully covered by insurance fund
|
||||||
|
{
|
||||||
|
let (perp_market, account) = setup_perp(-40, -50, -5).await;
|
||||||
|
fund_insurance(42).await;
|
||||||
|
|
||||||
|
send_tx(
|
||||||
|
solana,
|
||||||
|
PerpLiqQuoteAndBankruptcyInstruction {
|
||||||
|
liqor,
|
||||||
|
liqor_owner: owner,
|
||||||
|
liqee: account,
|
||||||
|
perp_market,
|
||||||
|
max_liab_transfer: u64::MAX,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(liq_event_amounts(), (5.0, 42, 0.0));
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue