Configurable perp settle token (#550)

This changes perp market margining to no longer assume all pnl is in USD
while settlement is in USDC. Instead, a configurable settle token is used for
pnl and settlement, defaulting to USDC. 

There is no difference while the USDC price is forced to $1 and the init and liab
weights are 1. But with this patch, it becomes possible to change that.

For now it is not recommended to use a token other than USDC or USDT (or
another USD targeting stable token) for perp settlement.

The patch also updates all insurance vault use to be aware that the insurance
fund is not in USD but in USDC and apply the USDC price before payouts.
To do this, the previous PerpLiqNegativePnlOrBankruptcy was replaced by
a new PerpLiqNegativePnlOrBankruptcyV2 instruction.

Co-authored-by: microwavedcola1 <89031858+microwavedcola1@users.noreply.github.com>
This commit is contained in:
Christian Kamm 2023-05-17 15:50:05 +02:00 committed by GitHub
parent 5d31d6bf32
commit 5fc7aa1092
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 3925 additions and 1573 deletions

View File

@ -283,83 +283,6 @@ impl<'a> LiquidateHelper<'a> {
Ok(Some(sig))
}
/*
async fn perp_settle_pnl(&self) -> anyhow::Result<Option<Signature>> {
let perp_settle_health = self.health_cache.perp_settle_health();
let mut perp_settleable_pnl = self
.liqee
.active_perp_positions()
.filter_map(|pp| {
if pp.base_position_lots() != 0 {
return None;
}
let pnl = pp.quote_position_native();
// TODO: outdated: must account for perp settle limit
let settleable_pnl = if pnl > 0 {
pnl
} else if pnl < 0 && perp_settle_health > 0 {
pnl.max(-perp_settle_health)
} else {
return None;
};
if settleable_pnl.abs() < 1 {
return None;
}
Some((pp.market_index, settleable_pnl))
})
.collect::<Vec<(PerpMarketIndex, I80F48)>>();
// sort by pnl, descending
perp_settleable_pnl.sort_by(|a, b| b.1.cmp(&a.1));
if perp_settleable_pnl.is_empty() {
return Ok(None);
}
for (perp_index, pnl) in perp_settleable_pnl {
let direction = if pnl > 0 {
client::perp_pnl::Direction::MaxNegative
} else {
client::perp_pnl::Direction::MaxPositive
};
let counters = client::perp_pnl::fetch_top(
&self.client.context,
self.account_fetcher,
perp_index,
direction,
2,
)
.await?;
if counters.is_empty() {
// If we can't settle some positive PNL because we're lacking a suitable counterparty,
// then liquidation should continue, even though this step produced no transaction
log::info!("Could not settle perp pnl {pnl} for account {}, perp market {perp_index}: no counterparty",
self.pubkey);
continue;
}
let (counter_key, counter_acc, counter_pnl) = counters.first().unwrap();
log::info!("Trying to settle perp pnl account: {} market_index: {perp_index} amount: {pnl} against {counter_key} with pnl: {counter_pnl}", self.pubkey);
let (account_a, account_b) = if pnl > 0 {
((self.pubkey, self.liqee), (counter_key, counter_acc))
} else {
((counter_key, counter_acc), (self.pubkey, self.liqee))
};
let sig = self
.client
.perp_settle_pnl(perp_index, account_a, account_b)
.await?;
log::info!(
"Settled perp pnl for perp market on account {}, market index {perp_index}, maint_health was {}, tx sig {sig:?}",
self.pubkey,
self.maint_health,
);
return Ok(Some(sig));
}
return Ok(None);
}
*/
async fn perp_liq_negative_pnl_or_bankruptcy(&self) -> anyhow::Result<Option<Signature>> {
if !self.health_cache.in_phase3_liquidation() {
return Ok(None);
@ -472,7 +395,7 @@ impl<'a> LiquidateHelper<'a> {
}
async fn token_liq(&self) -> anyhow::Result<Option<Signature>> {
if !self.health_cache.has_spot_assets() || !self.health_cache.has_spot_borrows() {
if !self.health_cache.has_possible_spot_liquidations() {
return Ok(None);
}
@ -541,7 +464,7 @@ impl<'a> LiquidateHelper<'a> {
}
async fn token_liq_bankruptcy(&self) -> anyhow::Result<Option<Signature>> {
if !self.health_cache.in_phase3_liquidation() || !self.health_cache.has_spot_borrows() {
if !self.health_cache.in_phase3_liquidation() || !self.health_cache.has_liq_spot_borrows() {
return Ok(None);
}

View File

@ -115,13 +115,14 @@ impl SettlementState {
.await
.context("creating health cache")?;
let liq_end_health = health_cache.health(HealthType::LiquidationEnd);
let perp_settle_health = health_cache.perp_settle_health();
for perp_market_index in perp_indexes {
let (perp_market, price) = match perp_market_info.get(&perp_market_index) {
Some(v) => v,
None => continue, // skip accounts with perp positions where we couldn't get the price and market
};
let perp_max_settle =
health_cache.perp_max_settle(perp_market.settle_token_index)?;
let perp_position = account.perp_position_mut(perp_market_index).unwrap();
perp_position.settle_funding(perp_market);
@ -132,7 +133,7 @@ impl SettlementState {
let settleable = if limited >= 0 {
limited
} else {
limited.max(-perp_settle_health).min(I80F48::ZERO)
limited.max(-perp_max_settle).min(I80F48::ZERO)
};
if settleable > 0 {

View File

@ -1081,6 +1081,7 @@ impl MangoClient {
let perp = self.context.perp(market_index);
let settle_token_info = self.context.token(perp.market.settle_token_index);
let insurance_token_info = self.context.token(INSURANCE_TOKEN_INDEX);
let health_remaining_ams = self
.derive_liquidation_health_check_remaining_account_metas(
@ -1095,7 +1096,7 @@ impl MangoClient {
program_id: mango_v4::id(),
accounts: {
let mut ams = anchor_lang::ToAccountMetas::to_account_metas(
&mango_v4::accounts::PerpLiqNegativePnlOrBankruptcy {
&mango_v4::accounts::PerpLiqNegativePnlOrBankruptcyV2 {
group: self.group(),
perp_market: perp.address,
oracle: perp.market.oracle,
@ -1106,6 +1107,9 @@ impl MangoClient {
settle_vault: settle_token_info.mint_info.first_vault(),
settle_oracle: settle_token_info.mint_info.oracle,
insurance_vault: group.insurance_vault,
insurance_bank: insurance_token_info.mint_info.first_bank(),
insurance_bank_vault: insurance_token_info.mint_info.first_vault(),
insurance_oracle: insurance_token_info.mint_info.oracle,
token_program: Token::id(),
},
None,
@ -1114,7 +1118,7 @@ impl MangoClient {
ams
},
data: anchor_lang::InstructionData::data(
&mango_v4::instruction::PerpLiqNegativePnlOrBankruptcy { max_liab_transfer },
&mango_v4::instruction::PerpLiqNegativePnlOrBankruptcyV2 { max_liab_transfer },
),
};
self.send_and_confirm_owner_tx(vec![ix]).await

View File

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

View File

@ -4550,6 +4550,87 @@
}
]
},
{
"name": "perpLiqNegativePnlOrBankruptcyV2",
"accounts": [
{
"name": "group",
"isMut": false,
"isSigner": false
},
{
"name": "liqor",
"isMut": true,
"isSigner": false
},
{
"name": "liqorOwner",
"isMut": false,
"isSigner": true
},
{
"name": "liqee",
"isMut": true,
"isSigner": false
},
{
"name": "perpMarket",
"isMut": true,
"isSigner": false
},
{
"name": "oracle",
"isMut": false,
"isSigner": false
},
{
"name": "settleBank",
"isMut": true,
"isSigner": false
},
{
"name": "settleVault",
"isMut": true,
"isSigner": false
},
{
"name": "settleOracle",
"isMut": false,
"isSigner": false
},
{
"name": "insuranceVault",
"isMut": true,
"isSigner": false
},
{
"name": "insuranceBank",
"isMut": true,
"isSigner": false
},
{
"name": "insuranceBankVault",
"isMut": true,
"isSigner": false
},
{
"name": "insuranceOracle",
"isMut": false,
"isSigner": false
},
{
"name": "tokenProgram",
"isMut": false,
"isSigner": false
}
],
"args": [
{
"name": "maxLiabTransfer",
"type": "u64"
}
]
},
{
"name": "altSet",
"accounts": [
@ -6105,7 +6186,13 @@
}
},
{
"name": "balanceNative",
"name": "balanceSpot",
"docs": [
"Freely available spot balance for the token.",
"",
"Includes TokenPosition and free Serum3OpenOrders balances.",
"Does not include perp upnl or Serum3 reserved amounts."
],
"type": {
"defined": "I80F48"
}
@ -6161,6 +6248,10 @@
"name": "perpMarketIndex",
"type": "u16"
},
{
"name": "settleTokenIndex",
"type": "u16"
},
{
"name": "maintBaseAssetWeight",
"type": {
@ -6220,7 +6311,7 @@
}
},
{
"name": "prices",
"name": "basePrices",
"type": {
"defined": "Prices"
}
@ -6480,8 +6571,14 @@
{
"name": "quotePositionNative",
"docs": [
"Active position in quote (conversation rate is that of the time the order was settled)",
"measured in native quote"
"Active position in oracle quote native. At the same time this is 1:1 a settle_token native amount.",
"",
"Example: Say there's a perp market on the BTC/USD price using SOL for settlement. The user buys",
"one long contract for $20k, then base = 1, quote = -20k. The price goes to $21k. Now their",
"unsettled pnl is (1 * 21k - 20k) __SOL__ = 1000 SOL. This is because the perp contract arbitrarily",
"decides that each unit of price difference creates 1 SOL worth of settlement.",
"(yes, causing 1 SOL of settlement for each $1 price change implies a lot of extra leverage; likely",
"there should be an extra configurable scaling factor before we use this for cases like that)"
],
"type": {
"defined": "I80F48"

View File

@ -32,8 +32,11 @@ pub struct PerpLiqBaseOrPositivePnl<'info> {
)]
pub liqee: AccountLoader<'info, MangoAccountFixed>,
// bank correctness is checked at #2
#[account(mut, has_one = group)]
#[account(
mut,
has_one = group,
constraint = settle_bank.load()?.token_index == perp_market.load()?.settle_token_index @ MangoError::InvalidBank
)]
pub settle_bank: AccountLoader<'info, Bank>,
#[account(

View File

@ -34,8 +34,11 @@ pub struct PerpLiqNegativePnlOrBankruptcy<'info> {
/// 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)]
#[account(
mut,
has_one = group,
constraint = settle_bank.load()?.token_index == perp_market.load()?.settle_token_index @ MangoError::InvalidBank
)]
pub settle_bank: AccountLoader<'info, Bank>,
#[account(
@ -56,12 +59,85 @@ pub struct PerpLiqNegativePnlOrBankruptcy<'info> {
pub token_program: Program<'info, Token>,
}
impl<'info> PerpLiqNegativePnlOrBankruptcy<'info> {
#[derive(Accounts)]
pub struct PerpLiqNegativePnlOrBankruptcyV2<'info> {
#[account(
has_one = insurance_vault,
constraint = group.load()?.is_ix_enabled(IxGate::PerpLiqNegativePnlOrBankruptcy) @ MangoError::IxIsDisabled,
)]
pub group: AccountLoader<'info, Group>,
#[account(
mut,
has_one = group,
constraint = liqor.load()?.is_operational() @ MangoError::AccountIsFrozen,
// 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,
constraint = liqee.load()?.is_operational() @ MangoError::AccountIsFrozen
)]
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>,
#[account(
mut,
has_one = group,
constraint = settle_bank.load()?.token_index == perp_market.load()?.settle_token_index @ MangoError::InvalidBank
)]
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>,
#[account(
mut,
has_one = group,
constraint = insurance_bank.load()?.token_index == INSURANCE_TOKEN_INDEX
)]
pub insurance_bank: AccountLoader<'info, Bank>,
#[account(
mut,
address = insurance_bank.load()?.vault
)]
pub insurance_bank_vault: Account<'info, TokenAccount>,
/// CHECK: Oracle can have different account types
#[account(address = insurance_bank.load()?.oracle)]
pub insurance_oracle: UncheckedAccount<'info>,
pub token_program: Program<'info, Token>,
}
impl<'info> PerpLiqNegativePnlOrBankruptcyV2<'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(),
to: self.insurance_bank_vault.to_account_info(),
authority: self.group.to_account_info(),
};
CpiContext::new(program, accounts)

View File

@ -40,6 +40,7 @@ pub struct TokenLiqBankruptcy<'info> {
#[account(mut)]
// address is checked at #2 a) and b)
// better name would be "insurance_bank_vault"
pub quote_vault: Account<'info, TokenAccount>,
// future: this would be an insurance fund vault specific to a

View File

@ -193,6 +193,72 @@ impl<T: KeyedAccountReader> AccountRetriever for FixedOrderAccountRetriever<T> {
}
}
pub struct ScannedBanksAndOracles<'a, 'info> {
banks: Vec<AccountInfoRefMut<'a, 'info>>,
oracles: Vec<AccountInfoRef<'a, 'info>>,
index_map: HashMap<TokenIndex, usize>,
staleness_slot: Option<u64>,
}
impl<'a, 'info> ScannedBanksAndOracles<'a, 'info> {
#[inline]
fn bank_index(&self, token_index: TokenIndex) -> Result<usize> {
Ok(*self.index_map.get(&token_index).ok_or_else(|| {
error_msg_typed!(
MangoError::TokenPositionDoesNotExist,
"token index {} not found",
token_index
)
})?)
}
#[allow(clippy::type_complexity)]
pub fn banks_mut_and_oracles(
&mut self,
token_index1: TokenIndex,
token_index2: TokenIndex,
) -> Result<(&mut Bank, I80F48, Option<(&mut Bank, I80F48)>)> {
if token_index1 == token_index2 {
let index = self.bank_index(token_index1)?;
let bank = self.banks[index].load_mut_fully_unchecked::<Bank>()?;
let oracle = &self.oracles[index];
let price = bank.oracle_price(oracle, self.staleness_slot)?;
return Ok((bank, price, None));
}
let index1 = self.bank_index(token_index1)?;
let index2 = self.bank_index(token_index2)?;
let (first, second, swap) = if index1 < index2 {
(index1, index2, false)
} else {
(index2, index1, true)
};
// split_at_mut after the first bank and after the second bank
let (first_bank_part, second_bank_part) = self.banks.split_at_mut(first + 1);
let bank1 = first_bank_part[first].load_mut_fully_unchecked::<Bank>()?;
let bank2 = second_bank_part[second - (first + 1)].load_mut_fully_unchecked::<Bank>()?;
let oracle1 = &self.oracles[first];
let oracle2 = &self.oracles[second];
let price1 = bank1.oracle_price(oracle1, self.staleness_slot)?;
let price2 = bank2.oracle_price(oracle2, self.staleness_slot)?;
if swap {
Ok((bank2, price2, Some((bank1, price1))))
} else {
Ok((bank1, price1, Some((bank2, price2))))
}
}
pub fn scanned_bank_and_oracle(&self, token_index: TokenIndex) -> Result<(&Bank, I80F48)> {
let index = self.bank_index(token_index)?;
// The account was already loaded successfully during construction
let bank = self.banks[index].load_fully_unchecked::<Bank>()?;
let oracle = &self.oracles[index];
let price = bank.oracle_price(oracle, self.staleness_slot)?;
Ok((bank, price))
}
}
/// Takes a list of account infos containing
/// - an unknown number of Banks in any order, followed by
/// - the same number of oracles in the same order as the banks, followed by
@ -202,14 +268,11 @@ impl<T: KeyedAccountReader> AccountRetriever for FixedOrderAccountRetriever<T> {
/// and retrieves accounts needed for the health computation by doing a linear
/// scan for each request.
pub struct ScanningAccountRetriever<'a, 'info> {
banks: Vec<AccountInfoRefMut<'a, 'info>>,
oracles: Vec<AccountInfoRef<'a, 'info>>,
banks_and_oracles: ScannedBanksAndOracles<'a, 'info>,
perp_markets: Vec<AccountInfoRef<'a, 'info>>,
perp_oracles: Vec<AccountInfoRef<'a, 'info>>,
serum3_oos: Vec<AccountInfoRef<'a, 'info>>,
token_index_map: HashMap<TokenIndex, usize>,
perp_index_map: HashMap<PerpMarketIndex, usize>,
staleness_slot: Option<u64>,
}
/// Returns None if `ai` doesn't have the owner or discriminator for T.
@ -289,28 +352,19 @@ impl<'a, 'info> ScanningAccountRetriever<'a, 'info> {
let serum3_start = perp_oracles_start + n_perps;
Ok(Self {
banks: AccountInfoRefMut::borrow_slice(&ais[..n_banks])?,
oracles: AccountInfoRef::borrow_slice(&ais[n_banks..perps_start])?,
banks_and_oracles: ScannedBanksAndOracles {
banks: AccountInfoRefMut::borrow_slice(&ais[..n_banks])?,
oracles: AccountInfoRef::borrow_slice(&ais[n_banks..perps_start])?,
index_map: token_index_map,
staleness_slot,
},
perp_markets: AccountInfoRef::borrow_slice(&ais[perps_start..perp_oracles_start])?,
perp_oracles: AccountInfoRef::borrow_slice(&ais[perp_oracles_start..serum3_start])?,
serum3_oos: AccountInfoRef::borrow_slice(&ais[serum3_start..])?,
token_index_map,
perp_index_map,
staleness_slot,
})
}
#[inline]
fn bank_index(&self, token_index: TokenIndex) -> Result<usize> {
Ok(*self.token_index_map.get(&token_index).ok_or_else(|| {
error_msg_typed!(
MangoError::TokenPositionDoesNotExist,
"token index {} not found",
token_index
)
})?)
}
#[inline]
fn perp_market_index(&self, perp_market_index: PerpMarketIndex) -> Result<usize> {
Ok(*self
@ -325,44 +379,12 @@ impl<'a, 'info> ScanningAccountRetriever<'a, 'info> {
token_index1: TokenIndex,
token_index2: TokenIndex,
) -> Result<(&mut Bank, I80F48, Option<(&mut Bank, I80F48)>)> {
if token_index1 == token_index2 {
let index = self.bank_index(token_index1)?;
let bank = self.banks[index].load_mut_fully_unchecked::<Bank>()?;
let oracle = &self.oracles[index];
let price = bank.oracle_price(oracle, self.staleness_slot)?;
return Ok((bank, price, None));
}
let index1 = self.bank_index(token_index1)?;
let index2 = self.bank_index(token_index2)?;
let (first, second, swap) = if index1 < index2 {
(index1, index2, false)
} else {
(index2, index1, true)
};
// split_at_mut after the first bank and after the second bank
let (first_bank_part, second_bank_part) = self.banks.split_at_mut(first + 1);
let bank1 = first_bank_part[first].load_mut_fully_unchecked::<Bank>()?;
let bank2 = second_bank_part[second - (first + 1)].load_mut_fully_unchecked::<Bank>()?;
let oracle1 = &self.oracles[first];
let oracle2 = &self.oracles[second];
let price1 = bank1.oracle_price(oracle1, self.staleness_slot)?;
let price2 = bank2.oracle_price(oracle2, self.staleness_slot)?;
if swap {
Ok((bank2, price2, Some((bank1, price1))))
} else {
Ok((bank1, price1, Some((bank2, price2))))
}
self.banks_and_oracles
.banks_mut_and_oracles(token_index1, token_index2)
}
pub fn scanned_bank_and_oracle(&self, token_index: TokenIndex) -> Result<(&Bank, I80F48)> {
let index = self.bank_index(token_index)?;
// The account was already loaded successfully during construction
let bank = self.banks[index].load_fully_unchecked::<Bank>()?;
let oracle = &self.oracles[index];
let price = bank.oracle_price(oracle, self.staleness_slot)?;
Ok((bank, price))
self.banks_and_oracles.scanned_bank_and_oracle(token_index)
}
pub fn scanned_perp_market_and_oracle(
@ -373,7 +395,7 @@ impl<'a, 'info> ScanningAccountRetriever<'a, 'info> {
// The account was already loaded successfully during construction
let perp_market = self.perp_markets[index].load_fully_unchecked::<PerpMarket>()?;
let oracle_acc = &self.perp_oracles[index];
let price = perp_market.oracle_price(oracle_acc, self.staleness_slot)?;
let price = perp_market.oracle_price(oracle_acc, self.banks_and_oracles.staleness_slot)?;
Ok((perp_market, price))
}
@ -385,6 +407,10 @@ impl<'a, 'info> ScanningAccountRetriever<'a, 'info> {
.ok_or_else(|| error_msg!("no serum3 open orders for key {}", key))?;
serum3_cpi::load_open_orders(oo)
}
pub fn into_banks_and_oracles(self) -> ScannedBanksAndOracles<'a, 'info> {
self.banks_and_oracles
}
}
impl<'a, 'info> AccountRetriever for ScanningAccountRetriever<'a, 'info> {
@ -471,9 +497,9 @@ mod tests {
let mut retriever =
ScanningAccountRetriever::new_with_staleness(&ais, &group, None).unwrap();
assert_eq!(retriever.banks.len(), 3);
assert_eq!(retriever.token_index_map.len(), 3);
assert_eq!(retriever.oracles.len(), 3);
assert_eq!(retriever.banks_and_oracles.banks.len(), 3);
assert_eq!(retriever.banks_and_oracles.index_map.len(), 3);
assert_eq!(retriever.banks_and_oracles.oracles.len(), 3);
assert_eq!(retriever.perp_markets.len(), 2);
assert_eq!(retriever.perp_oracles.len(), 2);
assert_eq!(retriever.perp_index_map.len(), 2);

View File

@ -89,6 +89,55 @@ pub fn compute_health(
Ok(new_health_cache(account, retriever)?.health(health_type))
}
/// How much of a token can be taken away before health decreases to zero?
///
/// If health is negative, returns 0.
pub fn spot_amount_taken_for_health_zero(
mut health: I80F48,
starting_spot: I80F48,
asset_weighted_price: I80F48,
liab_weighted_price: I80F48,
) -> Result<I80F48> {
if health <= 0 {
return Ok(I80F48::ZERO);
}
let mut taken_spot = I80F48::ZERO;
if starting_spot > 0 {
if asset_weighted_price > 0 {
let asset_max = health / asset_weighted_price;
if asset_max <= starting_spot {
return Ok(asset_max);
}
}
taken_spot = starting_spot;
health -= starting_spot * asset_weighted_price;
}
if health > 0 {
require_gt!(liab_weighted_price, 0);
taken_spot += health / liab_weighted_price;
}
Ok(taken_spot)
}
/// How much of a token can be gained before health increases to zero?
///
/// Returns 0 if health is positive.
pub fn spot_amount_given_for_health_zero(
health: I80F48,
starting_spot: I80F48,
asset_weighted_price: I80F48,
liab_weighted_price: I80F48,
) -> Result<I80F48> {
// asset/liab prices are reversed intentionally
spot_amount_taken_for_health_zero(
-health,
-starting_spot,
liab_weighted_price,
asset_weighted_price,
)
}
#[derive(Clone, AnchorDeserialize, AnchorSerialize, Debug)]
pub struct TokenInfo {
pub token_index: TokenIndex,
@ -99,7 +148,25 @@ pub struct TokenInfo {
pub init_liab_weight: I80F48,
pub init_scaled_liab_weight: I80F48,
pub prices: Prices,
pub balance_native: I80F48,
/// Freely available spot balance for the token.
///
/// Includes TokenPosition and free Serum3OpenOrders balances.
/// Does not include perp upnl or Serum3 reserved amounts.
pub balance_spot: I80F48,
}
/// Temporary value used during health computations
#[derive(Clone, Default)]
pub struct TokenBalance {
/// Sum of token_info.balance_spot and perp health_unsettled_pnl balances
pub spot_and_perp: I80F48,
}
#[derive(Clone, Default)]
pub struct TokenMaxReserved {
/// The sum of serum-reserved amounts over all markets
pub max_serum_reserved: I80F48,
}
impl TokenInfo {
@ -112,6 +179,11 @@ impl TokenInfo {
}
}
#[inline(always)]
pub fn asset_weighted_price(&self, health_type: HealthType) -> I80F48 {
self.asset_weight(health_type) * self.prices.asset(health_type)
}
#[inline(always)]
fn liab_weight(&self, health_type: HealthType) -> I80F48 {
match health_type {
@ -122,16 +194,18 @@ impl TokenInfo {
}
#[inline(always)]
fn health_contribution(&self, health_type: HealthType) -> I80F48 {
let (weight, price) = if self.balance_native.is_negative() {
(self.liab_weight(health_type), self.prices.liab(health_type))
pub fn liab_weighted_price(&self, health_type: HealthType) -> I80F48 {
self.liab_weight(health_type) * self.prices.liab(health_type)
}
#[inline(always)]
pub fn health_contribution(&self, health_type: HealthType, balance: I80F48) -> I80F48 {
let weighted_price = if balance.is_negative() {
self.liab_weighted_price(health_type)
} else {
(
self.asset_weight(health_type),
self.prices.asset(health_type),
)
self.asset_weighted_price(health_type)
};
self.balance_native * price * weight
balance * weighted_price
}
}
@ -141,8 +215,10 @@ pub struct Serum3Info {
pub reserved_base: I80F48,
pub reserved_quote: I80F48,
// Index into TokenInfos _not_ a TokenIndex
pub base_index: usize,
pub quote_index: usize,
pub market_index: Serum3MarketIndex,
/// The open orders account has no free or reserved funds
pub has_zero_funds: bool,
@ -180,7 +256,8 @@ impl Serum3Info {
&self,
health_type: HealthType,
token_infos: &[TokenInfo],
token_max_reserved: &[I80F48],
token_balances: &[TokenBalance],
token_max_reserved: &[TokenMaxReserved],
market_reserved: &Serum3Reserved,
) -> I80F48 {
if market_reserved.all_reserved_as_base.is_zero()
@ -191,43 +268,45 @@ impl Serum3Info {
let base_info = &token_infos[self.base_index];
let quote_info = &token_infos[self.quote_index];
let base_max_reserved = token_max_reserved[self.base_index];
let quote_max_reserved = token_max_reserved[self.quote_index];
// How much would health increase if the reserved balance were applied to the passed
// token info?
let compute_health_effect =
|token_info: &TokenInfo, token_max_reserved: I80F48, market_reserved: I80F48| {
// This balance includes all possible reserved funds from markets that relate to the
// token, including this market itself: `market_reserved` is already included in `token_max_reserved`.
let max_balance = token_info.balance_native + token_max_reserved;
let compute_health_effect = |token_info: &TokenInfo,
balance: &TokenBalance,
max_reserved: &TokenMaxReserved,
market_reserved: I80F48| {
// This balance includes all possible reserved funds from markets that relate to the
// token, including this market itself: `market_reserved` is already included in `max_serum_reserved`.
let max_balance = balance.spot_and_perp + max_reserved.max_serum_reserved;
// For simplicity, we assume that `market_reserved` was added to `max_balance` last
// (it underestimates health because that gives the smallest effects): how much did
// health change because of it?
let (asset_part, liab_part) = if max_balance >= market_reserved {
(market_reserved, I80F48::ZERO)
} else if max_balance.is_negative() {
(I80F48::ZERO, market_reserved)
} else {
(max_balance, market_reserved - max_balance)
};
let asset_weight = token_info.asset_weight(health_type);
let liab_weight = token_info.liab_weight(health_type);
let asset_price = token_info.prices.asset(health_type);
let liab_price = token_info.prices.liab(health_type);
asset_part * asset_weight * asset_price + liab_part * liab_weight * liab_price
// For simplicity, we assume that `market_reserved` was added to `max_balance` last
// (it underestimates health because that gives the smallest effects): how much did
// health change because of it?
let (asset_part, liab_part) = if max_balance >= market_reserved {
(market_reserved, I80F48::ZERO)
} else if max_balance.is_negative() {
(I80F48::ZERO, market_reserved)
} else {
(max_balance, market_reserved - max_balance)
};
let asset_weight = token_info.asset_weight(health_type);
let liab_weight = token_info.liab_weight(health_type);
let asset_price = token_info.prices.asset(health_type);
let liab_price = token_info.prices.liab(health_type);
asset_part * asset_weight * asset_price + liab_part * liab_weight * liab_price
};
let health_base = compute_health_effect(
base_info,
base_max_reserved,
&token_balances[self.base_index],
&token_max_reserved[self.base_index],
market_reserved.all_reserved_as_base,
);
let health_quote = compute_health_effect(
quote_info,
quote_max_reserved,
&token_balances[self.quote_index],
&token_max_reserved[self.quote_index],
market_reserved.all_reserved_as_quote,
);
health_base.min(health_quote)
@ -245,6 +324,7 @@ pub(crate) struct Serum3Reserved {
#[derive(Clone, AnchorDeserialize, AnchorSerialize, Debug)]
pub struct PerpInfo {
pub perp_market_index: PerpMarketIndex,
pub settle_token_index: TokenIndex,
pub maint_base_asset_weight: I80F48,
pub init_base_asset_weight: I80F48,
pub maint_base_liab_weight: I80F48,
@ -257,13 +337,17 @@ pub struct PerpInfo {
pub asks_base_lots: i64,
// in health-reference-token native units, no asset/liab factor needed
pub quote: I80F48,
pub prices: Prices,
pub base_prices: Prices,
pub has_open_orders: bool,
pub has_open_fills: bool,
}
impl PerpInfo {
fn new(perp_position: &PerpPosition, perp_market: &PerpMarket, prices: Prices) -> Result<Self> {
fn new(
perp_position: &PerpPosition,
perp_market: &PerpMarket,
base_prices: Prices,
) -> Result<Self> {
let base_lots = perp_position.base_position_lots() + perp_position.taker_base_lots;
let unsettled_funding = perp_position.unsettled_funding(perp_market);
@ -272,6 +356,7 @@ impl PerpInfo {
Ok(Self {
perp_market_index: perp_market.perp_market_index,
settle_token_index: perp_market.settle_token_index,
init_base_asset_weight: perp_market.init_base_asset_weight,
init_base_liab_weight: perp_market.init_base_liab_weight,
maint_base_asset_weight: perp_market.maint_base_asset_weight,
@ -283,20 +368,25 @@ impl PerpInfo {
bids_base_lots: perp_position.bids_base_lots,
asks_base_lots: perp_position.asks_base_lots,
quote: quote_current,
prices,
base_prices,
has_open_orders: perp_position.has_open_orders(),
has_open_fills: perp_position.has_open_taker_fills(),
})
}
/// Total health contribution from perp balances
/// The perp-risk (but not token-risk) adjusted upnl. Also called "hupnl".
///
/// In settle token native units.
///
/// This is what gets added to effective_token_balances() and then contributes
/// to account health.
///
/// For fully isolated perp markets, users may never borrow against unsettled
/// positive perp pnl, there pnl_asset_weight == 0 and there can't be positive
/// positive perp pnl, there overall_asset_weight == 0 and there can't be positive
/// health contributions from these perp market. We sometimes call these markets
/// "untrusted markets".
///
/// Users need to settle their perp pnl with other perp market participants
/// In these, users need to settle their perp pnl with other perp market participants
/// in order to realize their gains if they want to use them as collateral.
///
/// This is because we don't trust the perp's base price to not suddenly jump to
@ -306,29 +396,31 @@ impl PerpInfo {
///
/// Other markets may be liquid enough that we have enough confidence to allow
/// users to borrow against unsettled positive pnl to some extend. In these cases,
/// the pnl asset weights would be >0.
/// the overall asset weights would be >0.
#[inline(always)]
pub fn health_contribution(&self, health_type: HealthType) -> I80F48 {
let contribution = self.unweighted_health_contribution(health_type);
self.weigh_health_contribution(contribution, health_type)
pub fn health_unsettled_pnl(&self, health_type: HealthType) -> I80F48 {
let contribution = self.unweighted_health_unsettled_pnl(health_type);
self.weigh_uhupnl_overall(contribution, health_type)
}
/// Convert uhupnl to hupnl by applying the overall weight.
#[inline(always)]
pub fn weigh_health_contribution(&self, unweighted: I80F48, health_type: HealthType) -> I80F48 {
fn weigh_uhupnl_overall(&self, unweighted: I80F48, health_type: HealthType) -> I80F48 {
if unweighted > 0 {
let asset_weight = match health_type {
let overall_weight = match health_type {
HealthType::Init | HealthType::LiquidationEnd => self.init_overall_asset_weight,
HealthType::Maint => self.maint_overall_asset_weight,
};
asset_weight * unweighted
overall_weight * unweighted
} else {
unweighted
}
}
/// Health in terms of settle token, without the overall asset weight or the settle token weight or price.
/// also called "uhupnl"
#[inline(always)]
pub fn unweighted_health_contribution(&self, health_type: HealthType) -> I80F48 {
pub fn unweighted_health_unsettled_pnl(&self, health_type: HealthType) -> I80F48 {
let order_execution_case = |orders_base_lots: i64, order_price: I80F48| {
let net_base_native =
I80F48::from((self.base_lots + orders_base_lots) * self.base_lot_size);
@ -343,9 +435,9 @@ impl PerpInfo {
(HealthType::Maint, false) => self.maint_base_asset_weight,
};
let base_price = if net_base_native.is_negative() {
self.prices.liab(health_type)
self.base_prices.liab(health_type)
} else {
self.prices.asset(health_type)
self.base_prices.asset(health_type)
};
// Total value of the order-execution adjusted base position
@ -359,8 +451,10 @@ impl PerpInfo {
};
// What is worse: Executing all bids at oracle_price.liab, or executing all asks at oracle_price.asset?
let bids_case = order_execution_case(self.bids_base_lots, self.prices.liab(health_type));
let asks_case = order_execution_case(-self.asks_base_lots, self.prices.asset(health_type));
let bids_case =
order_execution_case(self.bids_base_lots, self.base_prices.liab(health_type));
let asks_case =
order_execution_case(-self.asks_base_lots, self.base_prices.asset(health_type));
let worst_case = bids_case.min(asks_case);
self.quote + worst_case
@ -377,35 +471,135 @@ pub struct HealthCache {
impl HealthCache {
pub fn health(&self, health_type: HealthType) -> I80F48 {
let token_balances = self.effective_token_balances(health_type);
let mut health = I80F48::ZERO;
let sum = |contrib| {
health += contrib;
};
self.health_sum(health_type, sum);
self.health_sum(health_type, sum, &token_balances);
health
}
/// Sum of only the positive health components (assets) and
/// sum of absolute values of all negative health components (liabs, always >= 0)
pub fn health_assets_and_liabs(&self, health_type: HealthType) -> (I80F48, I80F48) {
let mut assets = I80F48::ZERO;
let mut liabs = I80F48::ZERO;
let sum = |contrib| {
if contrib > 0 {
assets += contrib;
pub fn health_assets_and_liabs_stable_assets(
&self,
health_type: HealthType,
) -> (I80F48, I80F48) {
self.health_assets_and_liabs(health_type, true)
}
pub fn health_assets_and_liabs_stable_liabs(
&self,
health_type: HealthType,
) -> (I80F48, I80F48) {
self.health_assets_and_liabs(health_type, false)
}
/// Loop over the token, perp, serum contributions and add up all positive values into `assets`
/// and (the abs) of negative values separately into `liabs`. Return (assets, liabs).
///
/// Due to the way token and perp positions sum before being weighted, there's some flexibility
/// in how the sum is split up. It can either be split up such that the amount of liabs stays
/// constant when assets change, or the other way around.
///
/// For example, if assets are held stable: An account with $10 in SOL and -$12 hupnl in a
/// SOL-settled perp market would have:
/// - assets: $10 * SOL_asset_weight
/// - liabs: $10 * SOL_asset_weight + $2 * SOL_liab_weight
/// because some of the liabs are weighted lower as they are just compensating the assets.
///
/// Same example if liabs are held stable:
/// - liabs: $12 * SOL_liab_weight
/// - assets: $10 * SOL_liab_weight
///
/// The value `assets - liabs` is the health and the same in both cases.
fn health_assets_and_liabs(
&self,
health_type: HealthType,
stable_assets: bool,
) -> (I80F48, I80F48) {
let mut total_assets = I80F48::ZERO;
let mut total_liabs = I80F48::ZERO;
let add = |assets: &mut I80F48, liabs: &mut I80F48, value: I80F48| {
if value > 0 {
*assets += value;
} else {
liabs -= contrib;
*liabs += -value;
}
};
self.health_sum(health_type, sum);
(assets, liabs)
for token_info in self.token_infos.iter() {
// For each token, health only considers the effective token position. But for
// this function we want to distinguish the contribution from token deposits from
// contributions by perp markets.
// However, the overall weight is determined by the sum, so first collect all
// assets parts and all liab parts and then determine the actual values.
let mut asset_balance = I80F48::ZERO;
let mut liab_balance = I80F48::ZERO;
add(
&mut asset_balance,
&mut liab_balance,
token_info.balance_spot,
);
for perp_info in self.perp_infos.iter() {
if perp_info.settle_token_index != token_info.token_index {
continue;
}
let health_unsettled = perp_info.health_unsettled_pnl(health_type);
add(&mut asset_balance, &mut liab_balance, health_unsettled);
}
// The assignment to total_assets and total_liabs is a bit arbitrary.
// As long as the (added_assets - added_liabs) = weighted(asset_balance - liab_balance),
// the result will be consistent.
if stable_assets {
let asset_weighted_price = token_info.asset_weighted_price(health_type);
let assets = asset_balance * asset_weighted_price;
total_assets += assets;
if asset_balance >= liab_balance {
// liabs partially compensate
total_liabs += liab_balance * asset_weighted_price;
} else {
let liab_weighted_price = token_info.liab_weighted_price(health_type);
// the liabs fully compensate the assets and even add something extra
total_liabs += assets + (liab_balance - asset_balance) * liab_weighted_price;
}
} else {
let liab_weighted_price = token_info.liab_weighted_price(health_type);
let liabs = liab_balance * liab_weighted_price;
total_liabs += liabs;
if asset_balance >= liab_balance {
let asset_weighted_price = token_info.asset_weighted_price(health_type);
// the assets fully compensate the liabs and even add something extra
total_assets += liabs + (asset_balance - liab_balance) * asset_weighted_price;
} else {
// assets partially compensate
total_assets += asset_balance * liab_weighted_price;
}
}
}
let token_balances = self.effective_token_balances(health_type);
let (token_max_reserved, serum3_reserved) = self.compute_serum3_reservations(health_type);
for (serum3_info, reserved) in self.serum3_infos.iter().zip(serum3_reserved.iter()) {
let contrib = serum3_info.health_contribution(
health_type,
&self.token_infos,
&token_balances,
&token_max_reserved,
reserved,
);
add(&mut total_assets, &mut total_liabs, contrib);
}
(total_assets, total_liabs)
}
pub fn token_info(&self, token_index: TokenIndex) -> Result<&TokenInfo> {
Ok(&self.token_infos[self.token_info_index(token_index)?])
}
fn token_info_index(&self, token_index: TokenIndex) -> Result<usize> {
pub fn token_info_index(&self, token_index: TokenIndex) -> Result<usize> {
self.token_infos
.iter()
.position(|t| t.token_index == token_index)
@ -451,7 +645,7 @@ impl HealthCache {
// We need to make sure that if balance is before * price, then change = -before
// brings it to exactly zero.
let removed_contribution = -change;
entry.balance_native -= removed_contribution;
entry.balance_spot -= removed_contribution;
Ok(())
}
@ -476,11 +670,11 @@ impl HealthCache {
// Apply it to the tokens
{
let base_entry = &mut self.token_infos[base_entry_index];
base_entry.balance_native += free_base_change;
base_entry.balance_spot += free_base_change;
}
{
let quote_entry = &mut self.token_infos[quote_entry_index];
quote_entry.balance_native += free_quote_change;
quote_entry.balance_spot += free_quote_change;
}
// Apply it to the serum3 info
@ -504,15 +698,37 @@ impl HealthCache {
.iter_mut()
.find(|m| m.perp_market_index == perp_market.perp_market_index)
.ok_or_else(|| error_msg!("perp market {} not found", perp_market.perp_market_index))?;
*perp_entry = PerpInfo::new(perp_position, perp_market, perp_entry.prices.clone())?;
*perp_entry = PerpInfo::new(perp_position, perp_market, perp_entry.base_prices.clone())?;
Ok(())
}
pub fn has_spot_assets(&self) -> bool {
self.token_infos.iter().any(|ti| {
// can use token_liq_with_token
ti.balance_native >= 1
})
/// Liquidatable spot assets mean: actual token deposits and also a positive effective token balance
pub fn has_liq_spot_assets(&self) -> bool {
let health_token_balances = self.effective_token_balances(HealthType::LiquidationEnd);
self.token_infos
.iter()
.zip(health_token_balances.iter())
.any(|(ti, b)| {
// need 1 native token to use token_liq_with_token
ti.balance_spot >= 1 && b.spot_and_perp >= 1
})
}
/// Liquidatable spot borrows mean: actual toen borrows plus a negative effective token balance
pub fn has_liq_spot_borrows(&self) -> bool {
let health_token_balances = self.effective_token_balances(HealthType::LiquidationEnd);
self.token_infos
.iter()
.zip(health_token_balances.iter())
.any(|(ti, b)| ti.balance_spot < 0 && b.spot_and_perp < 0)
}
// This function exists separately from has_liq_spot_assets and has_liq_spot_borrows for performance reasons
pub fn has_possible_spot_liquidations(&self) -> bool {
let health_token_balances = self.effective_token_balances(HealthType::LiquidationEnd);
let all_iter = || self.token_infos.iter().zip(health_token_balances.iter());
all_iter().any(|(ti, b)| ti.balance_spot < 0 && b.spot_and_perp < 0)
&& all_iter().any(|(ti, b)| ti.balance_spot >= 1 && b.spot_and_perp >= 1)
}
pub fn has_serum3_open_orders_funds(&self) -> bool {
@ -537,8 +753,10 @@ impl HealthCache {
.any(|p| p.base_lots == 0 && p.quote > 0)
}
pub fn has_perp_negative_pnl(&self) -> bool {
self.perp_infos.iter().any(|p| p.quote < 0)
pub fn has_perp_negative_pnl_no_base(&self) -> bool {
self.perp_infos
.iter()
.any(|p| p.base_lots == 0 && p.quote < 0)
}
/// Phase1 is spot/perp order cancellation and spot settlement since
@ -566,7 +784,7 @@ impl HealthCache {
/// it changes the base position, so need to wait for it to be processed...)
/// - 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_possible_spot_liquidations()
|| self.has_perp_base_positions()
|| self.has_perp_open_fills()
|| self.has_perp_positive_pnl_no_base()
@ -575,7 +793,7 @@ impl HealthCache {
pub fn require_after_phase2_liquidation(&self) -> Result<()> {
self.require_after_phase1_liquidation()?;
require!(
!self.has_spot_assets() || !self.has_spot_borrows(),
!self.has_possible_spot_liquidations(),
MangoError::HasLiquidatableTokenPosition
);
require!(
@ -601,7 +819,7 @@ impl HealthCache {
/// - token bankruptcy
/// - perp bankruptcy
pub fn has_phase3_liquidatable(&self) -> bool {
self.has_spot_borrows() || self.has_perp_negative_pnl()
self.has_liq_spot_borrows() || self.has_perp_negative_pnl_no_base()
}
pub fn in_phase3_liquidation(&self) -> bool {
@ -610,16 +828,11 @@ impl HealthCache {
&& self.has_phase3_liquidatable()
}
pub fn has_spot_borrows(&self) -> bool {
self.token_infos.iter().any(|ti| ti.balance_native < 0)
}
pub(crate) fn compute_serum3_reservations(
&self,
health_type: HealthType,
) -> (Vec<I80F48>, Vec<Serum3Reserved>) {
// For each token, compute the sum of serum-reserved amounts over all markets.
let mut token_max_reserved = vec![I80F48::ZERO; self.token_infos.len()];
) -> (Vec<TokenMaxReserved>, Vec<Serum3Reserved>) {
let mut token_max_reserved = vec![TokenMaxReserved::default(); self.token_infos.len()];
// For each serum market, compute what happened if reserved_base was converted to quote
// or reserved_quote was converted to base.
@ -634,10 +847,8 @@ impl HealthCache {
let all_reserved_as_quote =
info.all_reserved_as_quote(health_type, quote_info, base_info);
let base_max_reserved = &mut token_max_reserved[info.base_index];
*base_max_reserved += all_reserved_as_base;
let quote_max_reserved = &mut token_max_reserved[info.quote_index];
*quote_max_reserved += all_reserved_as_quote;
token_max_reserved[info.base_index].max_serum_reserved += all_reserved_as_base;
token_max_reserved[info.quote_index].max_serum_reserved += all_reserved_as_quote;
serum3_reserved.push(Serum3Reserved {
all_reserved_as_base,
@ -648,9 +859,54 @@ impl HealthCache {
(token_max_reserved, serum3_reserved)
}
pub(crate) fn health_sum(&self, health_type: HealthType, mut action: impl FnMut(I80F48)) {
for token_info in self.token_infos.iter() {
let contrib = token_info.health_contribution(health_type);
/// Returns token balances that account for spot and perp contributions
///
/// Spot contributions are just the regular deposits or borrows, as well as from free
/// funds on serum3 open orders accounts.
///
/// Perp contributions come from perp positions in markets that use the token as a settle token:
/// For these the hupnl is added to the total because that's the risk-adjusted expected to be
/// gained or lost from settlement.
pub fn effective_token_balances(&self, health_type: HealthType) -> Vec<TokenBalance> {
self.effective_token_balances_internal(health_type, false)
}
/// Implementation of effective_token_balances()
///
/// The ignore_negative_perp flag exists for perp_max_settle(). When it is enabled, all negative
/// token contributions from perp markets are ignored. That's useful for knowing how much token
/// collateral is available when limiting negative upnl settlement.
fn effective_token_balances_internal(
&self,
health_type: HealthType,
ignore_negative_perp: bool,
) -> Vec<TokenBalance> {
let mut token_balances = vec![TokenBalance::default(); self.token_infos.len()];
for perp_info in self.perp_infos.iter() {
let settle_token_index = self.token_info_index(perp_info.settle_token_index).unwrap();
let perp_settle_token = &mut token_balances[settle_token_index];
let health_unsettled = perp_info.health_unsettled_pnl(health_type);
if !ignore_negative_perp || health_unsettled > 0 {
perp_settle_token.spot_and_perp += health_unsettled;
}
}
for (token_info, token_balance) in self.token_infos.iter().zip(token_balances.iter_mut()) {
token_balance.spot_and_perp += token_info.balance_spot;
}
token_balances
}
pub(crate) fn health_sum(
&self,
health_type: HealthType,
mut action: impl FnMut(I80F48),
token_balances: &[TokenBalance],
) {
for (token_info, token_balance) in self.token_infos.iter().zip(token_balances.iter()) {
let contrib = token_info.health_contribution(health_type, token_balance.spot_and_perp);
action(contrib);
}
@ -659,19 +915,33 @@ impl HealthCache {
let contrib = serum3_info.health_contribution(
health_type,
&self.token_infos,
&token_balances,
&token_max_reserved,
reserved,
);
action(contrib);
}
for perp_info in self.perp_infos.iter() {
let contrib = perp_info.health_contribution(health_type);
action(contrib);
}
}
/// Compute the health when it comes to settling perp pnl
/// Returns how much pnl is settleable for a given settle token.
///
/// The idea of this limit is that settlement is only permissible as long as there are
/// non-perp assets that back it. If an account with 1 USD deposited somehow gets
/// a large negative perp upnl, it should not be allowed to settle that perp loss into
/// the spot world fully (because of perp/spot isolation, translating perp losses and
/// gains into tokens is restricted). Only 1 USD worth would be allowed.
///
/// Effectively, there's a health variant "perp settle health" that ignores negative
/// token contributions from perp markets. Settlement is allowed as long as perp settle
/// health remains >= 0.
///
/// For example, if perp_settle_health is 50 USD, then the settleable amount in SOL
/// would depend on the SOL price, the user's current spot balance and the SOL weights:
/// We need to compute how much the user's spot SOL balance may decrease before the
/// perp_settle_health becomes zero.
///
/// Note that the account's actual health would not change during settling negative upnl:
/// the spot balance goes down but the perp hupnl goes up accordingly.
///
/// Examples:
/// - An account may have maint_health < 0, but settling perp pnl could still be allowed.
@ -681,30 +951,24 @@ impl HealthCache {
/// (+100 USDC health, -150 perp1 health, -150 perp2 health -> allow settling 100 health worth)
/// - Positive trusted perp pnl can enable settling.
/// (+100 trusted perp1 health, -100 perp2 health -> allow settling of 100 health worth)
pub fn perp_settle_health(&self) -> I80F48 {
let health_type = HealthType::Maint;
let mut health = I80F48::ZERO;
for token_info in self.token_infos.iter() {
let contrib = token_info.health_contribution(health_type);
health += contrib;
}
pub fn perp_max_settle(&self, settle_token_index: TokenIndex) -> Result<I80F48> {
let maint_type = HealthType::Maint;
let (token_max_reserved, serum3_reserved) = self.compute_serum3_reservations(health_type);
for (serum3_info, reserved) in self.serum3_infos.iter().zip(serum3_reserved.iter()) {
let contrib = serum3_info.health_contribution(
health_type,
&self.token_infos,
&token_max_reserved,
reserved,
);
health += contrib;
}
let token_balances = self.effective_token_balances_internal(maint_type, true);
let mut perp_settle_health = I80F48::ZERO;
let sum = |contrib| {
perp_settle_health += contrib;
};
self.health_sum(maint_type, sum, &token_balances);
for perp_info in self.perp_infos.iter() {
let positive_contrib = perp_info.health_contribution(health_type).max(I80F48::ZERO);
health += positive_contrib;
}
health
let token_info_index = self.token_info_index(settle_token_index)?;
let token = &self.token_infos[token_info_index];
spot_amount_taken_for_health_zero(
perp_settle_health,
token_balances[token_info_index].spot_and_perp,
token.asset_weighted_price(maint_type),
token.liab_weighted_price(maint_type),
)
}
pub fn total_serum3_potential(
@ -780,7 +1044,7 @@ pub fn new_health_cache(
init_liab_weight: bank.init_liab_weight,
init_scaled_liab_weight: bank.scaled_init_liab_weight(liab_price),
prices,
balance_native: native,
balance_spot: native,
});
}
@ -795,11 +1059,11 @@ pub fn new_health_cache(
// add the amounts that are freely settleable immediately to token balances
let base_free = I80F48::from(oo.native_coin_free);
let quote_free = I80F48::from(oo.native_pc_free + oo.referrer_rebates_accrued);
let quote_free = I80F48::from(oo.native_pc_free);
let base_info = &mut token_infos[base_index];
base_info.balance_native += base_free;
base_info.balance_spot += base_free;
let quote_info = &mut token_infos[quote_index];
quote_info.balance_native += quote_free;
quote_info.balance_spot += quote_free;
// track the reserved amounts
let reserved_base = I80F48::from(oo.native_coin_total - oo.native_coin_free);
@ -891,12 +1155,12 @@ mod tests {
let group = Pubkey::new_unique();
let (mut bank1, mut oracle1) = mock_bank_and_oracle(group, 1, 1.0, 0.2, 0.1);
let (mut bank1, mut oracle1) = mock_bank_and_oracle(group, 0, 1.0, 0.2, 0.1);
let (mut bank2, mut oracle2) = mock_bank_and_oracle(group, 4, 5.0, 0.5, 0.3);
bank1
.data()
.deposit(
account.ensure_token_position(1).unwrap().0,
account.ensure_token_position(0).unwrap().0,
I80F48::from(100),
DUMMY_NOW_TS,
)
@ -914,7 +1178,7 @@ mod tests {
let serum3account = account.create_serum3_orders(2).unwrap();
serum3account.open_orders = oo1.pubkey;
serum3account.base_token_index = 4;
serum3account.quote_token_index = 1;
serum3account.quote_token_index = 0;
oo1.data().native_pc_total = 21;
oo1.data().native_coin_total = 18;
oo1.data().native_pc_free = 1;
@ -922,7 +1186,7 @@ mod tests {
oo1.data().referrer_rebates_accrued = 2;
let mut perp1 = mock_perp_market(group, oracle2.pubkey, 5.0, 9, (0.2, 0.1), (0.05, 0.02));
let perpaccount = account.ensure_perp_position(9, 1).unwrap().0;
let perpaccount = account.ensure_perp_position(9, 0).unwrap().0;
perpaccount.record_trade(perp1.data(), 3, -I80F48::from(310u16));
perpaccount.bids_base_lots = 7;
perpaccount.asks_base_lots = 11;
@ -943,16 +1207,18 @@ mod tests {
let retriever = ScanningAccountRetriever::new_with_staleness(&ais, &group, None).unwrap();
// for bank1/oracle1, including open orders (scenario: bids execute)
let health1 = (100.0 + 1.0 + 2.0 + (20.0 + 15.0 * 5.0)) * 0.8;
// for bank1/oracle1
// including open orders (scenario: bids execute)
let serum1 = 1.0 + (20.0 + 15.0 * 5.0);
// and perp (scenario: bids execute)
let perp1 =
(3.0 + 7.0 + 1.0) * 10.0 * 5.0 * 0.8 + (-310.0 + 2.0 * 100.0 - 7.0 * 10.0 * 5.0);
let health1 = (100.0 + serum1 + perp1) * 0.8;
// for bank2/oracle2
let health2 = (-10.0 + 3.0) * 5.0 * 1.5;
// for perp (scenario: bids execute)
let health3 =
(3.0 + 7.0 + 1.0) * 10.0 * 5.0 * 0.8 + (-310.0 + 2.0 * 100.0 - 7.0 * 10.0 * 5.0);
assert!(health_eq(
compute_health(&account.borrow(), HealthType::Init, &retriever).unwrap(),
health1 + health2 + health3
health1 + health2
));
}
@ -981,13 +1247,13 @@ mod tests {
let group = Pubkey::new_unique();
let (mut bank1, mut oracle1) = mock_bank_and_oracle(group, 1, 1.0, 0.2, 0.1);
let (mut bank1, mut oracle1) = mock_bank_and_oracle(group, 0, 1.0, 0.2, 0.1);
let (mut bank2, mut oracle2) = mock_bank_and_oracle(group, 4, 5.0, 0.5, 0.3);
let (mut bank3, mut oracle3) = mock_bank_and_oracle(group, 5, 10.0, 0.5, 0.3);
bank1
.data()
.change_without_fee(
account.ensure_token_position(1).unwrap().0,
account.ensure_token_position(0).unwrap().0,
I80F48::from(testcase.token1),
DUMMY_NOW_TS,
)
@ -1030,7 +1296,7 @@ mod tests {
let serum3account1 = account.create_serum3_orders(2).unwrap();
serum3account1.open_orders = oo1.pubkey;
serum3account1.base_token_index = 4;
serum3account1.quote_token_index = 1;
serum3account1.quote_token_index = 0;
oo1.data().native_pc_total = testcase.oo_1_2.0;
oo1.data().native_coin_total = testcase.oo_1_2.1;
@ -1038,12 +1304,12 @@ mod tests {
let serum3account2 = account.create_serum3_orders(3).unwrap();
serum3account2.open_orders = oo2.pubkey;
serum3account2.base_token_index = 5;
serum3account2.quote_token_index = 1;
serum3account2.quote_token_index = 0;
oo2.data().native_pc_total = testcase.oo_1_3.0;
oo2.data().native_coin_total = testcase.oo_1_3.1;
let mut perp1 = mock_perp_market(group, oracle2.pubkey, 5.0, 9, (0.2, 0.1), (0.05, 0.02));
let perpaccount = account.ensure_perp_position(9, 1).unwrap().0;
let perpaccount = account.ensure_perp_position(9, 0).unwrap().0;
perpaccount.record_trade(
perp1.data(),
testcase.perp1.0,
@ -1086,12 +1352,14 @@ mod tests {
oo_1_2: (20, 15),
perp1: (3, -131, 7, 11),
expected_health:
// for token1, including open orders (scenario: bids execute)
(100.0 + (20.0 + 15.0 * base_price)) * 0.8
// for token1
0.8 * (100.0
// including open orders (scenario: bids execute)
+ (20.0 + 15.0 * base_price)
// including perp (scenario: bids execute)
+ (3.0 + 7.0) * base_lots_to_quote * 0.8 + (-131.0 - 7.0 * base_lots_to_quote))
// for token2
- 10.0 * base_price * 1.5
// for perp (scenario: bids execute)
+ (3.0 + 7.0) * base_lots_to_quote * 0.8 + (-131.0 - 7.0 * base_lots_to_quote),
- 10.0 * base_price * 1.5,
..Default::default()
},
TestHealth1Case { // 1
@ -1101,35 +1369,35 @@ mod tests {
perp1: (-10, -131, 7, 11),
expected_health:
// for token1
-100.0 * 1.2
// for token2, including open orders (scenario: asks execute)
+ (10.0 * base_price + (20.0 + 15.0 * base_price)) * 0.5
1.2 * (-100.0
// for perp (scenario: asks execute)
+ (-10.0 - 11.0) * base_lots_to_quote * 1.2 + (-131.0 + 11.0 * base_lots_to_quote),
+ (-10.0 - 11.0) * base_lots_to_quote * 1.2 + (-131.0 + 11.0 * base_lots_to_quote))
// for token2, including open orders (scenario: asks execute)
+ (10.0 * base_price + (20.0 + 15.0 * base_price)) * 0.5,
..Default::default()
},
TestHealth1Case {
// 2: weighted positive perp pnl
perp1: (-1, 100, 0, 0),
expected_health: 0.95 * (100.0 - 1.2 * 1.0 * base_lots_to_quote),
expected_health: 0.8 * 0.95 * (100.0 - 1.2 * 1.0 * base_lots_to_quote),
..Default::default()
},
TestHealth1Case {
// 3: negative perp pnl is not weighted
// 3: negative perp pnl is not weighted (only the settle token weight)
perp1: (1, -100, 0, 0),
expected_health: -100.0 + 0.8 * 1.0 * base_lots_to_quote,
expected_health: 1.2 * (-100.0 + 0.8 * 1.0 * base_lots_to_quote),
..Default::default()
},
TestHealth1Case {
// 4: perp health
perp1: (10, 100, 0, 0),
expected_health: 0.95 * (100.0 + 0.8 * 10.0 * base_lots_to_quote),
expected_health: 0.8 * 0.95 * (100.0 + 0.8 * 10.0 * base_lots_to_quote),
..Default::default()
},
TestHealth1Case {
// 5: perp health
perp1: (30, -100, 0, 0),
expected_health: 0.95 * (-100.0 + 0.8 * 30.0 * base_lots_to_quote),
expected_health: 0.8 * 0.95 * (-100.0 + 0.8 * 30.0 * base_lots_to_quote),
..Default::default()
},
TestHealth1Case { // 6, reserved oo funds
@ -1250,6 +1518,20 @@ mod tests {
- 1.5 * 100.0 * 10.0 * (10000.0 * 10.0 / 10000.0),
..Default::default()
},
TestHealth1Case {
// 12: positive perp health offsets token borrow
token1: -100,
perp1: (1, 100, 0, 0),
expected_health: 0.8 * (-100.0 + 0.95 * (100.0 + 0.8 * 1.0 * base_lots_to_quote)),
..Default::default()
},
TestHealth1Case {
// 13: negative perp health offsets token deposit
token1: 100,
perp1: (-1, -100, 0, 0),
expected_health: 1.2 * (100.0 - 100.0 - 1.2 * 1.0 * base_lots_to_quote),
..Default::default()
},
];
for (i, testcase) in testcases.iter().enumerate() {

View File

@ -27,7 +27,7 @@ impl HealthCache {
///
/// Maybe talking about the collateralization ratio assets/liabs is more intuitive?
pub fn health_ratio(&self, health_type: HealthType) -> I80F48 {
let (assets, liabs) = self.health_assets_and_liabs(health_type);
let (assets, liabs) = self.health_assets_and_liabs_stable_liabs(health_type);
let hundred = I80F48::from(100);
if liabs > 0 {
// feel free to saturate to MAX for tiny liabs
@ -152,8 +152,12 @@ impl HealthCache {
let target = &self.token_infos[target_index];
let (tokens_max_reserved, _) = self.compute_serum3_reservations(health_type);
let source_reserved = tokens_max_reserved[source_index];
let target_reserved = tokens_max_reserved[target_index];
let source_reserved = tokens_max_reserved[source_index].max_serum_reserved;
let target_reserved = tokens_max_reserved[target_index].max_serum_reserved;
let token_balances = self.effective_token_balances(health_type);
let source_balance = token_balances[source_index].spot_and_perp;
let target_balance = token_balances[target_index].spot_and_perp;
// If the price is sufficiently good, then health will just increase from swapping:
// once we've swapped enough, swapping x reduces health by x * source_liab_weight and
@ -194,8 +198,8 @@ impl HealthCache {
// The first thing we do is to find this maximum.
let (amount_for_max_value, max_value) = {
// The largest amount that the maximum could be at
let rightmost = (source.balance_native.abs() + source_reserved)
.max((target.balance_native.abs() + target_reserved) / price);
let rightmost = (source_balance.abs() + source_reserved)
.max((target_balance.abs() + target_reserved) / price);
find_maximum(
I80F48::ZERO,
rightmost,
@ -279,20 +283,24 @@ impl HealthCache {
let perp_info_index = self.perp_info_index(perp_market_index)?;
let perp_info = &self.perp_infos[perp_info_index];
let prices = &perp_info.prices;
let prices = &perp_info.base_prices;
let base_lot_size = I80F48::from(perp_info.base_lot_size);
let settle_info_index = self.token_info_index(perp_info.settle_token_index)?;
let settle_info = &self.token_infos[settle_info_index];
// If the price is sufficiently good then health will just increase from trading.
// It's ok to ignore the pnl_asset_weight here because we'll jump out early if this
// slope is >=0, and the extra asset weight would just decrease it.
let final_health_slope = if direction == 1 {
// It's ok to ignore the overall_asset_weight and token asset weight here because
// we'll jump out early if this slope is >=0, and those weights would just decrease it.
let mut final_health_slope = if direction == 1 {
perp_info.init_base_asset_weight * prices.asset(health_type) - price
} else {
price - perp_info.init_base_liab_weight * prices.liab(health_type)
-perp_info.init_base_liab_weight * prices.liab(health_type) + price
};
if final_health_slope >= 0 {
return Ok(i64::MAX);
}
final_health_slope *= settle_info.liab_weighted_price(health_type);
let cache_after_trade = |base_lots: i64| -> Result<HealthCache> {
let mut adjusted_cache = self.clone();
@ -302,7 +310,7 @@ impl HealthCache {
Ok(adjusted_cache)
};
let health_ratio_after_trade =
|base_lots: i64| Ok(cache_after_trade(base_lots)?.health_ratio(HealthType::Init));
|base_lots: i64| Ok(cache_after_trade(base_lots)?.health_ratio(health_type));
let health_ratio_after_trade_trunc =
|base_lots: I80F48| health_ratio_after_trade(base_lots.round_to_zero().to_num());
@ -336,22 +344,29 @@ impl HealthCache {
// Need to figure out how many lots to trade to reach zero health (zero_health_amount).
// We do this by looking at the starting health and the health slope per
// traded base lot (final_health_slope).
let start_cache = cache_after_trade(case1_start)?;
let start_health = start_cache.health(HealthType::Init);
let mut start_cache = cache_after_trade(case1_start)?;
// The perp market's contribution to the health above may be capped. But we need to trade
// enough to fully reduce any positive-pnl buffer. Thus get the uncapped health by fixing
// the overall weight.
start_cache.perp_infos[perp_info_index].init_overall_asset_weight = I80F48::ONE;
// We don't want to deal with slope changes due to settle token assets being
// reduced first, so modify the weights to use settle token liab scaling everywhere.
// That way the final_health_slope is applicable from the start.
{
let settle_info = &mut start_cache.token_infos[settle_info_index];
settle_info.init_asset_weight = settle_info.init_liab_weight;
settle_info.init_scaled_asset_weight = settle_info.init_scaled_liab_weight;
}
let start_health = start_cache.health(health_type);
if start_health <= 0 {
return Ok(0);
}
// The perp market's contribution to the health above may be capped. But we need to trade
// enough to fully reduce any positive-pnl buffer. Thus get the uncapped health:
let perp_info = &start_cache.perp_infos[perp_info_index];
let start_health_uncapped = start_health
- perp_info.health_contribution(HealthType::Init)
+ perp_info.unweighted_health_contribution(HealthType::Init);
// We add 1 here because health is computed for truncated base_lots and we want to guarantee
// zero_health_ratio <= 0.
// zero_health_ratio <= 0. Similarly, scale down the per-lot slope slightly for a benign
// overestimation that guards against rounding issues.
let zero_health_amount = case1_start_i80f48
- start_health_uncapped / final_health_slope / base_lot_size
- start_health / (final_health_slope * base_lot_size * I80F48::from_num(0.99))
+ I80F48::ONE;
let zero_health_ratio = health_ratio_after_trade_trunc(zero_health_amount)?;
assert!(zero_health_ratio <= 0);
@ -397,6 +412,8 @@ impl HealthCache {
// positions for the source and target token index.
let token_info_index = find_token_info_index(&self.token_infos, bank.token_index)?;
let token = &self.token_infos[token_info_index];
let token_balance =
self.effective_token_balances(health_type)[token_info_index].spot_and_perp;
let cache_after_borrow = |amount: I80F48| -> Result<HealthCache> {
let now_ts = system_epoch_secs();
@ -421,7 +438,7 @@ impl HealthCache {
// At most withdraw all deposits plus enough borrows to bring health to zero
// (ensure this works with zero asset weight)
let limit = token.balance_native.max(I80F48::ZERO)
let limit = token_balance.max(I80F48::ZERO)
+ self.health(health_type).max(I80F48::ZERO) / token.init_scaled_liab_weight;
if limit <= 0 {
return Ok(I80F48::ZERO);
@ -621,7 +638,28 @@ mod tests {
init_liab_weight: I80F48::from_num(1.0 + x),
init_scaled_liab_weight: I80F48::from_num(1.0 + x),
prices: Prices::new_single_price(I80F48::from_num(price)),
balance_native: I80F48::ZERO,
balance_spot: I80F48::ZERO,
}
}
fn default_perp_info(x: f64) -> PerpInfo {
PerpInfo {
perp_market_index: 0,
settle_token_index: 0,
maint_base_asset_weight: I80F48::from_num(1.0 - x),
init_base_asset_weight: I80F48::from_num(1.0 - x),
maint_base_liab_weight: I80F48::from_num(1.0 + x),
init_base_liab_weight: I80F48::from_num(1.0 + x),
maint_overall_asset_weight: I80F48::from_num(0.6),
init_overall_asset_weight: I80F48::from_num(0.6),
base_lot_size: 1,
base_lots: 0,
bids_base_lots: 0,
asks_base_lots: 0,
quote: I80F48::ZERO,
base_prices: Prices::new_single_price(I80F48::from_num(2.0)),
has_open_orders: false,
has_open_fills: false,
}
}
@ -683,7 +721,7 @@ mod tests {
let adjust_by_usdc = |c: &mut HealthCache, ti: TokenIndex, usdc: f64| {
let ti = &mut c.token_infos[ti as usize];
ti.balance_native += I80F48::from_num(usdc) / ti.prices.oracle;
ti.balance_spot += I80F48::from_num(usdc) / ti.prices.oracle;
};
let find_max_swap_actual = |c: &HealthCache,
source: TokenIndex,
@ -980,39 +1018,54 @@ mod tests {
}
}
}
{
// swap while influenced by a perp market
println!("test 10 {test_name}");
let mut health_cache = health_cache.clone();
health_cache.perp_infos.push(PerpInfo {
perp_market_index: 0,
settle_token_index: 1,
..default_perp_info(0.3)
});
adjust_by_usdc(&mut health_cache, 0, 60.0);
for perp_quote in [-10, 10] {
health_cache.perp_infos[0].quote = I80F48::from_num(perp_quote);
for price_factor in [0.9, 1.1] {
for target in 1..100 {
let target = target as f64;
check(&health_cache, 0, 1, target, price_factor, banks);
check(&health_cache, 1, 0, target, price_factor, banks);
}
}
}
}
}
}
#[test]
fn test_max_perp() {
let base_lot_size = 100;
let default_perp_info = |x| PerpInfo {
perp_market_index: 0,
maint_base_asset_weight: I80F48::from_num(1.0 - x),
init_base_asset_weight: I80F48::from_num(1.0 - x),
maint_base_liab_weight: I80F48::from_num(1.0 + x),
init_base_liab_weight: I80F48::from_num(1.0 + x),
maint_overall_asset_weight: I80F48::from_num(0.6),
init_overall_asset_weight: I80F48::from_num(0.6),
base_lot_size,
base_lots: 0,
bids_base_lots: 0,
asks_base_lots: 0,
quote: I80F48::ZERO,
prices: Prices::new_single_price(I80F48::from_num(2.0)),
has_open_orders: false,
has_open_fills: false,
};
let health_cache = HealthCache {
token_infos: vec![TokenInfo {
token_index: 0,
balance_native: I80F48::ZERO,
..default_token_info(0.0, 1.0)
}],
token_infos: vec![
TokenInfo {
token_index: 0,
balance_spot: I80F48::ZERO,
..default_token_info(0.0, 1.0)
},
TokenInfo {
token_index: 1,
balance_spot: I80F48::ZERO,
..default_token_info(0.2, 1.5)
},
],
serum3_infos: vec![],
perp_infos: vec![PerpInfo {
perp_market_index: 0,
settle_token_index: 1,
base_lot_size,
..default_perp_info(0.3)
}],
being_liquidated: false,
@ -1032,13 +1085,13 @@ mod tests {
I80F48::ZERO
);
let adjust_token = |c: &mut HealthCache, value: f64| {
let ti = &mut c.token_infos[0];
ti.balance_native += I80F48::from_num(value);
let adjust_token = |c: &mut HealthCache, info_index: usize, value: f64| {
let ti = &mut c.token_infos[info_index];
ti.balance_spot += I80F48::from_num(value);
};
let find_max_trade =
|c: &HealthCache, side: PerpOrderSide, ratio: f64, price_factor: f64| {
let prices = &c.perp_infos[0].prices;
let prices = &c.perp_infos[0].base_prices;
let trade_price = I80F48::from_num(price_factor) * prices.oracle;
let base_lots = c
.max_perp_for_health_ratio(0, trade_price, side, I80F48::from_num(ratio))
@ -1088,18 +1141,24 @@ mod tests {
{
let mut health_cache = health_cache.clone();
adjust_token(&mut health_cache, 3000.0);
adjust_token(&mut health_cache, 0, 3000.0);
for existing in [-5, 0, 3] {
for existing_settle in [-500.0, 0.0, 300.0] {
let mut c = health_cache.clone();
c.perp_infos[0].base_lots += existing;
c.perp_infos[0].quote -= I80F48::from(existing * base_lot_size * 2);
adjust_token(&mut c, 1, existing_settle);
for existing_lots in [-5, 0, 3] {
let mut c = c.clone();
c.perp_infos[0].base_lots += existing_lots;
c.perp_infos[0].quote -= I80F48::from(existing_lots * base_lot_size * 2);
for side in [PerpOrderSide::Bid, PerpOrderSide::Ask] {
println!("test 0: existing {existing}, side {side:?}");
for price_factor in [0.8, 1.0, 1.1] {
for ratio in 1..=100 {
check_max_trade(&c, side, ratio as f64, price_factor);
for side in [PerpOrderSide::Bid, PerpOrderSide::Ask] {
println!(
"test 0: lots {existing_lots}, settle {existing_settle}, side {side:?}"
);
for price_factor in [0.8, 1.0, 1.1] {
for ratio in 1..=100 {
check_max_trade(&c, side, ratio as f64, price_factor);
}
}
}
}
@ -1128,11 +1187,11 @@ mod tests {
let group = Pubkey::new_unique();
let (mut bank1, mut oracle1) = mock_bank_and_oracle(group, 1, 1.0, 0.2, 0.1);
let (mut bank1, mut oracle1) = mock_bank_and_oracle(group, 0, 1.0, 0.2, 0.1);
bank1
.data()
.change_without_fee(
account.ensure_token_position(1).unwrap().0,
account.ensure_token_position(0).unwrap().0,
I80F48::from(100),
DUMMY_NOW_TS,
)
@ -1140,7 +1199,7 @@ mod tests {
let mut perp1 = mock_perp_market(group, oracle1.pubkey, 1.0, 9, (0.2, 0.1), (0.05, 0.02));
perp1.data().long_funding = I80F48::from_num(10.1);
let perpaccount = account.ensure_perp_position(9, 1).unwrap().0;
let perpaccount = account.ensure_perp_position(9, 0).unwrap().0;
perpaccount.record_trade(perp1.data(), 10, I80F48::from(-110));
perpaccount.long_settled_funding = I80F48::from_num(10.0);
@ -1157,13 +1216,13 @@ mod tests {
assert!(health_eq(
compute_health(&account.borrow(), HealthType::Init, &retriever).unwrap(),
// token
0.8 * 100.0
0.8 * (100.0
// perp base
+ 0.8 * 100.0
// perp quote
- 110.0
// perp funding (10 * (10.1 - 10.0))
- 1.0
- 1.0)
));
}
@ -1209,12 +1268,12 @@ mod tests {
let group = Pubkey::new_unique();
let (mut bank1, mut oracle1) = mock_bank_and_oracle(group, 1, 1.0, 0.2, 0.1);
let (mut bank1, mut oracle1) = mock_bank_and_oracle(group, 0, 1.0, 0.2, 0.1);
bank1.data().stable_price_model.stable_price = 0.5;
bank1
.data()
.change_without_fee(
account.ensure_token_position(1).unwrap().0,
account.ensure_token_position(0).unwrap().0,
I80F48::from(100),
DUMMY_NOW_TS,
)
@ -1222,7 +1281,7 @@ mod tests {
bank1
.data()
.change_without_fee(
account2.ensure_token_position(1).unwrap().0,
account2.ensure_token_position(0).unwrap().0,
I80F48::from(-100),
DUMMY_NOW_TS,
)
@ -1230,7 +1289,7 @@ mod tests {
let mut perp1 = mock_perp_market(group, oracle1.pubkey, 1.0, 9, (0.2, 0.1), (0.05, 0.02));
perp1.data().stable_price_model.stable_price = 0.5;
let perpaccount = account3.ensure_perp_position(9, 1).unwrap().0;
let perpaccount = account3.ensure_perp_position(9, 0).unwrap().0;
perpaccount.record_trade(perp1.data(), 10, I80F48::from(-100));
let oracle1_ai = oracle1.as_account_info();
@ -1261,11 +1320,11 @@ mod tests {
));
assert!(health_eq(
compute_health(&account3.borrow(), HealthType::Init, &retriever).unwrap(),
0.8 * 0.5 * 10.0 * 10.0 - 100.0
1.2 * (0.8 * 0.5 * 10.0 * 10.0 - 100.0)
));
assert!(health_eq(
compute_health(&account3.borrow(), HealthType::Maint, &retriever).unwrap(),
0.9 * 1.0 * 10.0 * 10.0 - 100.0
1.1 * (0.9 * 1.0 * 10.0 * 10.0 - 100.0)
));
}
@ -1312,13 +1371,13 @@ mod tests {
// compute the health ratio we'd get when executing the trade
let actual_ratio = {
let mut c = c.clone();
c.token_infos[0].balance_native -= max_borrow;
c.token_infos[0].balance_spot -= max_borrow;
c.health_ratio(HealthType::Init).to_num::<f64>()
};
// the ratio for borrowing one native token extra
let plus_ratio = {
let mut c = c.clone();
c.token_infos[0].balance_native -= max_borrow + I80F48::ONE;
c.token_infos[0].balance_spot -= max_borrow + I80F48::ONE;
c.health_ratio(HealthType::Init).to_num::<f64>()
};
(max_borrow, actual_ratio, plus_ratio)
@ -1339,31 +1398,82 @@ mod tests {
{
let mut health_cache = health_cache.clone();
health_cache.token_infos[0].balance_native = I80F48::from_num(100.0);
health_cache.token_infos[0].balance_spot = I80F48::from_num(100.0);
assert_eq!(check_max_borrow(&health_cache, 50.0), 100.0);
}
{
let mut health_cache = health_cache.clone();
health_cache.token_infos[1].balance_native = I80F48::from_num(50.0); // price 2, so 2*50*0.8 = 80 health
health_cache.token_infos[1].balance_spot = I80F48::from_num(50.0); // price 2, so 2*50*0.8 = 80 health
check_max_borrow(&health_cache, 100.0);
check_max_borrow(&health_cache, 50.0);
check_max_borrow(&health_cache, 0.0);
}
{
let mut health_cache = health_cache.clone();
health_cache.token_infos[0].balance_native = I80F48::from_num(50.0);
health_cache.token_infos[1].balance_native = I80F48::from_num(50.0);
health_cache.token_infos[0].balance_spot = I80F48::from_num(50.0);
health_cache.token_infos[1].balance_spot = I80F48::from_num(50.0);
check_max_borrow(&health_cache, 100.0);
check_max_borrow(&health_cache, 50.0);
check_max_borrow(&health_cache, 0.0);
}
{
let mut health_cache = health_cache.clone();
health_cache.token_infos[0].balance_native = I80F48::from_num(-50.0);
health_cache.token_infos[1].balance_native = I80F48::from_num(50.0);
health_cache.token_infos[0].balance_spot = I80F48::from_num(-50.0);
health_cache.token_infos[1].balance_spot = I80F48::from_num(50.0);
check_max_borrow(&health_cache, 100.0);
check_max_borrow(&health_cache, 50.0);
check_max_borrow(&health_cache, 0.0);
}
}
#[test]
fn test_assets_and_borrows() {
let health_cache = HealthCache {
token_infos: vec![
TokenInfo {
token_index: 0,
..default_token_info(0.0, 1.0)
},
TokenInfo {
token_index: 1,
..default_token_info(0.2, 2.0)
},
],
serum3_infos: vec![],
perp_infos: vec![PerpInfo {
perp_market_index: 0,
settle_token_index: 0,
..default_perp_info(0.3)
}],
being_liquidated: false,
};
{
let mut hc = health_cache.clone();
hc.token_infos[1].balance_spot = I80F48::from(10);
hc.perp_infos[0].quote = I80F48::from(-12);
let (assets, liabs) = hc.health_assets_and_liabs_stable_assets(HealthType::Init);
assert!((assets.to_num::<f64>() - 2.0 * 10.0 * 0.8) < 0.01);
assert!((liabs.to_num::<f64>() - 2.0 * (10.0 * 0.8 + 2.0 * 1.2)) < 0.01);
let (assets, liabs) = hc.health_assets_and_liabs_stable_liabs(HealthType::Init);
assert!((liabs.to_num::<f64>() - 2.0 * 12.0 * 1.2) < 0.01);
assert!((assets.to_num::<f64>() - 2.0 * 10.0 * 1.2) < 0.01);
}
{
let mut hc = health_cache.clone();
hc.token_infos[1].balance_spot = I80F48::from(12);
hc.perp_infos[0].quote = I80F48::from(-10);
let (assets, liabs) = hc.health_assets_and_liabs_stable_assets(HealthType::Init);
assert!((assets.to_num::<f64>() - 2.0 * 12.0 * 0.8) < 0.01);
assert!((liabs.to_num::<f64>() - 2.0 * 10.0 * 0.8) < 0.01);
let (assets, liabs) = hc.health_assets_and_liabs_stable_liabs(HealthType::Init);
assert!((liabs.to_num::<f64>() - 2.0 * 10.0 * 1.2) < 0.01);
assert!((assets.to_num::<f64>() - 2.0 * (10.0 * 1.2 + 2.0 * 0.8)) < 0.01);
}
}
}

View File

@ -125,5 +125,6 @@ pub fn mock_perp_market(
pm.data().quote_lot_size = 100;
pm.data().base_lot_size = 10;
pm.data().stable_price_model.reset_to_price(price, 0);
pm.data().settle_pnl_limit_window_size_ts = 1;
pm
}

View File

@ -40,17 +40,6 @@ pub fn perp_create_market(
settle_pnl_limit_window_size_ts: u64,
positive_pnl_liquidation_fee: f32,
) -> Result<()> {
// Settlement tokens that aren't USDC aren't fully implemented, the main missing steps are:
// - In health: the perp health needs to be adjusted by the settlement token weights.
// Otherwise settling perp pnl could decrease health.
// - In settle pnl and settle fees: use the settle oracle to convert the pnl from USD to token.
// - In perp bankruptcy: fix the assumption that the insurance fund has the same mint as
// the settlement token.
require_msg!(
settle_token_index == PERP_SETTLE_TOKEN_INDEX,
"settlement tokens != USDC are not fully implemented"
);
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
let mut perp_market = ctx.accounts.perp_market.load_init()?;

View File

@ -16,11 +16,10 @@ use crate::logs::{emit_perp_balances, PerpLiqBaseOrPositivePnlLog, TokenBalanceL
///
/// It's a combined instruction because reducing the base position is not necessarily
/// a health-increasing action when perp overall asset weight = 0. There, the pnl
/// takeover can allow further base position to be reduced.
/// takeover is necessary to allow the base position to be reduced to zero.
///
/// Taking over negative pnl - or positive pnl when the unweighted perp health contributin
/// is negative - never increases liqee health. That's why it's relegated to the
/// separate liq_negative_pnl_or_bankruptcy instruction instead.
/// Taking over pnl while health_unsettled_pnl() is negative never increases liqee health.
/// That's why it's relegated to the separate liq_negative_pnl_or_bankruptcy instruction instead.
pub fn perp_liq_base_or_positive_pnl(
ctx: Context<PerpLiqBaseOrPositivePnl>,
mut max_base_transfer: i64,
@ -67,11 +66,6 @@ pub fn perp_liq_base_or_positive_pnl(
let settle_token_index = perp_market.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 oracle_price = perp_market.oracle_price(
@ -113,7 +107,7 @@ pub fn perp_liq_base_or_positive_pnl(
)?;
//
// Wrap up
// Log changes
//
let liqee_perp_position = liqee.perp_position_mut(perp_market_index)?;
@ -208,18 +202,27 @@ pub(crate) fn liquidation_action(
max_base_transfer: i64,
max_pnl_transfer: u64,
) -> Result<(i64, I80F48, I80F48, I80F48)> {
let liq_end_type = HealthType::LiquidationEnd;
let perp_market_index = perp_market.perp_market_index;
let settle_token_index = perp_market.settle_token_index;
let liqee_perp_position = liqee.perp_position_mut(perp_market_index)?;
let liqor_perp_position = liqor.perp_position_mut(perp_market_index)?;
let token_balances = liqee_health_cache.effective_token_balances(liq_end_type);
let settle_token_balance = &token_balances[liqee_health_cache
.token_infos
.iter()
.position(|ti| ti.token_index == settle_token_index)
.unwrap()];
let settle_token_info = liqee_health_cache.token_info(settle_token_index).unwrap();
let perp_info = liqee_health_cache.perp_info(perp_market_index)?;
let settle_token_oracle_price = liqee_health_cache
.token_info(settle_token_index)?
.prices
.oracle;
let oracle_price = perp_info.prices.oracle;
let oracle_price = perp_info.base_prices.oracle;
let base_lot_size = I80F48::from(perp_market.base_lot_size);
let oracle_price_per_lot = base_lot_size * oracle_price;
@ -230,30 +233,60 @@ pub(crate) fn liquidation_action(
// (1-positive_pnl_liq_fee) USDC and only gains init_overall_asset_weight in perp health.
let max_pnl_transfer = I80F48::from(max_pnl_transfer);
// This instruction has two aspects:
//
// base reduction:
// Increase perp health by reducing the base position towards 0, exchanging base for quote
// at oracle price.
// This step will reduce unsettled_pnl() due to fees, but increase health_unsettled_pnl().
// The amount of overall health gained depends on the effective token balance (spot and
// other perp market contributions) and the overall perp asset weight.
// pnl settlement:
// Increase health by settling positive health_unsettled_pnl().
// Settling pnl when health_unsettled_pnl<0 has a negative effect on health, since it
// replaces unsettled pnl with (1-positive_pnl_liq_fee) spot tokens. But since the
// overall perp weight is less than (1-fee), settling positive health_unsettled_pnl will
// increase overall health.
//
// Base reduction increases the capacity for worthwhile pnl settlement. Also, base reduction
// may not improve overall health when the overall perp asset weight is zero. That means both need
// to be done in conjunction, where we only do enough reduction such that settlement will be able
// to bring health above the LiquidationEnd threshold.
// Liquidation steps:
// 1. If health_unsettled_pnl is negative, reduce base until it's >=0 or total health over threshold.
// Pnl settlement is not beneficial at that point anyway.
// 2. Pnl settlement of health_unsettled_pnl > 0 (while total health below threshold)
// 3. While if more settlement is possible, reduce base more to increase health_unsettled_pnl, as
// long as projected health (after settlement) stays under threshold.
// 4. Pnl settlement of health_unsettled_pnl > 0 (while total health below threshold)
// 5. If perp_overall_weight>0, reduce base further while total health under threshold.
// Take over the liqee's base in exchange for quote
let liqee_base_lots = liqee_perp_position.base_position_lots();
// Each lot the base position gets closer to 0, the unweighted perp health contribution
// increases by this amount.
let unweighted_health_per_lot;
// Each lot the base position gets closer to 0, the "unweighted health unsettled pnl"
// increases by this amount
let uhupnl_per_lot;
// -1 (liqee base lots decrease) or +1 (liqee base lots increase)
let direction: i64;
// Either 1+fee or 1-fee, depending on direction.
let fee_factor;
let base_fee_factor;
if liqee_base_lots > 0 {
require_msg!(
max_base_transfer >= 0,
"max_base_transfer can't be negative when liqee's base_position is positive"
);
// the unweighted perp health contribution gets reduced by `base * price * perp_init_asset_weight`
// and increased by `base * price * (1 - liq_fee) * quote_init_asset_weight`
let quote_init_asset_weight = I80F48::ONE;
// the health_unsettled_pnl gets reduced by `base * base_price * perp_init_asset_weight`
// and increased by `base * base_price * (1 - liq_fee)`
direction = -1;
fee_factor = I80F48::ONE - perp_market.base_liquidation_fee;
let asset_price = perp_info.prices.asset(HealthType::LiquidationEnd);
unweighted_health_per_lot =
-asset_price * base_lot_size * perp_market.init_base_asset_weight
+ oracle_price_per_lot * quote_init_asset_weight * fee_factor;
base_fee_factor = I80F48::ONE - perp_market.base_liquidation_fee;
uhupnl_per_lot =
oracle_price_per_lot * (-perp_market.init_base_asset_weight + base_fee_factor);
} else {
// liqee_base_lots <= 0
require_msg!(
@ -261,123 +294,234 @@ pub(crate) fn liquidation_action(
"max_base_transfer can't be positive when liqee's base_position is negative"
);
// health gets increased by `base * price * perp_init_liab_weight`
// and reduced by `base * price * (1 + liq_fee) * quote_init_liab_weight`
let quote_init_liab_weight = I80F48::ONE;
// health gets increased by `base * base_price * perp_init_liab_weight`
// and reduced by `base * base_price * (1 + liq_fee)`
direction = 1;
fee_factor = I80F48::ONE + perp_market.base_liquidation_fee;
let liab_price = perp_info.prices.liab(HealthType::LiquidationEnd);
unweighted_health_per_lot = liab_price * base_lot_size * perp_market.init_base_liab_weight
- oracle_price_per_lot * quote_init_liab_weight * fee_factor;
base_fee_factor = I80F48::ONE + perp_market.base_liquidation_fee;
uhupnl_per_lot =
oracle_price_per_lot * (perp_market.init_base_liab_weight - base_fee_factor);
};
assert!(unweighted_health_per_lot > 0);
assert!(uhupnl_per_lot > 0);
// Amount of settle token received for each token that is settled
let spot_gain_per_settled = I80F48::ONE - perp_market.positive_pnl_liquidation_fee;
let init_overall_asset_weight = perp_market.init_overall_asset_weight;
// The overall health contribution from perp including spot health increases from settling pnl.
// This is needed in order to reduce the base position the right amount when taking into
// account the settlement that will happen afterwards.
let expected_perp_health = |unweighted: I80F48| {
if unweighted < 0 {
unweighted
} else if unweighted < max_pnl_transfer {
unweighted * spot_gain_per_settled
} else {
let unsettled = unweighted - max_pnl_transfer;
max_pnl_transfer * spot_gain_per_settled + unsettled * init_overall_asset_weight
}
};
//
// Several steps of perp base position reduction will follow, and they'll update
// these variables
//
let mut base_reduction = 0;
let mut current_unweighted_perp_health =
perp_info.unweighted_health_contribution(HealthType::LiquidationEnd);
let initial_weighted_perp_health = perp_info
.weigh_health_contribution(current_unweighted_perp_health, HealthType::LiquidationEnd);
let mut current_expected_perp_health = expected_perp_health(current_unweighted_perp_health);
let mut current_expected_health =
liqee_liq_end_health + current_expected_perp_health - initial_weighted_perp_health;
let mut pnl_transfer = I80F48::ZERO;
let mut current_uhupnl = perp_info.unweighted_health_unsettled_pnl(liq_end_type);
let mut current_health = liqee_liq_end_health;
let mut current_settle_token = settle_token_balance.spot_and_perp;
// Helper function to reduce base position in exchange for effective settle token balance changes
//
// This function does only deal with reducing the base position and getting settle token balance
// in exchange. This exchange does not always lead to health improvements (like when the overall
// weight is 0 and the uhupnl is already positive). That's why we pass an "expected" gain per lot
// as well, which incorporates the fact that the increase in uhupnl can get settled later and
// becomes health then.
//
// This means that the amount of base_lots reduced by this function is determined in anticipation
// of future pnl settlement. It only changes the current_* args according to the actual reduction
// that happened. The settlement happens later, separately, in settle_pnl() below.
let mut reduce_base = |step: &str,
health_amount: I80F48,
health_per_lot: I80F48,
current_unweighted_perp_health: &mut I80F48| {
// How much are we willing to increase the unweighted perp health?
let health_limit = health_amount
.min(-current_expected_health)
.max(I80F48::ZERO);
uhupnl_limit: I80F48,
// How much effective settle token will be gained when taking future
// upnl settlement into account.
expected_settle_token_per_lot: I80F48,
// Effective settle token gained directly from the operation (before settlement)
actual_settle_token_per_lot: I80F48,
uhupnl_per_lot: I80F48,
current_uhupnl: &mut I80F48,
current_settle_token: &mut I80F48,
current_health: &mut I80F48| {
let max_settle_token_for_health = spot_amount_given_for_health_zero(
*current_health,
*current_settle_token,
settle_token_info.asset_weighted_price(liq_end_type),
settle_token_info.liab_weighted_price(liq_end_type),
)
.unwrap();
let max_settle_token = max_settle_token_for_health.min(uhupnl_limit);
// How many lots to transfer?
let base_lots = (health_limit / health_per_lot)
let base_lots = (max_settle_token / expected_settle_token_per_lot)
.ceil() // overshoot to aim for init_health >= 0
.to_num::<i64>()
.min(liqee_base_lots.abs() - base_reduction)
.min(max_base_transfer.abs() - base_reduction)
.max(0);
let unweighted_change = I80F48::from(base_lots) * unweighted_health_per_lot;
let current_unweighted = *current_unweighted_perp_health;
let new_unweighted_perp = current_unweighted + unweighted_change;
let new_expected_perp = expected_perp_health(new_unweighted_perp);
let new_expected_health =
current_expected_health + (new_expected_perp - current_expected_perp_health);
// Note, the expected health and settle token change is just for logging
let expected_settle_token_gain = expected_settle_token_per_lot * I80F48::from(base_lots);
let new_expected_settle_token = *current_settle_token + expected_settle_token_gain;
let new_expected_health = *current_health
- settle_token_info.health_contribution(liq_end_type, *current_settle_token)
+ settle_token_info.health_contribution(liq_end_type, new_expected_settle_token);
let uhupnl_gain = uhupnl_per_lot * I80F48::from(base_lots);
let new_uhupnl = *current_uhupnl + uhupnl_gain;
let new_settle_token =
*current_settle_token + actual_settle_token_per_lot * I80F48::from(base_lots);
let new_health = *current_health
- settle_token_info.health_contribution(liq_end_type, *current_settle_token)
+ settle_token_info.health_contribution(liq_end_type, new_settle_token);
msg!(
"{}: {} lots, health {} -> {}, unweighted perp {} -> {}",
"{} total: {} lots, health {} -> {}, exhealth -> {}, uhupnl: {} -> {}",
step,
base_lots,
current_expected_health,
new_expected_health,
current_unweighted,
new_unweighted_perp
// logging i64 is significantly cheaper
current_health.to_num::<i64>(),
new_health.to_num::<i64>(),
new_expected_health.to_num::<i64>(),
current_uhupnl.to_num::<i64>(),
new_uhupnl.to_num::<i64>(),
);
*current_settle_token = new_settle_token;
*current_health = new_health;
*current_uhupnl = new_uhupnl;
base_reduction += base_lots;
current_expected_health = new_expected_health;
*current_unweighted_perp_health = new_unweighted_perp;
current_expected_perp_health = new_expected_perp;
};
let settle_pnl = |step: &str,
settle_token_per_settle: I80F48,
pnl_transfer: &mut I80F48,
current_uhupnl: &mut I80F48,
current_settle_token: &mut I80F48,
current_health: &mut I80F48| {
let max_settle_token_for_health = spot_amount_given_for_health_zero(
*current_health,
*current_settle_token,
settle_token_info.asset_weighted_price(liq_end_type),
settle_token_info.liab_weighted_price(liq_end_type),
)
.unwrap();
// How many units to settle?
let settle = (max_settle_token_for_health / settle_token_per_settle)
.min(max_pnl_transfer - *pnl_transfer)
.min(*current_uhupnl)
.max(I80F48::ZERO);
let settle_token_gain = settle * settle_token_per_settle;
let new_settle_token = *current_settle_token + settle_token_gain;
let new_uhupnl = *current_uhupnl - settle;
let new_expected_health = *current_health
- settle_token_info.health_contribution(liq_end_type, *current_settle_token)
+ settle_token_info.health_contribution(liq_end_type, new_settle_token);
msg!(
"{}: {} settled, health {} -> {}, uhupnl: {} -> {}",
step,
settle.to_num::<i64>(),
current_health.to_num::<i64>(),
new_expected_health.to_num::<i64>(),
current_uhupnl.to_num::<i64>(),
new_uhupnl.to_num::<i64>(),
);
*current_settle_token = new_settle_token;
*current_health = new_expected_health;
*current_uhupnl = new_uhupnl;
*pnl_transfer += settle;
};
//
// Step 1: While the perp unsettled health is negative, any perp base position reduction
// directly increases it for the full amount.
//
if current_unweighted_perp_health < 0 {
if current_uhupnl < 0 {
reduce_base(
"negative",
-current_unweighted_perp_health,
unweighted_health_per_lot,
&mut current_unweighted_perp_health,
-current_uhupnl,
// for negative values uhupnl and hupnl are the same and increases
// directly improve health
uhupnl_per_lot,
uhupnl_per_lot,
uhupnl_per_lot,
&mut current_uhupnl,
&mut current_settle_token,
&mut current_health,
);
}
//
// Step 2: If perp unsettled health is positive but below max_settle, perp base position reductions
// Step 2: pnl settlement if uhupnl > 0
//
if current_uhupnl >= 0 && spot_gain_per_settled > init_overall_asset_weight {
// Settlement produces direct spot (after fees) and loses perp-positive-uhupnl weighted settle token pos
let settle_token_per_settle = spot_gain_per_settled - init_overall_asset_weight;
settle_pnl(
"pre-settle",
settle_token_per_settle,
&mut pnl_transfer,
&mut current_uhupnl,
&mut current_settle_token,
&mut current_health,
);
}
//
// Step 3: If perp unsettled health is positive but below max_settle, perp base position reductions
// benefit account health slightly less because of the settlement liquidation fee.
//
if current_unweighted_perp_health >= 0 && current_unweighted_perp_health < max_pnl_transfer {
let settled_health_per_lot = unweighted_health_per_lot * spot_gain_per_settled;
if current_uhupnl >= 0 && pnl_transfer < max_pnl_transfer {
reduce_base(
"settleable",
max_pnl_transfer - current_unweighted_perp_health,
settled_health_per_lot,
&mut current_unweighted_perp_health,
max_pnl_transfer - pnl_transfer,
uhupnl_per_lot * spot_gain_per_settled,
uhupnl_per_lot * init_overall_asset_weight,
uhupnl_per_lot,
&mut current_uhupnl,
&mut current_settle_token,
&mut current_health,
);
}
//
// Step 3: Above that, perp base positions only benefit account health if the pnl asset weight is positive
// Step 4: Again, pnl settlement if uhupnl > 0
//
if current_unweighted_perp_health >= max_pnl_transfer && init_overall_asset_weight > 0 {
let weighted_health_per_lot = unweighted_health_per_lot * init_overall_asset_weight;
if current_uhupnl >= 0 && spot_gain_per_settled > init_overall_asset_weight {
// Settlement produces direct spot (after fees) and loses perp-positive-uhupnl weighted settle token pos
let settle_token_per_settle = spot_gain_per_settled - init_overall_asset_weight;
settle_pnl(
"post-settle",
settle_token_per_settle,
&mut pnl_transfer,
&mut current_uhupnl,
&mut current_settle_token,
&mut current_health,
);
}
//
// Step 5: Above that, perp base positions only benefit account health if the pnl asset weight is positive
//
// Using -0.5 as a health target prevents rounding related issues. Any health >-1 will be enough
// to get the account out of being-liquidated state.
let health_target = I80F48::from_num(-0.5);
if current_health < health_target && init_overall_asset_weight > 0 {
let settle_token_per_lot = uhupnl_per_lot * init_overall_asset_weight;
reduce_base(
"positive",
I80F48::MAX,
weighted_health_per_lot,
&mut current_unweighted_perp_health,
settle_token_per_lot,
settle_token_per_lot,
uhupnl_per_lot,
&mut current_uhupnl,
&mut current_settle_token,
&mut current_health,
);
}
@ -385,8 +529,9 @@ pub(crate) fn liquidation_action(
// Execute the base reduction. This is essentially a forced trade and updates the
// liqee and liqors entry and break even prices.
//
assert!(base_reduction <= liqee_base_lots.abs());
let base_transfer = direction * base_reduction;
let quote_transfer = -I80F48::from(base_transfer) * oracle_price_per_lot * fee_factor;
let quote_transfer = -I80F48::from(base_transfer) * oracle_price_per_lot * base_fee_factor;
if base_transfer != 0 {
msg!(
"transfering: {} base lots and {} quote",
@ -398,20 +543,10 @@ pub(crate) fn liquidation_action(
}
//
// Step 4: Let the liqor take over positive pnl until the account health is positive,
// but only while the unweighted perp health is positive (otherwise it would decrease liqee health!)
// Let the liqor take over positive pnl until the account health is positive,
// but only while the health_unsettled_pnl is positive (otherwise it would decrease liqee health!)
//
let final_weighted_perp_health = perp_info
.weigh_health_contribution(current_unweighted_perp_health, HealthType::LiquidationEnd);
let current_actual_health =
liqee_liq_end_health - initial_weighted_perp_health + final_weighted_perp_health;
let pnl_transfer_possible =
current_actual_health < 0 && current_unweighted_perp_health > 0 && max_pnl_transfer > 0;
let (pnl_transfer, limit_transfer) = if pnl_transfer_possible {
let health_per_transfer = spot_gain_per_settled - init_overall_asset_weight;
let transfer_for_zero = (-current_actual_health / health_per_transfer).ceil();
let liqee_pnl = liqee_perp_position.unsettled_pnl(&perp_market, oracle_price)?;
let limit_transfer = if pnl_transfer > 0 {
// Allow taking over *more* than the liqee_positive_settle_limit. In exchange, the liqor
// also can't settle fully immediately and just takes over a fractional chunk of the limit.
//
@ -419,14 +554,12 @@ pub(crate) fn liquidation_action(
// base position to zero and would need to deal with that in bankruptcy. Also, the settle
// limit changes with the base position price, so it'd be hard to say when this liquidation
// step is done.
let pnl_transfer = liqee_pnl
.min(max_pnl_transfer)
.min(transfer_for_zero)
.min(current_unweighted_perp_health)
.max(I80F48::ZERO);
let limit_transfer = {
// take care, liqee_limit may be i64::MAX
let liqee_limit: i128 = liqee_positive_settle_limit.into();
let liqee_pnl = liqee_perp_position
.unsettled_pnl(perp_market, oracle_price)?
.max(I80F48::ONE);
let settle = pnl_transfer.floor().to_num::<i128>();
let total = liqee_pnl.ceil().to_num::<i128>();
let liqor_limit: i64 = (liqee_limit * settle / total).try_into().unwrap();
@ -436,24 +569,23 @@ pub(crate) fn liquidation_action(
// The liqor pays less than the full amount to receive the positive pnl
let token_transfer = pnl_transfer * spot_gain_per_settled;
if pnl_transfer > 0 {
liqor_perp_position.record_liquidation_pnl_takeover(pnl_transfer, limit_transfer);
liqee_perp_position.record_settle(pnl_transfer);
liqor_perp_position.record_liquidation_pnl_takeover(pnl_transfer, limit_transfer);
liqee_perp_position.record_settle(pnl_transfer);
// Update the accounts' perp_spot_transfer statistics.
let transfer_i64 = token_transfer.round_to_zero().to_num::<i64>();
liqor_perp_position.perp_spot_transfers -= transfer_i64;
liqee_perp_position.perp_spot_transfers += transfer_i64;
liqor.fixed.perp_spot_transfers -= transfer_i64;
liqee.fixed.perp_spot_transfers += transfer_i64;
// Update the accounts' perp_spot_transfer statistics.
let transfer_i64 = token_transfer.round_to_zero().to_num::<i64>();
liqor_perp_position.perp_spot_transfers -= transfer_i64;
liqee_perp_position.perp_spot_transfers += transfer_i64;
liqor.fixed.perp_spot_transfers -= transfer_i64;
liqee.fixed.perp_spot_transfers += transfer_i64;
// 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(liqee_token_position, token_transfer, now_ts)?;
settle_bank.withdraw_without_fee(liqor_token_position, token_transfer, now_ts)?;
liqee_health_cache.adjust_token_balance(&settle_bank, token_transfer)?;
// 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(liqee_token_position, token_transfer, now_ts)?;
settle_bank.withdraw_without_fee(liqor_token_position, token_transfer, now_ts)?;
liqee_health_cache.adjust_token_balance(&settle_bank, token_transfer)?;
}
msg!(
"pnl {} was transferred to liqor for quote {} with settle limit {}",
pnl_transfer,
@ -461,9 +593,9 @@ pub(crate) fn liquidation_action(
limit_transfer
);
(pnl_transfer, limit_transfer)
limit_transfer
} else {
(I80F48::ZERO, I80F48::ZERO)
I80F48::ZERO
};
let liqee_perp_position = liqee.perp_position_mut(perp_market_index)?;
@ -482,6 +614,8 @@ mod tests {
group: Pubkey,
settle_bank: TestAccount<Bank>,
settle_oracle: TestAccount<StubOracle>,
other_bank: TestAccount<Bank>,
other_oracle: TestAccount<StubOracle>,
perp_market: TestAccount<PerpMarket>,
perp_oracle: TestAccount<StubOracle>,
liqee: MangoAccountValue,
@ -492,6 +626,7 @@ mod tests {
fn new() -> Self {
let group = Pubkey::new_unique();
let (settle_bank, settle_oracle) = mock_bank_and_oracle(group, 0, 1.0, 0.0, 0.0);
let (other_bank, other_oracle) = mock_bank_and_oracle(group, 1, 1.0, 0.0, 0.0);
let (_bank2, perp_oracle) = mock_bank_and_oracle(group, 4, 1.0, 0.5, 0.3);
let mut perp_market =
mock_perp_market(group, perp_oracle.pubkey, 1.0, 9, (0.2, 0.1), (0.05, 0.02));
@ -501,6 +636,7 @@ mod tests {
let mut liqee = MangoAccountValue::from_bytes(&liqee_buffer).unwrap();
{
liqee.ensure_token_position(0).unwrap();
liqee.ensure_token_position(1).unwrap();
liqee.ensure_perp_position(9, 0).unwrap();
}
@ -508,6 +644,7 @@ mod tests {
let mut liqor = MangoAccountValue::from_bytes(&liqor_buffer).unwrap();
{
liqor.ensure_token_position(0).unwrap();
liqor.ensure_token_position(1).unwrap();
liqor.ensure_perp_position(9, 0).unwrap();
}
@ -515,6 +652,8 @@ mod tests {
group,
settle_bank,
settle_oracle,
other_bank,
other_oracle,
perp_market,
perp_oracle,
liqee,
@ -527,7 +666,9 @@ mod tests {
let ais = vec![
setup.settle_bank.as_account_info(),
setup.other_bank.as_account_info(),
setup.settle_oracle.as_account_info(),
setup.other_oracle.as_account_info(),
setup.perp_market.as_account_info(),
setup.perp_oracle.as_account_info(),
];
@ -562,6 +703,9 @@ mod tests {
fn token_p(account: &mut MangoAccountValue) -> &mut TokenPosition {
account.token_position_mut(0).unwrap().0
}
fn other_p(account: &mut MangoAccountValue) -> &mut TokenPosition {
account.token_position_mut(1).unwrap().0
}
fn perp_p(account: &mut MangoAccountValue) -> &mut PerpPosition {
account.perp_position_mut(9).unwrap()
}
@ -580,8 +724,8 @@ mod tests {
let test_cases = vec![
(
"nothing",
(0.9, 0.9),
(0.0, 0, 0.0),
(0.9, 0.9, 0.0),
(0.0, 0, 0.0, 0.0),
(0.0, 0, 0.0),
(0, 100),
),
@ -590,43 +734,43 @@ mod tests {
//
(
"neg base liq 1: limited",
(0.5, 0.5),
(5.0, -10, 0.0),
(0.5, 0.5, 0.0),
(5.0, -10, 0.0, 0.0),
(5.0, -9, -1.0),
(-1, 100),
),
(
"neg base liq 2: base to zero",
(0.5, 0.5),
(5.0, -10, 0.0),
(0.5, 0.5, 0.0),
(5.0, -10, 0.0, 0.0),
(5.0, 0, -10.0),
(-20, 100),
),
(
"neg base liq 3: health positive",
(0.5, 0.5),
(5.0, -4, 0.0),
(0.5, 0.5, 0.0),
(5.0, -4, 0.0, 0.0),
(5.0, -2, -2.0),
(-20, 100),
),
(
"pos base liq 1: limited",
(0.5, 0.5),
(5.0, 20, -20.0),
(0.5, 0.5, 0.0),
(5.0, 20, -20.0, 0.0),
(5.0, 19, -19.0),
(1, 100),
),
(
"pos base liq 2: base to zero",
(0.5, 0.5),
(0.0, 20, -30.0),
(0.5, 0.5, 0.0),
(0.0, 20, -30.0, 0.0),
(0.0, 0, -10.0),
(100, 100),
),
(
"pos base liq 3: health positive",
(0.5, 0.5),
(5.0, 20, -20.0),
(0.5, 0.5, 0.0),
(5.0, 20, -20.0, 0.0),
(5.0, 10, -10.0),
(100, 100),
),
@ -635,57 +779,57 @@ mod tests {
//
(
"base liq, pos perp health 1: until health positive",
(0.5, 1.0),
(-20.0, 20, 5.0),
(-20.0, 10, 15.0),
(0.5, 0.8, 0.0),
(-20.0, 20, 5.0, 0.0),
(0.0, 10, -5.0), // alternate: (-20, 0, 25). would it be better to reduce base more instead?
(100, 100),
),
(
"base liq, pos perp health 2-1: settle until health positive",
(0.5, 0.5),
(-19.0, 20, 10.0),
(0.5, 0.5, 0.0),
(-19.0, 20, 10.0, 0.0),
(-1.0, 20, -8.0),
(100, 100),
),
(
"base liq, pos perp health 2-2: base+settle until health positive",
(0.5, 0.5),
(-25.0, 20, 10.0),
(0.0, 10, -5.0),
(0.5, 0.5, 0.0),
(-25.0, 20, 10.0, 0.0),
(0.0, 10, -5.0), // alternate: (-5, 0, 10) better?
(100, 100),
),
(
"base liq, pos perp health 2-3: base+settle until pnl limit",
(0.5, 0.5),
(-23.0, 20, 10.0),
(0.5, 0.5, 0.0),
(-23.0, 20, 10.0, 0.0),
(-2.0, 10, -1.0),
(100, 21),
),
(
"base liq, pos perp health 2-4: base+settle until base limit",
(0.5, 0.5),
(-25.0, 20, 10.0),
(0.5, 0.5, 0.0),
(-25.0, 20, 10.0, 0.0),
(-4.0, 18, -9.0),
(2, 100),
),
(
"base liq, pos perp health 2-5: base+settle until both limits",
(0.5, 0.5),
(-25.0, 20, 10.0),
(0.5, 0.5, 0.0),
(-25.0, 20, 10.0, 0.0),
(-4.0, 16, -7.0),
(4, 21),
),
(
"base liq, pos perp health 4: liq some base, then settle some",
(0.5, 0.5),
(-20.0, 20, 10.0),
(0.5, 0.5, 0.0),
(-20.0, 20, 10.0, 0.0),
(-15.0, 10, 15.0),
(10, 5),
),
(
"base liq, pos perp health 5: base to zero even without settlement",
(0.5, 0.5),
(-20.0, 20, 10.0),
(0.5, 0.5, 0.0),
(-20.0, 20, 10.0, 0.0),
(-20.0, 0, 30.0),
(100, 0),
),
@ -694,39 +838,141 @@ mod tests {
//
(
"base liq, pos perp health 6: don't touch base without settlement",
(0.5, 0.0),
(-20.0, 20, 10.0),
(0.5, 0.0, 0.0),
(-20.0, 20, 10.0, 0.0),
(-20.0, 20, 10.0),
(10, 0),
),
(
"base liq, pos perp health 7: settlement without base",
(0.5, 0.0),
(-20.0, 20, 10.0),
(0.5, 0.0, 0.0),
(-20.0, 20, 10.0, 0.0),
(-15.0, 20, 5.0),
(10, 5),
),
(
"base liq, pos perp health 8: settlement enables base",
(0.5, 0.0),
(-30.0, 20, 10.0),
(0.5, 0.0, 0.0),
(-30.0, 20, 10.0, 0.0),
(-7.5, 15, -7.5),
(5, 30),
),
(
"base liq, pos perp health 9: until health positive",
(0.5, 0.0),
(-25.0, 20, 10.0),
(0.5, 0.0, 0.0),
(-25.0, 20, 10.0, 0.0),
(0.0, 10, -5.0),
(200, 200),
),
//
// Liquidation in cases where another token contributes to health too
//
(
"base liq of negative perp health: where total token pos goes from negative to positive",
(0.5, 0.0, 0.0),
(2.0, 60, -35.0, -2.0), // settle token: 2 + (60/2 - 35) = -3
(2.0, 50, -25.0), // settle token: 2 + (50/2 - 25) = 2
(200, 200),
),
(
"pre-settle: where total token pos goes from negative to positive",
(0.5, 0.0, 0.0),
(-7.0, 60, -20.0, -3.0), // settle token: -7 + 0.0 * (60/2 - 20) = -7
(3.0, 60, -30.0), // settle token: 3 + 0.0 * (60/2 - 30) = 3
(200, 200),
),
(
"base liq and post-settle: where total token pos goes from negative to positive",
(0.5, 0.0, 0.0),
(-7.0, 60, -30.0, -3.0), // settle token: -7 + 0.0 * (60/2 - 30) = -7
(3.0, 40, -20.0), // settle token: 3 + 0.0 * (40/2 - 20) = 3
(200, 200),
),
(
"base liq of positive perp health: where total token pos goes from negative to positive",
(0.5, 0.5, 0.0),
(-7.0, 60, -20.0, -3.0), // settle token: -7 + 0.5 * (60/2 - 20) = -2
(-7.0, 40, 0.0), // settle token: -7 + 0.5 * (40/2 - 0) = 3
(200, 0),
),
(
"use all liquidation phases 1",
(0.5, 0.5, 0.0),
(-7.0, 70, -40.0, -100.0),
// reduce 10
// no pre-settle
// reduce 20 + post-settle 10
// reduce 40
(3.0, 0, 20.0),
(200, 10),
),
(
"use all liquidation phases 2",
(0.5, 0.5, 0.0),
(-7.0, 70, -30.0, -100.0),
// no "reduce while health negative"
// pre-settle 5
// reduce 20 + post-settle 10
// reduce 40
(8.0, 10, 15.0),
(60, 15),
),
//
// Involving a settle fee
//
(
"settle fee 1",
(0.5, 0.5, 0.25),
(-7.0, 70, -30.0, -100.0),
// no "reduce while health negative"
// pre-settle 5
// reduce 40 + post-settle 15
// reduce 20
(8.0, 10, 10.0), // spot increased only by 20 * (1-0.25)
(60, 20),
),
(
"settle fee 2",
(0.5, 0.5, 0.25),
(-7.0, 70, -30.0, -12.0),
// no "reduce while health negative"
// pre-settle 5
// reduce 40 + post-settle 15
// reduce 6
(8.0, 24, -4.0), // 8 + (24*0.5 - 4)*0.5 = 12
(60, 20),
),
(
"settle fee 3",
(0.5, 0.5, 0.25),
(-7.0, 70, -30.0, -6.0),
// no "reduce while health negative"
// pre-settle 5
// reduce 25 + post-settle 12
(-7.0 + 12.75, 45, -22.0), // 5.75 + (45*0.5 - 22)*0.5 = 6
(60, 20),
),
(
"settle fee 4",
(0.5, 0.5, 0.25),
(-7.0, 70, -30.0, 4.0),
// no "reduce while health negative"
// pre-settle 2
(-7.0 + 1.5, 70, -32.0), // -5.5 + (70*0.5 - 32)*0.5 = -4
(60, 20),
),
];
for (
name,
(base_weight, overall_weight),
(init_liqee_spot, init_liqee_base, init_liqee_quote),
// the perp base asset weight (liab is symmetric) and the perp overall asset weight to use
(base_weight, overall_weight, pos_pnl_liq_fee),
// the liqee's starting point: (-5, 10, -5, 1) would mean:
// USDC: -5, perp base: 10, perp quote -5, other token: 1 (which also has weight/price 1)
(init_liqee_spot, init_liqee_base, init_liqee_quote, init_other_spot),
// the expected liqee end position
(exp_liqee_spot, exp_liqee_base, exp_liqee_quote),
// maximum liquidation the liqor requests
(max_base, max_pnl),
) in test_cases
{
@ -737,6 +983,7 @@ mod tests {
pm.init_base_asset_weight = I80F48::from_num(base_weight);
pm.init_base_liab_weight = I80F48::from_num(2.0 - base_weight);
pm.init_overall_asset_weight = I80F48::from_num(overall_weight);
pm.positive_pnl_liquidation_fee = I80F48::from_num(pos_pnl_liq_fee);
}
{
let p = perp_p(&mut setup.liqee);
@ -760,6 +1007,15 @@ mod tests {
settle_bank
.change_without_fee(token_p(&mut setup.liqor), I80F48::from_num(1000.0), 0)
.unwrap();
let other_bank = setup.other_bank.data();
other_bank
.change_without_fee(
other_p(&mut setup.liqee),
I80F48::from_num(init_other_spot),
0,
)
.unwrap();
}
let mut result = setup.run(max_base, max_pnl).unwrap();
@ -767,6 +1023,7 @@ mod tests {
let liqee_perp = perp_p(&mut result.liqee);
assert_eq!(liqee_perp.base_position_lots(), exp_liqee_base);
assert_eq_f!(liqee_perp.quote_position_native(), exp_liqee_quote, 0.01);
let liqor_perp = perp_p(&mut result.liqor);
assert_eq!(
liqor_perp.base_position_lots(),

View File

@ -1,29 +1,57 @@
use std::ops::DerefMut;
use anchor_lang::prelude::*;
use anchor_spl::token;
use anchor_spl::token::{self, TokenAccount};
use fixed::types::I80F48;
use crate::accounts_ix::*;
use crate::accounts_zerocopy::AccountInfoRef;
use crate::error::*;
use crate::health::{compute_health, new_health_cache, HealthType, ScanningAccountRetriever};
use crate::health::*;
use crate::logs::{
emit_perp_balances, PerpLiqBankruptcyLog, PerpLiqNegativePnlOrBankruptcyLog, TokenBalanceLog,
};
use crate::state::*;
pub fn perp_liq_negative_pnl_or_bankruptcy(
ctx: Context<PerpLiqNegativePnlOrBankruptcy>,
ctx: Context<PerpLiqNegativePnlOrBankruptcyV2>,
max_liab_transfer: u64,
) -> Result<()> {
let mango_group = ctx.accounts.group.key();
let (perp_market_index, settle_token_index) = {
let now_slot = Clock::get()?.slot;
let now_ts = Clock::get()?.unix_timestamp.try_into().unwrap();
let perp_market_index;
let settle_token_index;
let perp_oracle_price;
let settle_token_oracle_price;
let insurance_token_oracle_price;
{
let perp_market = ctx.accounts.perp_market.load()?;
(
perp_market.perp_market_index,
perp_market.settle_token_index,
)
};
perp_market_index = perp_market.perp_market_index;
settle_token_index = perp_market.settle_token_index;
perp_oracle_price = perp_market.oracle_price(
&AccountInfoRef::borrow(&ctx.accounts.oracle)?,
Some(now_slot),
)?;
let settle_bank = ctx.accounts.settle_bank.load()?;
settle_token_oracle_price = settle_bank.oracle_price(
&AccountInfoRef::borrow(&ctx.accounts.settle_oracle)?,
Some(now_slot),
)?;
drop(settle_bank); // could be the same as insurance_bank
let insurance_bank = ctx.accounts.insurance_bank.load()?;
// We're not getting the insurance token price from the HealthCache because
// the liqee isn't guaranteed to have an insurance fund token position.
insurance_token_oracle_price = insurance_bank.oracle_price(
&AccountInfoRef::borrow(&ctx.accounts.insurance_oracle)?,
Some(now_slot),
)?;
}
require_keys_neq!(ctx.accounts.liqor.key(), ctx.accounts.liqee.key());
let mut liqee = ctx.accounts.liqee.load_full_mut()?;
@ -41,13 +69,13 @@ pub fn perp_liq_negative_pnl_or_bankruptcy(
"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 retriever = ScanningAccountRetriever::new(ctx.remaining_accounts, &mango_group)
.context("create account retriever")?;
let mut liqee_health_cache = new_health_cache(&liqee.borrow(), &retriever)?;
drop(retriever);
let liqee_liq_end_health = liqee_health_cache.health(HealthType::LiquidationEnd);
let liqee_settle_health = liqee_health_cache.perp_settle_health();
// Guarantees that perp base position is 0 and perp quote position is <= 0.
liqee_health_cache.require_after_phase2_liquidation()?;
if liqee.check_liquidatable(&liqee_health_cache)? != CheckLiquidatable::Liquidatable {
@ -62,180 +90,51 @@ pub fn perp_liq_negative_pnl_or_bankruptcy(
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 = liqee_health_cache
.perp_info(perp_market_index)?
.prices
.oracle;
let settle_token_oracle_price = liqee_health_cache
.token_info(settle_token_index)?
.prices
.oracle;
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.unsettled_pnl(&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().to_num::<i64>();
liqor_perp_position.perp_spot_transfers += settlement_i64;
liqee_perp_position.perp_spot_transfers -= settlement_i64;
liqor.fixed.perp_spot_transfers += settlement_i64;
liqee.fixed.perp_spot_transfers -= settlement_i64;
// 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)?;
liqee_health_cache.adjust_token_balance(&settle_bank, -settlement)?;
emit!(PerpLiqNegativePnlOrBankruptcyLog {
mango_group,
liqee: ctx.accounts.liqee.key(),
liqor: ctx.accounts.liqor.key(),
perp_market_index,
settlement: settlement.to_bits(),
});
msg!("liquidated pnl = {}", settlement);
}
};
let max_liab_transfer = 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.unsettled_pnl(&perp_market, oracle_price)?;
let max_liab_transfer_from_liqee =
(-liqee_pnl).min(-liqee_liq_end_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 = I80F48::ONE + perp_market.base_liquidation_fee;
// Amount given to the liqor from the insurance fund
let insurance_transfer = (liab_transfer * liquidation_fee_factor)
.ceil()
.to_num::<u64>()
.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 =
(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
let (settlement, insurance_transfer) = {
let mut settle_bank = ctx.accounts.settle_bank.load_mut()?;
let mut insurance_bank_opt =
if ctx.accounts.settle_bank.key() != ctx.accounts.insurance_bank.key() {
Some(ctx.accounts.insurance_bank.load_mut()?)
} else {
None
};
liquidation_action(
ctx.accounts.group.key(),
&mut perp_market,
perp_oracle_price,
&mut settle_bank,
settle_token_oracle_price,
insurance_bank_opt.as_mut().map(|v| v.deref_mut()),
insurance_token_oracle_price,
&ctx.accounts.insurance_vault,
&mut liqor.borrow_mut(),
ctx.accounts.liqor.key(),
&mut liqee.borrow_mut(),
ctx.accounts.liqee.key(),
&mut liqee_health_cache,
liqee_liq_end_health,
now_ts,
max_liab_transfer,
)?
};
// Execute the insurance fund transfer if needed
if insurance_transfer > 0 {
let group = ctx.accounts.group.load()?;
let group_seeds = group_seeds!(group);
token::transfer(
ctx.accounts.transfer_ctx().with_signer(&[group_seeds]),
insurance_transfer,
)?;
}
//
// Log positions aftewards
// Log positions afterwards
//
if settlement > 0 || insurance_transfer > 0 {
if settlement > 0 {
let settle_bank = ctx.accounts.settle_bank.load()?;
let liqor_token_position = liqor.token_position(settle_token_index)?;
emit!(TokenBalanceLog {
mango_group,
@ -245,9 +144,7 @@ pub fn perp_liq_negative_pnl_or_bankruptcy(
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,
@ -259,6 +156,19 @@ pub fn perp_liq_negative_pnl_or_bankruptcy(
});
}
if insurance_transfer > 0 {
let insurance_bank = ctx.accounts.insurance_bank.load()?;
let liqor_token_position = liqor.token_position(insurance_bank.token_index)?;
emit!(TokenBalanceLog {
mango_group,
mango_account: ctx.accounts.liqor.key(),
token_index: insurance_bank.token_index,
indexed_position: liqor_token_position.indexed_position.to_bits(),
deposit_index: insurance_bank.deposit_index.to_bits(),
borrow_index: insurance_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(
@ -282,7 +192,6 @@ pub fn perp_liq_negative_pnl_or_bankruptcy(
.maybe_recover_from_being_liquidated(liqee_liq_end_health);
drop(perp_market);
drop(settle_bank);
// Check liqor's health
if !liqor.fixed.is_in_health_region() {
@ -295,3 +204,606 @@ pub fn perp_liq_negative_pnl_or_bankruptcy(
Ok(())
}
pub(crate) fn liquidation_action(
group_key: Pubkey,
perp_market: &mut PerpMarket,
perp_oracle_price: I80F48,
settle_bank: &mut Bank,
settle_token_oracle_price: I80F48,
insurance_bank_opt: Option<&mut Bank>,
insurance_token_oracle_price: I80F48,
insurance_vault: &TokenAccount,
liqor: &mut MangoAccountRefMut,
liqor_key: Pubkey,
liqee: &mut MangoAccountRefMut,
liqee_key: Pubkey,
liqee_health_cache: &mut HealthCache,
liqee_liq_end_health: I80F48,
now_ts: u64,
max_liab_transfer: u64,
) -> Result<(I80F48, u64)> {
let perp_market_index = perp_market.perp_market_index;
let settle_token_index = perp_market.settle_token_index;
let liqee_max_settle = liqee_health_cache.perp_max_settle(settle_token_index)?;
let liqee_health_token_balances =
liqee_health_cache.effective_token_balances(HealthType::LiquidationEnd);
//
// Step 1: Allow the liqor to take over ("settle") negative liqee pnl.
//
// The only limitation is the liqee's perp_max_settle and its perp pnl settle limit.
// This does not change liqee health.
//
let settlement;
let max_settlement_liqee;
let mut liqee_pnl;
{
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);
liqee_pnl = liqee_perp_position.unsettled_pnl(&perp_market, perp_oracle_price)?;
require_gt!(0, liqee_pnl, 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_max_settle
.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().to_num::<i64>();
liqor_perp_position.perp_spot_transfers += settlement_i64;
liqee_perp_position.perp_spot_transfers -= settlement_i64;
liqor.fixed.perp_spot_transfers += settlement_i64;
liqee.fixed.perp_spot_transfers -= settlement_i64;
// 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)?;
liqee_health_cache.adjust_token_balance(&settle_bank, -settlement)?;
emit!(PerpLiqNegativePnlOrBankruptcyLog {
mango_group: group_key,
liqee: liqee_key,
liqor: liqor_key,
perp_market_index,
settlement: settlement.to_bits(),
});
liqee_pnl += settlement;
msg!("liquidated pnl = {}", settlement);
}
};
let max_liab_transfer = I80F48::from(max_liab_transfer) - settlement;
// Step 2: bankruptcy
//
// If the liqee still has negative pnl and couldn't possibly be settled further, allow bankruptcy
// to reduce the negative pnl.
//
// 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 (this does not involve the liqor)
//
let insurance_transfer;
if settlement == max_settlement_liqee && liqee_pnl < 0 {
// Preparation that's needed for both, insurance fund based pnl takeover and socialized loss
let liqee_settle_token_balance = liqee_health_token_balances
[liqee_health_cache.token_info_index(settle_token_index)?]
.spot_and_perp;
let liqee_perp_position = liqee.perp_position_mut(perp_market_index)?;
// recompute for safety
liqee_pnl = liqee_perp_position.unsettled_pnl(&perp_market, perp_oracle_price)?;
// Each unit of pnl increase (towards 0) increases health, but the amount depends on whether
// the health token position is negative or positive.
// Compute how much pnl would need to be increased to reach liq end health 0 (while ignoring
// liqee_pnl and other constraints initially, those are applied below)
let max_for_health = {
let liab_weighted_price = settle_token_oracle_price * settle_bank.init_liab_weight;
let asset_weighted_price = settle_token_oracle_price * settle_bank.init_asset_weight;
spot_amount_given_for_health_zero(
liqee_liq_end_health,
liqee_settle_token_balance,
asset_weighted_price,
liab_weighted_price,
)?
};
let max_liab_transfer_from_liqee = (-liqee_pnl).min(max_for_health).max(I80F48::ZERO);
let max_liab_transfer_to_liqor = max_liab_transfer_from_liqee
.min(max_liab_transfer)
.max(I80F48::ZERO);
// Check if the insurance fund can be used to reimburse the liqor for taking on negative pnl
// Available insurance fund coverage
let insurance_vault_amount = if perp_market.elligible_for_group_insurance_fund() {
insurance_vault.amount
} else {
0
};
let liquidation_fee_factor = I80F48::ONE + perp_market.base_liquidation_fee;
let settle_token_price_with_fee = settle_token_oracle_price * liquidation_fee_factor;
// Amount given to the liqor from the insurance fund
insurance_transfer = (max_liab_transfer_to_liqor * settle_token_price_with_fee
/ insurance_token_oracle_price)
.ceil()
.to_num::<u64>()
.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 = (insurance_transfer_i80f48 * insurance_token_oracle_price
/ settle_token_price_with_fee)
.min(max_liab_transfer_to_liqor);
// Try using the insurance fund if possible
if insurance_transfer > 0 {
let insurance_bank = insurance_bank_opt.unwrap_or(settle_bank);
require_keys_eq!(insurance_bank.mint, insurance_vault.mint);
// moving insurance assets into the insurance bank vault happens outside
// of this function to ensure this is unittestable!
// credit the liqor with quote tokens
let (liqor_quote, _, _) = liqor.ensure_token_position(insurance_bank.token_index)?;
insurance_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);
msg!(
"bankruptcy: {} pnl for {} insurance",
insurance_liab_transfer,
insurance_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;
msg!("socialized loss: {}", socialized_loss);
}
emit!(PerpLiqBankruptcyLog {
mango_group: group_key,
liqee: liqee_key,
liqor: 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(),
});
} else {
insurance_transfer = 0;
};
Ok((settlement, insurance_transfer))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::health::{self, test::*};
#[derive(Clone)]
struct TestSetup {
group: Pubkey,
insurance_bank: TestAccount<Bank>,
settle_bank: TestAccount<Bank>,
other_bank: TestAccount<Bank>,
insurance_oracle: TestAccount<StubOracle>,
settle_oracle: TestAccount<StubOracle>,
other_oracle: TestAccount<StubOracle>,
perp_market: TestAccount<PerpMarket>,
perp_oracle: TestAccount<StubOracle>,
liqee: MangoAccountValue,
liqor: MangoAccountValue,
insurance_vault: spl_token::state::Account,
}
impl TestSetup {
fn new() -> Self {
let group = Pubkey::new_unique();
let (mut insurance_bank, insurance_oracle) =
mock_bank_and_oracle(group, 0, 1.0, 0.0, 0.0);
let (settle_bank, settle_oracle) = mock_bank_and_oracle(group, 1, 1.0, 0.0, 0.0);
let (_bank3, perp_oracle) = mock_bank_and_oracle(group, 4, 1.0, 0.5, 0.3);
let mut perp_market =
mock_perp_market(group, perp_oracle.pubkey, 1.0, 9, (0.2, 0.1), (0.2, 0.1));
perp_market.data().settle_token_index = 1;
perp_market.data().base_lot_size = 1;
perp_market.data().group_insurance_fund = 1;
let (other_bank, other_oracle) = mock_bank_and_oracle(group, 2, 1.0, 0.0, 0.0);
let liqee_buffer = MangoAccount::default_for_tests().try_to_vec().unwrap();
let mut liqee = MangoAccountValue::from_bytes(&liqee_buffer).unwrap();
{
liqee.ensure_token_position(1).unwrap();
liqee.ensure_token_position(2).unwrap();
liqee.ensure_perp_position(9, 1).unwrap();
}
let liqor_buffer = MangoAccount::default_for_tests().try_to_vec().unwrap();
let mut liqor = MangoAccountValue::from_bytes(&liqor_buffer).unwrap();
{
liqor.ensure_token_position(0).unwrap();
liqor.ensure_token_position(1).unwrap();
liqor.ensure_perp_position(9, 1).unwrap();
}
let mut insurance_vault = spl_token::state::Account::default();
insurance_vault.state = spl_token::state::AccountState::Initialized;
insurance_vault.mint = insurance_bank.data().mint;
Self {
group,
insurance_bank,
settle_bank,
other_bank,
insurance_oracle,
settle_oracle,
other_oracle,
perp_market,
perp_oracle,
liqee,
liqor,
insurance_vault,
}
}
fn run(&self, max_liab_transfer: u64) -> Result<Self> {
let mut setup = self.clone();
let mut liqee_health_cache;
let liqee_liq_end_health;
{
let ais = vec![
setup.insurance_bank.as_account_info(),
setup.settle_bank.as_account_info(),
setup.other_bank.as_account_info(),
setup.insurance_oracle.as_account_info(),
setup.settle_oracle.as_account_info(),
setup.other_oracle.as_account_info(),
setup.perp_market.as_account_info(),
setup.perp_oracle.as_account_info(),
];
let retriever =
ScanningAccountRetriever::new_with_staleness(&ais, &setup.group, None).unwrap();
liqee_health_cache =
health::new_health_cache(&setup.liqee.borrow(), &retriever).unwrap();
liqee_liq_end_health = liqee_health_cache.health(HealthType::LiquidationEnd);
}
let insurance_oracle_ai = setup.insurance_oracle.as_account_info();
let settle_oracle_ai = setup.settle_oracle.as_account_info();
let perp_oracle_ai = setup.perp_oracle.as_account_info();
let insurance_price = setup
.insurance_bank
.data()
.oracle_price(&AccountInfoRef::borrow(&insurance_oracle_ai).unwrap(), None)
.unwrap();
let settle_price = setup
.settle_bank
.data()
.oracle_price(&AccountInfoRef::borrow(&settle_oracle_ai).unwrap(), None)
.unwrap();
let perp_price = setup
.perp_market
.data()
.oracle_price(&AccountInfoRef::borrow(&perp_oracle_ai).unwrap(), None)
.unwrap();
// There's no way to construct a TokenAccount directly...
let mut buffer = [0u8; 165];
use solana_program::program_pack::Pack;
setup.insurance_vault.pack_into_slice(&mut buffer);
let insurance_vault =
TokenAccount::try_deserialize_unchecked(&mut &buffer[..]).unwrap();
liquidation_action(
setup.group.key(),
setup.perp_market.data(),
perp_price,
setup.settle_bank.data(),
settle_price,
Some(setup.insurance_bank.data()),
insurance_price,
&insurance_vault,
&mut setup.liqor.borrow_mut(),
Pubkey::new_unique(),
&mut setup.liqee.borrow_mut(),
Pubkey::new_unique(),
&mut liqee_health_cache,
liqee_liq_end_health,
0,
max_liab_transfer,
)?;
Ok(setup)
}
}
fn insurance_p(account: &mut MangoAccountValue) -> &mut TokenPosition {
account.token_position_mut(0).unwrap().0
}
fn settle_p(account: &mut MangoAccountValue) -> &mut TokenPosition {
account.token_position_mut(1).unwrap().0
}
fn other_p(account: &mut MangoAccountValue) -> &mut TokenPosition {
account.token_position_mut(2).unwrap().0
}
fn perp_p(account: &mut MangoAccountValue) -> &mut PerpPosition {
account.perp_position_mut(9).unwrap()
}
macro_rules! assert_eq_f {
($value:expr, $expected:expr, $max_error:expr) => {
let value = $value;
let expected = $expected;
let ok = (value.to_num::<f64>() - expected).abs() < $max_error;
assert!(ok, "value: {value}, expected: {expected}");
};
}
#[test]
fn test_liq_negative_pnl_or_bankruptcy() {
let test_cases = vec![
(
"nothing",
(0.9, 1.0, 1.0),
(0.0, 0.0, 0.0, 0, 0),
(false, 0.0, 0.0),
(0.0, 0.0, 0.0, 0.0),
100,
),
(
"settle 1",
(0.9, 2.0, 3.0),
// perp_settle_health = 40 * 2.0 * 0.9 - 36 = 36
// max settle = 36 / (0.9 * 2.0) = 20
(40.0, -50.0, -36.0, 6, 100),
(true, 30.0, -40.0),
(10.0, -10.0, 0.0, 0.0),
10,
),
(
"settle 2 (+insurance)",
(0.9, 2.0, 3.0),
// perp_settle_health = 40 * 2.0 * 0.9 - 36 = 36
// max settle = 36 / (0.9 * 2.0) = 20
(40.0, -50.0, -36.0, 100, 100),
(true, 20.0, -21.0),
(20.0, -29.0, 6.0, 0.0),
29, // limited by max_liab_transfer
),
(
"settle 3 (+insurance)",
(0.9, 2.0, 3.0),
// perp_settle_health = 40 * 2.0 * 0.9 - 36 = 36
// max settle = 36 / (0.9 * 2.0) = 20
(40.0, -50.0, -36.0, 100, 10), // limited by settleable pnl
(true, 30.0, -31.0),
(10.0, -19.0, 6.0, 0.0),
19,
),
(
"settle 4 (+socialized loss)",
(0.9, 2.0, 3.0),
// perp_settle_health = 5 * 2.0 * 0.9 + 30 = 39
// max settle = 5 + (39 - 9) / (2*1.1) = 18.63
(5.0, -20.0, 30.0, 0, 100),
(true, -13.63, 0.0),
(18.63, -18.63, 0.0, 1.36), // socialized loss
100,
),
(
"bankruptcy, no insurance 1",
(0.9, 2.0, 1.0),
(0.0, -5.0, 2.2, 0, 0),
(true, 0.0, -1.0), // -1 * 2.0 * 1.1 = 2.2
(0.0, 0.0, 0.0, 4.0),
0,
),
(
"bankruptcy, no insurance 2",
(0.9, 2.0, 1.0),
(4.0, -5.0, -3.6, 0, 0), // health token balance goes from -1 to +2
(true, 4.0, -2.0),
(0.0, 0.0, 0.0, 3.0),
0,
),
(
"bankruptcy, no insurance 3",
(0.9, 2.0, 1.0),
(4.0, -5.0, -3.6, 0, 0),
(true, 4.0, -2.0),
(0.0, 0.0, 0.0, 3.0),
100, // liqor being willing to take over changes nothing
),
(
"bankruptcy, with insurance 1",
(0.9, 2.0, 3.0),
(40.0, -50.0, -36.0, 6, 0),
(true, 40.0, -20.0),
(0.0, -9.0, 6.0, 21.0), // 6 * 3.0 / 2.0 = 9 taken over, rest socialized loss
100,
),
(
"bankruptcy, with insurance 2",
(0.9, 2.0, 3.0),
(40.0, -50.0, -36.0, 6, 0),
(true, 40.0, -47.0),
(0.0, -3.0, 2.0, 0.0),
3, // liqor is limited, don't socialize loss since insurance not exhausted!
),
(
"bankruptcy, with insurance 3",
(0.9, 2.0, 3.0),
(40.0, -50.0, -36.0, 1000, 0), // insurance fund is big enough to cover fully
(true, 40.0, -20.0),
(0.0, -30.0, 20.0, 0.0),
100,
),
(
"everything 1",
(0.9, 2.0, 3.0),
// perp_settle_health = 40 * 2.0 * 0.9 - 36 = 36
// max settle = 36 / (0.9 * 2.0) = 20
(40.0, -50.0, -36.0, 6, 100),
(true, 20.0, 0.0),
(20.0, -29.0, 6.0, 21.0),
40,
),
(
"everything 2",
(0.9, 2.0, 3.0),
// perp_settle_health = 10 * 2.0 * 0.9 + 9 = 19
// max settle = 10 + 9 / (1.1 * 2.0) = 14.1
(10.0, -50.0, 9.0, 6, 100),
(true, -4.1, 0.0), // perp position always goes to 0 because we use the same weights for init and maint
(14.1, -14.1 - 9.0, 6.0, 50.0 - 14.1 - 9.0),
40,
),
];
for (
name,
(settle_token_weight, settle_price, insurance_price),
// starting position
(init_settle_token, init_perp, init_other, insurance_amount, settle_limit),
// the expected liqee end position
(exp_success, exp_liqee_settle_token, exp_liqee_perp),
// expected liqor end position
(exp_liqor_settle_token, exp_liqor_perp, exp_liqor_insurance, exp_socialized_loss),
// maximum liquidation the liqor requests
max_liab_transfer,
) in test_cases
{
println!("test: {name}");
let mut setup = TestSetup::new();
{
let t = setup.settle_bank.data();
t.init_asset_weight = I80F48::from_num(settle_token_weight);
t.init_liab_weight = I80F48::from_num(2.0 - settle_token_weight);
// maint weights used for perp settle health
t.maint_asset_weight = I80F48::from_num(settle_token_weight);
t.maint_liab_weight = I80F48::from_num(2.0 - settle_token_weight);
t.stable_price_model.stable_price = settle_price;
setup.settle_oracle.data().price = I80F48::from_num(settle_price);
let t = setup.insurance_bank.data();
t.stable_price_model.stable_price = insurance_price;
setup.insurance_oracle.data().price = I80F48::from_num(insurance_price);
let p = setup.perp_market.data();
p.init_overall_asset_weight = I80F48::from_num(0.0);
p.open_interest = 1;
setup.insurance_vault.amount = insurance_amount;
}
{
let p = perp_p(&mut setup.liqee);
p.quote_position_native = I80F48::from_num(init_perp);
p.settle_pnl_limit_realized_trade = -settle_limit;
let settle_bank = setup.settle_bank.data();
settle_bank
.change_without_fee(
settle_p(&mut setup.liqee),
I80F48::from_num(init_settle_token),
0,
)
.unwrap();
let other_bank = setup.other_bank.data();
other_bank
.change_without_fee(other_p(&mut setup.liqee), I80F48::from_num(init_other), 0)
.unwrap();
}
let result = setup.run(max_liab_transfer);
if !exp_success {
assert!(result.is_err());
continue;
}
let mut result = result.unwrap();
let settle_bank = result.settle_bank.data();
assert_eq_f!(
settle_p(&mut result.liqee).native(settle_bank),
exp_liqee_settle_token,
0.01
);
assert_eq_f!(
settle_p(&mut result.liqor).native(settle_bank),
exp_liqor_settle_token,
0.01
);
let insurance_bank = result.insurance_bank.data();
assert_eq_f!(
insurance_p(&mut result.liqor).native(insurance_bank),
exp_liqor_insurance,
0.01
);
assert_eq_f!(
perp_p(&mut result.liqee).quote_position_native,
exp_liqee_perp,
0.1
);
assert_eq_f!(
perp_p(&mut result.liqor).quote_position_native,
exp_liqor_perp,
0.1
);
assert_eq_f!(
result.perp_market.data().long_funding,
exp_socialized_loss,
0.1
);
}
}
}

View File

@ -38,12 +38,13 @@ pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>) -> Result<()> {
let a_liq_end_health;
let a_maint_health;
let b_settle_health;
let b_max_settle;
{
let retriever =
ScanningAccountRetriever::new(ctx.remaining_accounts, &ctx.accounts.group.key())
.context("create account retriever")?;
b_settle_health = new_health_cache(&account_b.borrow(), &retriever)?.perp_settle_health();
b_max_settle = new_health_cache(&account_b.borrow(), &retriever)?
.perp_max_settle(settle_token_index)?;
let a_cache = new_health_cache(&account_a.borrow(), &retriever)?;
a_liq_end_health = a_cache.health(HealthType::LiquidationEnd);
a_maint_health = a_cache.health(HealthType::Maint);
@ -119,16 +120,16 @@ pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>) -> Result<()> {
// 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,
b_max_settle > 0,
MangoError::HealthMustBePositive,
"account b settle health is negative: {}",
b_settle_health
"account b settle max is not positive: {}",
b_max_settle
);
// Settle for the maximum possible capped to target's settle health
let settlement = a_settleable_pnl
.min(-b_settleable_pnl)
.min(b_settle_health)
.min(b_max_settle)
.max(I80F48::ZERO);
require_msg_typed!(
settlement >= 0,
@ -136,7 +137,7 @@ pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>) -> Result<()> {
"a settleable: {}, b settleable: {}, b settle health: {}",
a_settleable_pnl,
b_settleable_pnl,
b_settle_health,
b_max_settle,
);
let fee = perp_market.compute_settle_fee(settlement, a_liq_end_health, a_maint_health)?;

View File

@ -301,9 +301,7 @@ pub fn serum3_place_order(
// the total serum3 potential amount assumes all reserved amounts convert at the current
// oracle price.
if receiver_bank_reduce_only {
let balance = health_cache
.token_info(receiver_token_index)?
.balance_native;
let balance = health_cache.token_info(receiver_token_index)?.balance_spot;
let potential =
health_cache.total_serum3_potential(HealthType::Maint, receiver_token_index)?;
require_msg_typed!(

View File

@ -131,7 +131,7 @@ impl<'a, 'info> DepositCommon<'a, 'info> {
let group = self.group.load()?;
if group.deposit_limit_quote > 0 {
let assets = cache
.health_assets_and_liabs(HealthType::Init)
.health_assets_and_liabs_stable_assets(HealthType::Init)
.0
.round_to_zero()
.to_num::<u64>();

View File

@ -59,7 +59,18 @@ pub fn token_liq_bankruptcy(
let liab_borrow_index = liab_bank.borrow_index;
let (liqee_liab, liqee_raw_token_index) = liqee.token_position_mut(liab_token_index)?;
let initial_liab_native = liqee_liab.native(liab_bank);
let mut remaining_liab_loss = -initial_liab_native;
let liqee_health_token_balances =
liqee_health_cache.effective_token_balances(HealthType::LiquidationEnd);
let liqee_liab_health_balance = liqee_health_token_balances
[liqee_health_cache.token_info_index(liab_token_index)?]
.spot_and_perp;
// Allow token bankruptcy only while the spot position and health token position are both negative.
// In particular, a very negative perp hupnl does not allow token bankruptcy to happen,
// and if the perp hupnl is positive, we need to liquidate that before dealing with token
// bankruptcy!
let mut remaining_liab_loss = (-initial_liab_native).min(-liqee_liab_health_balance);
require_gt!(remaining_liab_loss, I80F48::ZERO);
// We pay for the liab token in quote. Example: SOL is at $20 and USDC is at $2, then for a liab
@ -100,7 +111,8 @@ pub fn token_liq_bankruptcy(
// liqee gets liab assets (enable dusting to prevent a case where the position is brought
// to +I80F48::DELTA)
liqee_liab_active = liab_bank.deposit_with_dusting(liqee_liab, liab_transfer, now_ts)?;
remaining_liab_loss = -liqee_liab.native(liab_bank);
// update correctly even if dusting happened
remaining_liab_loss -= liqee_liab.native(liab_bank) - initial_liab_native;
// move insurance assets into quote bank
let group_seeds = group_seeds!(group);

View File

@ -1,6 +1,5 @@
use anchor_lang::prelude::*;
use fixed::types::I80F48;
use std::cmp::min;
use crate::accounts_ix::*;
use crate::error::*;
@ -92,6 +91,8 @@ pub(crate) fn liquidation_action(
now_ts: u64,
max_liab_transfer: I80F48,
) -> Result<()> {
let liq_end_type = HealthType::LiquidationEnd;
// Get the mut banks and oracle prices
//
// This must happen _after_ the health computation, since immutable borrows of
@ -105,12 +106,12 @@ pub(crate) fn liquidation_action(
let (liqee_asset_position, liqee_asset_raw_index) =
liqee.token_position_and_raw_index(asset_token_index)?;
let liqee_asset_native = liqee_asset_position.native(asset_bank);
require!(liqee_asset_native.is_positive(), MangoError::SomeError);
require_gt!(liqee_asset_native, 0);
let (liqee_liab_position, liqee_liab_raw_index) =
liqee.token_position_and_raw_index(liab_token_index)?;
let liqee_liab_native = liqee_liab_position.native(liab_bank);
require!(liqee_liab_native.is_negative(), MangoError::SomeError);
require_gt!(0, liqee_liab_native);
// Liquidation fees work by giving the liqor more assets than the oracle price would
// indicate. Specifically we choose
@ -132,13 +133,44 @@ pub(crate) fn liquidation_action(
.token_info(liab_token_index)
.unwrap()
.prices
.liab(HealthType::LiquidationEnd);
.liab(liq_end_type);
// Health price for an asset of one native asset token
let asset_liq_end_price = liqee_health_cache
.token_info(asset_token_index)
.unwrap()
.prices
.asset(HealthType::LiquidationEnd);
.asset(liq_end_type);
let liqee_health_token_balances = liqee_health_cache.effective_token_balances(liq_end_type);
// At this point we've established that the liqee has a negative liab token position.
// However, the hupnl from perp markets can bring the health contribution for the token
// to a positive value.
// We'll only liquidate while the health token position is negative and rely on perp
// liquidation to offset the remainder by converting the upnl to a real spot position.
// At the same time an account with a very negative hupnl should not cause a large
// token liquidation: it'd be a way for perp losses to escape into the spot world. Only
// liquidate while the actual spot position is negative.
let liqee_liab_health_balance = liqee_health_token_balances
[liqee_health_cache.token_info_index(liab_token_index)?]
.spot_and_perp;
let max_liab_liquidation = max_liab_transfer
.min(-liqee_liab_native)
.min(-liqee_liab_health_balance)
.max(I80F48::ZERO);
// Similarly to the above, we should only reduce the asset token position while the
// health token balance and the actual token balance stay positive. Otherwise we'd be
// creating a new liability once perp upnl is settled.
let liqee_asset_health_balance = liqee_health_token_balances
[liqee_health_cache.token_info_index(asset_token_index)?]
.spot_and_perp;
let max_asset_transfer = liqee_asset_native
.min(liqee_asset_health_balance)
.max(I80F48::ZERO);
require_gt!(max_liab_liquidation, 0);
require_gt!(max_asset_transfer, 0);
// How much asset would need to be exchanged to liab in order to bring health to 0?
//
@ -159,20 +191,20 @@ pub(crate) fn liquidation_action(
// y = x * lopa / aop (native asset tokens, see above)
//
// Result: x = -init_health / (ilw * llep - iaw * lopa * alep / aop)
//
// Simplified for alep == aop:
assert!(asset_liq_end_price == asset_oracle_price);
let liab_needed = -liqee_liq_end_health
/ (liab_liq_end_price * init_liab_weight
- liab_oracle_price_adjusted
* init_asset_weight
* (asset_liq_end_price / asset_oracle_price));
/ (liab_liq_end_price * init_liab_weight - liab_oracle_price_adjusted * init_asset_weight);
// How much liab can we get at most for the asset balance?
let liab_possible = liqee_asset_native * asset_oracle_price / liab_oracle_price_adjusted;
let liab_possible = max_asset_transfer * asset_oracle_price / liab_oracle_price_adjusted;
// The amount of liab native tokens we will transfer
let liab_transfer = min(
min(min(liab_needed, -liqee_liab_native), liab_possible),
max_liab_transfer,
);
let liab_transfer = liab_needed
.min(liab_possible)
.min(max_liab_liquidation)
.max(I80F48::ZERO);
// The amount of asset native tokens we will give up for them
let asset_transfer = liab_transfer * liab_oracle_price_adjusted / asset_oracle_price;
@ -314,8 +346,14 @@ mod tests {
group: Pubkey,
asset_bank: TestAccount<Bank>,
liab_bank: TestAccount<Bank>,
other_bank: TestAccount<Bank>,
asset_oracle: TestAccount<StubOracle>,
liab_oracle: TestAccount<StubOracle>,
other_oracle: TestAccount<StubOracle>,
perp_market_asset: TestAccount<PerpMarket>,
perp_oracle_asset: TestAccount<StubOracle>,
perp_market_liab: TestAccount<PerpMarket>,
perp_oracle_liab: TestAccount<StubOracle>,
liqee: MangoAccountValue,
liqor: MangoAccountValue,
}
@ -326,11 +364,40 @@ mod tests {
let (asset_bank, asset_oracle) = mock_bank_and_oracle(group, 0, 1.0, 0.0, 0.0);
let (liab_bank, liab_oracle) = mock_bank_and_oracle(group, 1, 1.0, 0.0, 0.0);
let (_bank3, perp_oracle_asset) = mock_bank_and_oracle(group, 4, 1.0, 0.5, 0.3);
let mut perp_market_asset = mock_perp_market(
group,
perp_oracle_asset.pubkey,
1.0,
9,
(0.2, 0.1),
(0.2, 0.1),
);
perp_market_asset.data().settle_token_index = 1;
perp_market_asset.data().base_lot_size = 1;
let (_bank4, perp_oracle_liab) = mock_bank_and_oracle(group, 5, 1.0, 0.5, 0.3);
let mut perp_market_liab = mock_perp_market(
group,
perp_oracle_liab.pubkey,
1.0,
10,
(0.2, 0.1),
(0.2, 0.1),
);
perp_market_liab.data().settle_token_index = 0;
perp_market_liab.data().base_lot_size = 1;
let (other_bank, other_oracle) = mock_bank_and_oracle(group, 2, 1.0, 0.0, 0.0);
let liqee_buffer = MangoAccount::default_for_tests().try_to_vec().unwrap();
let mut liqee = MangoAccountValue::from_bytes(&liqee_buffer).unwrap();
{
liqee.ensure_token_position(0).unwrap();
liqee.ensure_token_position(1).unwrap();
liqee.ensure_token_position(2).unwrap();
liqee.ensure_perp_position(9, 1).unwrap();
liqee.ensure_perp_position(10, 0).unwrap();
}
let liqor_buffer = MangoAccount::default_for_tests().try_to_vec().unwrap();
@ -344,8 +411,14 @@ mod tests {
group,
asset_bank,
liab_bank,
other_bank,
asset_oracle,
liab_oracle,
other_oracle,
perp_market_asset,
perp_oracle_asset,
perp_market_liab,
perp_oracle_liab,
liqee,
liqor,
}
@ -357,8 +430,14 @@ mod tests {
let ais = vec![
setup.asset_bank.as_account_info(),
setup.liab_bank.as_account_info(),
setup.other_bank.as_account_info(),
setup.asset_oracle.as_account_info(),
setup.liab_oracle.as_account_info(),
setup.other_oracle.as_account_info(),
setup.perp_market_asset.as_account_info(),
setup.perp_market_liab.as_account_info(),
setup.perp_oracle_asset.as_account_info(),
setup.perp_oracle_liab.as_account_info(),
];
let retriever =
ScanningAccountRetriever::new_with_staleness(&ais, &setup.group, None).unwrap();
@ -372,8 +451,14 @@ mod tests {
let ais = vec![
setup.asset_bank.as_account_info(),
setup.liab_bank.as_account_info(),
setup.other_bank.as_account_info(),
setup.asset_oracle.as_account_info(),
setup.liab_oracle.as_account_info(),
setup.other_oracle.as_account_info(),
setup.perp_market_asset.as_account_info(),
setup.perp_market_liab.as_account_info(),
setup.perp_oracle_asset.as_account_info(),
setup.perp_oracle_liab.as_account_info(),
];
let mut retriever =
ScanningAccountRetriever::new_with_staleness(&ais, &setup.group, None).unwrap();
@ -409,6 +494,16 @@ mod tests {
fn liab_p(account: &mut MangoAccountValue) -> &mut TokenPosition {
account.token_position_mut(1).unwrap().0
}
fn other_p(account: &mut MangoAccountValue) -> &mut TokenPosition {
account.token_position_mut(2).unwrap().0
}
fn asset_perp_p(account: &mut MangoAccountValue) -> &mut PerpPosition {
account.perp_position_mut(10).unwrap()
}
fn liab_perp_p(account: &mut MangoAccountValue) -> &mut PerpPosition {
account.perp_position_mut(9).unwrap()
}
macro_rules! assert_eq_f {
($value:expr, $expected:expr, $max_error:expr) => {
@ -472,4 +567,205 @@ mod tests {
let hc = result.liqee_health_cache();
assert_eq_f!(hc.health(HealthType::LiquidationEnd), 0.0, 0.01);
}
#[test]
fn test_liq_with_token_while_perp() {
let test_cases = vec![
(
"nothing",
(0.9, 0.9, 0.9),
(0.0, 0.0, 0.0, 0.0, 0.0),
(false, 0.0, 0.0),
100,
),
(
"no liabs1",
(0.9, 0.9, 0.9),
(1.0, 0.0, 0.0, 0.0, 0.0),
(false, 0.0, 0.0),
100,
),
(
"no liabs2",
(0.9, 0.9, 0.9),
(1.0, 0.0, 1.0, -1.0, 0.0),
(false, 0.0, 0.0),
100,
),
(
"no assets1",
(0.9, 0.9, 0.9),
(0.0, 0.0, -1.0, 0.0, 0.0),
(false, 0.0, 0.0),
100,
),
(
"no assets2",
(0.9, 0.9, 0.9),
(99.999, -100.0, -1.0, 0.0, 0.0), // depositing 100 throws this off due to rounding
(false, 0.0, 0.0),
100,
),
(
"no perps1",
(0.9, 0.9, 0.9),
(10.0, 0.0, -11.0, 0.0, 0.0),
(true, 0.0, -1.0),
100,
),
(
"no perps1, limited",
(0.9, 0.9, 0.9),
(10.0, 0.0, -11.0, 0.0, 0.0),
(true, 8.0, -9.0),
2,
),
(
"no perps2",
(0.8, 0.8, 0.8),
(10.0, 0.0, -9.0, 0.0, 0.0),
(true, 3.0, -2.0), // 3 * 0.8 - 2 * 1.2 = 0
100,
),
(
"no perps2, other health1",
(0.8, 0.8, 0.8),
(10.0, 0.0, -9.0, 0.0, 0.4),
(true, 4.0, -3.0), // 4 * 0.8 - 3 * 1.2 + 0.4 = 0
100,
),
(
"no perps2, other health2",
(0.8, 0.8, 0.8),
(10.0, 0.0, -9.0, 0.0, -0.4),
(true, 2.0, -1.0), // 2 * 0.8 - 1 * 1.2 - 0.4 = 0
100,
),
(
"perp assets1",
(0.8, 0.8, 0.5),
(5.0, 6.0, -9.0, 0.0, 0.0),
(true, 0.0, -4.0), // (0 + 0.5 * 6) * 0.8 - 4 * 1.2 is still negative
100,
),
(
"perp assets2",
(0.8, 0.8, 0.5),
(5.0, 14.0, -9.0, 0.0, 0.0),
(true, 2.0, -6.0), // (2 + 0.5 * 14) * 0.8 - 6 * 1.2 = 0
100,
),
(
"perp assets3",
(0.8, 0.8, 0.5),
(0.0, 14.0, -9.0, 0.0, 0.0),
(false, 0.0, -9.0),
100,
),
(
"perp liabs1",
(0.8, 0.8, 0.5),
(10.0, 0.0, -4.0, -5.0, 0.0),
(true, 6.0, 0.0), // 6 * 0.8 - (0 - 5) * 1.2 is still negative
100,
),
(
"perp liabs2",
(0.8, 0.8, 0.5),
(10.0, 0.0, -8.0, -1.0, 0.0),
(true, 3.0, -1.0), // 3 * 0.8 - (-1 - 1) * 1.2 = 0
100,
),
(
"perp liabs3",
(0.8, 0.8, 0.5),
(10.0, 0.0, 0.0, -9.0, 0.0),
(false, 10.0, 0.0),
100,
),
];
for (
name,
(asset_token_weight, liab_token_weight, perp_overall_weight),
// starting position in asset spot/perp and liab spot/perp
(init_asset_token, init_asset_perp, init_liab_token, init_liab_perp, init_other),
// the expected liqee end position
(exp_success, exp_asset_token, exp_liab_token),
// maximum liquidation the liqor requests
max_liab_transfer,
) in test_cases
{
println!("test: {name}");
let mut setup = TestSetup::new();
{
let t = setup.asset_bank.data();
t.init_asset_weight = I80F48::from_num(asset_token_weight);
t.init_liab_weight = I80F48::from_num(2.0 - asset_token_weight);
let t = setup.liab_bank.data();
t.init_asset_weight = I80F48::from_num(liab_token_weight);
t.init_liab_weight = I80F48::from_num(2.0 - liab_token_weight);
let p = setup.perp_market_asset.data();
p.init_overall_asset_weight = I80F48::from_num(perp_overall_weight);
let p = setup.perp_market_liab.data();
p.init_overall_asset_weight = I80F48::from_num(perp_overall_weight);
}
{
asset_perp_p(&mut setup.liqee).quote_position_native =
I80F48::from_num(init_asset_perp);
liab_perp_p(&mut setup.liqee).quote_position_native =
I80F48::from_num(init_liab_perp);
let liab_bank = setup.liab_bank.data();
liab_bank
.change_without_fee(
liab_p(&mut setup.liqee),
I80F48::from_num(init_liab_token),
0,
)
.unwrap();
liab_bank
.change_without_fee(liab_p(&mut setup.liqor), I80F48::from_num(1000.0), 0)
.unwrap();
let asset_bank = setup.asset_bank.data();
asset_bank
.change_without_fee(
asset_p(&mut setup.liqee),
I80F48::from_num(init_asset_token),
0,
)
.unwrap();
let other_bank = setup.other_bank.data();
other_bank
.change_without_fee(other_p(&mut setup.liqee), I80F48::from_num(init_other), 0)
.unwrap();
}
let result = setup.run(I80F48::from_num(max_liab_transfer));
if !exp_success {
assert!(result.is_err());
continue;
}
let mut result = result.unwrap();
let liab_bank = result.liab_bank.data();
assert_eq_f!(
liab_p(&mut result.liqee).native(liab_bank),
exp_liab_token,
0.01
);
let asset_bank = result.asset_bank.data();
assert_eq_f!(
asset_p(&mut result.liqee).native(asset_bank),
exp_asset_token,
0.01
);
}
}
}

View File

@ -1097,6 +1097,15 @@ pub mod mango_v4 {
pub fn perp_liq_negative_pnl_or_bankruptcy(
ctx: Context<PerpLiqNegativePnlOrBankruptcy>,
max_liab_transfer: u64,
) -> Result<()> {
Err(error_msg!(
"PerpLiqNegativePnlOrBankruptcy was replaced by PerpLiqNegativePnlOrBankruptcyV2"
))
}
pub fn perp_liq_negative_pnl_or_bankruptcy_v2(
ctx: Context<PerpLiqNegativePnlOrBankruptcyV2>,
max_liab_transfer: u64,
) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::perp_liq_negative_pnl_or_bankruptcy(ctx, max_liab_transfer)?;

View File

@ -180,8 +180,15 @@ pub struct PerpPosition {
/// Active position size, measured in base lots
pub base_position_lots: i64,
/// Active position in quote (conversation rate is that of the time the order was settled)
/// measured in native quote
/// Active position in oracle quote native. At the same time this is 1:1 a settle_token native amount.
///
/// Example: Say there's a perp market on the BTC/USD price using SOL for settlement. The user buys
/// one long contract for $20k, then base = 1, quote = -20k. The price goes to $21k. Now their
/// unsettled pnl is (1 * 21k - 20k) __SOL__ = 1000 SOL. This is because the perp contract arbitrarily
/// decides that each unit of price difference creates 1 SOL worth of settlement.
/// (yes, causing 1 SOL of settlement for each $1 price change implies a lot of extra leverage; likely
/// there should be an extra configurable scaling factor before we use this for cases like that)
pub quote_position_native: I80F48,
/// Tracks what the position is to calculate average entry & break even price

View File

@ -375,11 +375,13 @@ impl PerpMarket {
}
/// Socialize the loss in this account across all longs and shorts
///
/// `loss` is in settle token native units
pub fn socialize_loss(&mut self, loss: I80F48) -> Result<I80F48> {
require_gte!(0, loss);
// TODO convert into only socializing on one side
// native USDC per contract open interest
// native settle token per contract open interest
let socialized_loss = if self.open_interest == 0 {
// AUDIT: think about the following:
// This is kind of an unfortunate situation. This means socialized loss occurs on the

View File

@ -3,15 +3,15 @@ use super::*;
#[tokio::test]
async fn test_liq_perps_base_and_bankruptcy() -> Result<(), TransportError> {
let mut test_builder = TestContextBuilder::new();
test_builder.test().set_compute_max_units(120_000); // PerpLiqBaseOrPositivePnl takes a lot of CU
test_builder.test().set_compute_max_units(150_000); // PerpLiqBaseOrPositivePnl 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..2];
let payer_mint_accounts = &context.users[1].token_accounts[0..2];
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
@ -52,7 +52,8 @@ async fn test_liq_perps_base_and_bankruptcy() -> Result<(), TransportError> {
}
let quote_token = &tokens[0];
let base_token = &tokens[1];
let settle_token = &tokens[1];
let base_token = &tokens[2];
// deposit some funds, to the vaults aren't empty
let liqor = create_funded_account(
@ -80,11 +81,12 @@ async fn test_liq_perps_base_and_bankruptcy() -> Result<(), TransportError> {
admin,
payer,
perp_market_index: 0,
settle_token_index: 1,
quote_lot_size: 10,
base_lot_size: 100,
maint_base_asset_weight: 0.8,
maint_base_asset_weight: 0.7,
init_base_asset_weight: 0.6,
maint_base_liab_weight: 1.2,
maint_base_liab_weight: 1.3,
init_base_liab_weight: 1.4,
base_liquidation_fee: 0.05,
maker_fee: 0.0,
@ -104,11 +106,11 @@ async fn test_liq_perps_base_and_bankruptcy() -> Result<(), TransportError> {
};
//
// SETUP: Make an two accounts and deposit some quote and base
// SETUP: Make an two accounts and deposit some quote
//
let context_ref = &context;
let make_account = |idx: u32| async move {
let deposit_amount = 1000;
let deposit_amount = 1330;
let account = create_funded_account(
&solana,
group,
@ -169,26 +171,26 @@ async fn test_liq_perps_base_and_bankruptcy() -> Result<(), TransportError> {
// 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
// 20*100*(0.6-1)*1.4 = -1120 for the long account0
// 20*100*(1-1.4)*1.4 = -1120 for the short account1
// (100 is base lot size)
assert_eq!(
account_init_health(solana, account_0).await.round(),
1000.0 - 800.0
1330.0 - 1120.0
);
assert_eq!(
account_init_health(solana, account_1).await.round(),
1000.0 - 800.0
1330.0 - 1120.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
// perp base health contrib decreases from 2000 * 0.6 * 1.4 to 2000 * 0.6 * 0.6 * 1.4, i.e. -672
//
set_bank_stub_oracle_price(solana, group, base_token, admin, 0.6).await;
assert_eq!(
account_init_health(solana, account_0).await.round(),
200.0 - 480.0
210.0 - 672.0
);
//
@ -248,16 +250,16 @@ async fn test_liq_perps_base_and_bankruptcy() -> Result<(), TransportError> {
.await
.unwrap();
let liq_amount_2 = 4.0 * 100.0 * 0.6 * (1.0 - 0.05);
let liq_amount_2 = 6.0 * 100.0 * 0.6 * (1.0 - 0.05);
let liqor_data = solana.get_account::<MangoAccount>(liqor).await;
assert_eq!(liqor_data.perps[0].base_position_lots(), 10 + 4);
assert_eq!(liqor_data.perps[0].base_position_lots(), 10 + 6);
assert!(assert_equal(
liqor_data.perps[0].quote_position_native(),
-liq_amount - liq_amount_2,
0.1
));
let liqee_data = solana.get_account::<MangoAccount>(account_0).await;
assert_eq!(liqee_data.perps[0].base_position_lots(), 6);
assert_eq!(liqee_data.perps[0].base_position_lots(), 4);
assert!(assert_equal(
liqee_data.perps[0].quote_position_native(),
-20.0 * 100.0 + liq_amount + liq_amount_2,
@ -282,7 +284,7 @@ async fn test_liq_perps_base_and_bankruptcy() -> Result<(), TransportError> {
//
// SETUP: Change the oracle to make health go negative for account_1
//
set_bank_stub_oracle_price(solana, group, base_token, admin, 1.3).await;
set_bank_stub_oracle_price(solana, group, base_token, admin, 1.32).await;
// verify health is bad: can't withdraw
assert!(send_tx(
@ -316,9 +318,9 @@ async fn test_liq_perps_base_and_bankruptcy() -> Result<(), TransportError> {
.await
.unwrap();
let liq_amount_3 = 10.0 * 100.0 * 1.3 * (1.0 + 0.05);
let liq_amount_3 = 10.0 * 100.0 * 1.32 * (1.0 + 0.05);
let liqor_data = solana.get_account::<MangoAccount>(liqor).await;
assert_eq!(liqor_data.perps[0].base_position_lots(), 14 - 10);
assert_eq!(liqor_data.perps[0].base_position_lots(), 16 - 10);
assert!(assert_equal(
liqor_data.perps[0].quote_position_native(),
-liq_amount - liq_amount_2 + liq_amount_3,
@ -349,16 +351,16 @@ async fn test_liq_perps_base_and_bankruptcy() -> Result<(), TransportError> {
.await
.unwrap();
let liq_amount_4 = 5.0 * 100.0 * 1.3 * (1.0 + 0.05);
let liq_amount_4 = 7.0 * 100.0 * 1.32 * (1.0 + 0.05);
let liqor_data = solana.get_account::<MangoAccount>(liqor).await;
assert_eq!(liqor_data.perps[0].base_position_lots(), 4 - 5);
assert_eq!(liqor_data.perps[0].base_position_lots(), 6 - 7);
assert!(assert_equal(
liqor_data.perps[0].quote_position_native(),
-liq_amount - liq_amount_2 + liq_amount_3 + liq_amount_4,
0.1
));
let liqee_data = solana.get_account::<MangoAccount>(account_1).await;
assert_eq!(liqee_data.perps[0].base_position_lots(), -5);
assert_eq!(liqee_data.perps[0].base_position_lots(), -3);
assert!(assert_equal(
liqee_data.perps[0].quote_position_native(),
20.0 * 100.0 - liq_amount_3 - liq_amount_4,
@ -383,7 +385,8 @@ async fn test_liq_perps_base_and_bankruptcy() -> Result<(), TransportError> {
//
// SETUP: liquidate base position to 0, so bankruptcy can be tested
//
set_bank_stub_oracle_price(solana, group, base_token, admin, 2.0).await;
let perp_oracle_price = 2.0;
set_bank_stub_oracle_price(solana, group, base_token, admin, perp_oracle_price).await;
//
// TEST: Liquidate base position max
@ -402,9 +405,9 @@ async fn test_liq_perps_base_and_bankruptcy() -> Result<(), TransportError> {
.await
.unwrap();
let liq_amount_5 = 5.0 * 100.0 * 2.0 * (1.0 + 0.05);
let liq_amount_5 = 3.0 * 100.0 * 2.0 * (1.0 + 0.05);
let liqor_data = solana.get_account::<MangoAccount>(liqor).await;
assert_eq!(liqor_data.perps[0].base_position_lots(), -1 - 5);
assert_eq!(liqor_data.perps[0].base_position_lots(), -1 - 3);
assert!(assert_equal(
liqor_data.perps[0].quote_position_native(),
-liq_amount - liq_amount_2 + liq_amount_3 + liq_amount_4 + liq_amount_5,
@ -418,29 +421,6 @@ async fn test_liq_perps_base_and_bankruptcy() -> Result<(), TransportError> {
0.1
));
//
// SETUP: We want pnl settling to cause a negative quote position,
// thus we deposit some base token collateral. To be able to do that,
// we need to temporarily raise health > 0, deposit, then bring health
// negative again for the test
//
set_bank_stub_oracle_price(solana, group, quote_token, admin, 2.0).await;
send_tx(
solana,
TokenDepositInstruction {
amount: 1,
reduce_only: false,
account: account_1,
owner,
token_account: payer_mint_accounts[1],
token_authority: payer,
bank_index: 0,
},
)
.await
.unwrap();
set_bank_stub_oracle_price(solana, group, quote_token, admin, 1.0).await;
//
// TEST: Can settle-pnl even though health is negative
//
@ -449,7 +429,6 @@ async fn test_liq_perps_base_and_bankruptcy() -> Result<(), TransportError> {
let liqor_max_settle = liqor_data.perps[0]
.available_settle_limit(&perp_market_data)
.1;
let account_1_quote_before = account_position(solana, account_1, quote_token.bank).await;
send_tx(
solana,
@ -459,15 +438,14 @@ async fn test_liq_perps_base_and_bankruptcy() -> Result<(), TransportError> {
account_a: liqor,
account_b: account_1,
perp_market,
settle_bank: tokens[0].bank,
},
)
.await
.unwrap();
let liqee_settle_health_before: f64 = 999.0 + 1.0 * 2.0 * 0.8;
let liqee_quote_deposits_before: f64 = 1329.0;
// the liqor's settle limit means we can't settle everything
let settle_amount = liqee_settle_health_before.min(liqor_max_settle as f64);
let settle_amount = liqee_quote_deposits_before.min(liqor_max_settle as f64);
let remaining_pnl = 20.0 * 100.0 - liq_amount_3 - liq_amount_4 - liq_amount_5 + settle_amount;
assert!(remaining_pnl < 0.0);
let liqee_data = solana.get_account::<MangoAccount>(account_1).await;
@ -479,16 +457,56 @@ async fn test_liq_perps_base_and_bankruptcy() -> Result<(), TransportError> {
));
assert_eq!(
account_position(solana, account_1, quote_token.bank).await,
account_1_quote_before - settle_amount as i64
liqee_quote_deposits_before as i64
);
assert_eq!(
account_position(solana, account_1, base_token.bank).await,
1
account_position(solana, account_1, settle_token.bank).await,
-settle_amount as i64
);
//
// SETUP: Leave the account with a small positive quote pos and a bigger negative perp health
//
set_bank_stub_oracle_price(solana, group, quote_token, admin, 10.0).await;
// clear the negative settle token position, to avoid the liquidatable token position
send_tx(
solana,
TokenDepositInstruction {
amount: u64::MAX,
reduce_only: true,
account: account_1,
owner,
token_authority: payer,
token_account: payer_mint_accounts[1],
bank_index: 0,
},
)
.await
.unwrap();
// reduce the quote position so we still are liquidatable
send_tx(
solana,
TokenWithdrawInstruction {
amount: liqee_quote_deposits_before as u64 - 100,
allow_borrow: false,
account: account_1,
owner,
token_account: payer_mint_accounts[0],
bank_index: 0,
},
)
.await
.unwrap();
set_bank_stub_oracle_price(solana, group, quote_token, admin, 1.0).await;
//
// TEST: Can liquidate/bankruptcy away remaining negative pnl
//
let account0_before = solana.get_account::<MangoAccount>(account_0).await;
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]
@ -508,10 +526,9 @@ async fn test_liq_perps_base_and_bankruptcy() -> Result<(), TransportError> {
.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;
let quote_bank = solana.get_account::<Bank>(quote_token.bank).await;
let settle_bank = solana.get_account::<Bank>(settle_token.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;
@ -520,7 +537,13 @@ async fn test_liq_perps_base_and_bankruptcy() -> Result<(), TransportError> {
assert_eq!(solana.token_account_balance(insurance_vault).await, 0);
assert!(assert_equal(
liqor_data.tokens[0].native(&quote_bank),
liqor_before.tokens[0].native(&quote_bank).to_num::<f64>() + liq_spot_amount,
liqor_before.tokens[0].native(&quote_bank).to_num::<f64>() + insurance_vault_funding as f64,
0.1
));
assert!(assert_equal(
liqor_data.tokens[0].native(&settle_bank),
liqor_before.tokens[0].native(&settle_bank).to_num::<f64>()
- liqee_settle_limit_before as f64 * 100.0, // 100 is base lot size
0.1
));
@ -541,20 +564,26 @@ async fn test_liq_perps_base_and_bankruptcy() -> Result<(), TransportError> {
// the remainder got socialized via funding payments
let perp_market = solana.get_account::<PerpMarket>(perp_market).await;
let pnl_before = liqee_before.perps[0]
.unsettled_pnl(&perp_market, I80F48::ONE)
.unsettled_pnl(&perp_market, I80F48::from_num(perp_oracle_price))
.unwrap();
let pnl_after = liqee_after.perps[0]
.unsettled_pnl(&perp_market, I80F48::ONE)
.unsettled_pnl(&perp_market, I80F48::from_num(perp_oracle_price))
.unwrap();
let socialized_amount = (pnl_after - pnl_before).to_num::<f64>() - liq_perp_quote_amount;
let open_interest = 2 * liqor_data.perps[0].base_position_lots.abs();
assert!(assert_equal(
perp_market.long_funding,
socialized_amount / 20.0,
socialized_amount / open_interest as f64,
0.1
));
assert!(assert_equal(
perp_market.short_funding,
-socialized_amount / 20.0,
-socialized_amount / open_interest as f64,
0.1
));
assert!(assert_equal(
account0_before.perps[0].unsettled_funding(&perp_market),
socialized_amount / 2.0,
0.1
));

View File

@ -3,15 +3,15 @@ use super::*;
#[tokio::test]
async fn test_liq_perps_positive_pnl() -> Result<(), TransportError> {
let mut test_builder = TestContextBuilder::new();
test_builder.test().set_compute_max_units(140_000); // PerpLiqNegativePnlOrBankruptcy takes a lot of CU
test_builder.test().set_compute_max_units(170_000); // PerpLiqBaseOrPositivePnlInstruction 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];
let mints = &context.mints[0..4];
let payer_mint_accounts = &context.users[1].token_accounts[0..4];
//
// SETUP: Create a group and an account to fill the vaults
@ -51,9 +51,10 @@ async fn test_liq_perps_positive_pnl() -> Result<(), TransportError> {
tx.send().await.unwrap();
}
let quote_token = &tokens[0];
let _quote_token = &tokens[0];
let base_token = &tokens[1];
let borrow_token = &tokens[2];
let settle_token = &tokens[3];
// deposit some funds, to the vaults aren't empty
let liqor = create_funded_account(
@ -78,6 +79,7 @@ async fn test_liq_perps_positive_pnl() -> Result<(), TransportError> {
admin,
payer,
perp_market_index: 0,
settle_token_index: 3,
quote_lot_size: 10,
base_lot_size: 100,
maint_base_asset_weight: 0.8,
@ -188,22 +190,22 @@ async fn test_liq_perps_positive_pnl() -> Result<(), TransportError> {
.unwrap();
// after this order exchange it is changed by
// 10*10*100*(0.5-1) = -5000 for the long account0
// 10*10*100*(1-1.5) = -5000 for the short account1
// 10*10*100*(0.5-1)*1.4 = -7000 for the long account0
// 10*10*100*(1-1.5)*1.4 = -7000 for the short account1
// (100 is base lot size)
assert_eq!(
account_init_health(solana, account_0).await.round(),
(10000.0f64 - 1000.5 * 1.4 - 5000.0).round()
(10000.0f64 - 1000.5 * 1.4 - 7000.0).round()
);
assert_eq!(
account_init_health(solana, account_1).await.round(),
10000.0 - 5000.0
10000.0 - 7000.0
);
//
// SETUP: Change the perp oracle to make perp-based health go positive for account_0
// perp base value goes to 10*21*100*0.5, exceeding the negative quote
// unweighted perp health is 10*1*100*0.5 = 500
// perp uhupnl is 10*21*100*0.5 - 10*10*100 = 500
// but health doesn't exceed 10k because of the 0 overall weight
//
set_perp_stub_oracle_price(solana, group, perp_market, &base_token, admin, 21.0).await;
@ -265,15 +267,15 @@ async fn test_liq_perps_positive_pnl() -> Result<(), TransportError> {
assert_eq!(liqor_data.perps[0].base_position_lots(), 0);
assert_eq!(liqor_data.perps[0].quote_position_native(), 100);
assert_eq!(
account_position(solana, liqor, quote_token.bank).await,
account_position(solana, liqor, settle_token.bank).await,
10000 - 95
);
let liqee_data = solana.get_account::<MangoAccount>(account_0).await;
assert_eq!(liqee_data.perps[0].base_position_lots(), 10);
assert_eq!(liqee_data.perps[0].quote_position_native(), -10100);
assert_eq!(
account_position(solana, account_0, quote_token.bank).await,
10000 + 95
account_position(solana, account_0, settle_token.bank).await,
95
);
//
@ -301,7 +303,7 @@ async fn test_liq_perps_positive_pnl() -> Result<(), TransportError> {
0.1
));
assert_eq!(
account_position(solana, liqor, quote_token.bank).await,
account_position(solana, liqor, settle_token.bank).await,
10000 - 95 - 570
);
let liqee_data = solana.get_account::<MangoAccount>(account_0).await;
@ -312,8 +314,8 @@ async fn test_liq_perps_positive_pnl() -> Result<(), TransportError> {
0.1
));
assert_eq!(
account_position(solana, account_0, quote_token.bank).await,
10000 + 95 + 570
account_position(solana, account_0, settle_token.bank).await,
95 + 570
);
//
@ -349,9 +351,6 @@ async fn test_liq_perps_positive_pnl() -> Result<(), TransportError> {
// TEST: if overall perp health weight is >0, we can liquidate the base position further
//
// reduce the price some more, so the liq instruction can do some of step1 and step2
set_perp_stub_oracle_price(solana, group, perp_market, &base_token, admin, 17.0).await;
send_tx(
solana,
PerpChangeWeights {
@ -403,7 +402,7 @@ async fn test_liq_perps_positive_pnl() -> Result<(), TransportError> {
.unwrap();
let liqee_data = solana.get_account::<MangoAccount>(account_0).await;
assert_eq!(liqee_data.perps[0].base_position_lots(), 3);
assert_eq!(liqee_data.perps[0].base_position_lots(), 1);
let health = account_init_health(solana, account_0).await;
assert!(health > 0.0);
assert!(health < 1.0);

View File

@ -386,7 +386,6 @@ async fn test_perp_fixed() -> Result<(), TransportError> {
account_a: account_0,
account_b: account_1,
perp_market,
settle_bank: tokens[0].bank,
},
)
.await
@ -396,7 +395,6 @@ async fn test_perp_fixed() -> Result<(), TransportError> {
PerpSettleFeesInstruction {
account: account_1,
perp_market,
settle_bank: tokens[0].bank,
max_settle_amount: u64::MAX,
},
)

View File

@ -1,14 +1,16 @@
use super::*;
#[tokio::test]
async fn test_perp_settle_pnl() -> Result<(), TransportError> {
let context = TestContext::new().await;
async fn test_perp_settle_pnl_basic() -> Result<(), TransportError> {
let mut test_builder = TestContextBuilder::new();
test_builder.test().set_compute_max_units(90_000); // the divisions in perp_max_settle are costly!
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..=2];
let mints = &context.mints[0..=3];
let initial_token_deposit = 10_000;
@ -173,26 +175,6 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> {
assert_eq!(mango_account_1.perps[0].quote_position_native(), 100_000);
}
// Bank must be valid for quote currency
let result = send_tx(
solana,
PerpSettlePnlInstruction {
settler,
settler_owner,
account_a: account_1,
account_b: account_0,
perp_market,
settle_bank: tokens[1].bank,
},
)
.await;
assert_mango_error(
&result,
MangoError::InvalidBank.into(),
"Bank must be valid for quote currency".to_string(),
);
// Cannot settle with yourself
let result = send_tx(
solana,
@ -202,7 +184,6 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> {
account_a: account_0,
account_b: account_0,
perp_market,
settle_bank: tokens[0].bank,
},
)
.await;
@ -222,7 +203,6 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> {
account_a: account_0,
account_b: account_1,
perp_market: perp_market_2,
settle_bank: tokens[0].bank,
},
)
.await;
@ -263,7 +243,6 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> {
account_a: account_1,
account_b: account_0,
perp_market,
settle_bank: tokens[0].bank,
},
)
.await;
@ -285,11 +264,16 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> {
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
let perp_market = solana.get_account::<PerpMarket>(perp_market).await;
assert_eq!(
get_pnl_native(&mango_account_0.perps[0], &perp_market, I80F48::from(1005)).round(),
mango_account_0.perps[0]
.unsettled_pnl(&perp_market, I80F48::from(1005))
.unwrap()
.round(),
expected_pnl_0
);
assert_eq!(
get_pnl_native(&mango_account_1.perps[0], &perp_market, I80F48::from(1005)),
mango_account_1.perps[0]
.unsettled_pnl(&perp_market, I80F48::from(1005))
.unwrap(),
expected_pnl_1
);
}
@ -305,18 +289,43 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> {
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
let perp_market = solana.get_account::<PerpMarket>(perp_market).await;
assert_eq!(
get_pnl_native(&mango_account_0.perps[0], &perp_market, I80F48::from(1500)).round(),
mango_account_0.perps[0]
.unsettled_pnl(&perp_market, I80F48::from(1500))
.unwrap()
.round(),
expected_pnl_0
);
assert_eq!(
get_pnl_native(&mango_account_1.perps[0], &perp_market, I80F48::from(1500)),
mango_account_1.perps[0]
.unsettled_pnl(&perp_market, I80F48::from(1500))
.unwrap(),
expected_pnl_1
);
}
// Settle as much PNL as account_1's health allows
let account_1_health_non_perp = I80F48::from_num(0.8 * 10000.0);
let expected_total_settle = account_1_health_non_perp;
//
// SETUP: Add some non-settle token, so the account's health has more contributions
//
send_tx(
solana,
TokenDepositInstruction {
amount: 1001,
reduce_only: false,
account: account_1,
owner,
token_account: context.users[1].token_accounts[2],
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
.unwrap();
// Settle as much PNL as account_1's health allows:
// The account perp_settle_health is 0.8 * 10000.0 + 0.8 * 1001.0,
// meaning we can bring it to zero by settling 10000 * 0.8 / 0.8 + 1001 * 0.8 / 1.2 = 10667.333
// because then we'd be left with -667.333 * 1.2 + 0.8 * 1000 = 0
let expected_total_settle = I80F48::from(10667);
send_tx(
solana,
PerpSettlePnlInstruction {
@ -325,7 +334,6 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> {
account_a: account_0,
account_b: account_1,
perp_market,
settle_bank: tokens[0].bank,
},
)
.await
@ -390,19 +398,25 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> {
// Change the oracle to a reasonable price in other direction
set_perp_stub_oracle_price(solana, group, perp_market, &tokens[1], admin, 995.0).await;
let expected_pnl_0 = I80F48::from(-8520);
let expected_pnl_1 = I80F48::from(8500);
let expected_pnl_0 = I80F48::from(1 * 100 * 995 - 1 * 100 * 1000 - 20) - expected_total_settle;
let expected_pnl_1 = I80F48::from(-1 * 100 * 995 + 1 * 100 * 1000) + expected_total_settle;
{
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
let perp_market = solana.get_account::<PerpMarket>(perp_market).await;
assert_eq!(
get_pnl_native(&mango_account_0.perps[0], &perp_market, I80F48::from(995)).round(),
mango_account_0.perps[0]
.unsettled_pnl(&perp_market, I80F48::from(995))
.unwrap()
.round(),
expected_pnl_0
);
assert_eq!(
get_pnl_native(&mango_account_1.perps[0], &perp_market, I80F48::from(995)).round(),
mango_account_1.perps[0]
.unsettled_pnl(&perp_market, I80F48::from(995))
.unwrap()
.round(),
expected_pnl_1
);
}
@ -417,7 +431,6 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> {
account_a: account_1,
account_b: account_0,
perp_market,
settle_bank: tokens[0].bank,
},
)
.await
@ -478,11 +491,17 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> {
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
let perp_market = solana.get_account::<PerpMarket>(perp_market).await;
assert_eq!(
get_pnl_native(&mango_account_0.perps[0], &perp_market, I80F48::from(995)).round(),
mango_account_0.perps[0]
.unsettled_pnl(&perp_market, I80F48::from(995))
.unwrap()
.round(),
-20 // fees
);
assert_eq!(
get_pnl_native(&mango_account_1.perps[0], &perp_market, I80F48::from(995)).round(),
mango_account_1.perps[0]
.unsettled_pnl(&perp_market, I80F48::from(995))
.unwrap()
.round(),
0
);
}
@ -670,7 +689,6 @@ async fn test_perp_settle_pnl_fees() -> Result<(), TransportError> {
account_a: account_0,
account_b: account_1,
perp_market,
settle_bank,
},
)
.await
@ -738,7 +756,6 @@ async fn test_perp_settle_pnl_fees() -> Result<(), TransportError> {
account_a: account_0,
account_b: account_1,
perp_market,
settle_bank,
},
)
.await
@ -950,7 +967,6 @@ async fn test_perp_pnl_settle_limit() -> Result<(), TransportError> {
account_a: account_0,
account_b: account_1,
perp_market,
settle_bank: tokens[0].bank,
},
)
.await
@ -983,7 +999,6 @@ async fn test_perp_pnl_settle_limit() -> Result<(), TransportError> {
account_a: account_0,
account_b: account_1,
perp_market,
settle_bank: tokens[0].bank,
},
)
.await;
@ -1071,7 +1086,6 @@ async fn test_perp_pnl_settle_limit() -> Result<(), TransportError> {
account_a: account_0,
account_b: account_1,
perp_market,
settle_bank: tokens[0].bank,
},
)
.await
@ -1113,7 +1127,6 @@ async fn test_perp_pnl_settle_limit() -> Result<(), TransportError> {
account_a: account_0,
account_b: account_1,
perp_market,
settle_bank: tokens[0].bank,
},
)
.await
@ -1143,7 +1156,6 @@ async fn test_perp_pnl_settle_limit() -> Result<(), TransportError> {
account_a: account_0,
account_b: account_1,
perp_market,
settle_bank: tokens[0].bank,
},
)
.await
@ -1176,7 +1188,6 @@ async fn test_perp_pnl_settle_limit() -> Result<(), TransportError> {
account_a: account_0,
account_b: account_1,
perp_market,
settle_bank: tokens[0].bank,
},
)
.await

View File

@ -8,10 +8,9 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> {
let admin = TestKeypair::new();
let owner = context.users[0].key;
let payer = context.users[1].key;
let mints = &context.mints[0..2];
let payer_mint_accounts = &context.users[1].token_accounts[0..=2];
let mints = &context.mints[0..4];
let initial_token_deposit = 10_000;
let initial_token_deposit = 1_000_000;
//
// SETUP: Create a group and an account
@ -26,110 +25,34 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> {
.create(solana)
.await;
let account_0 = send_tx(
solana,
AccountCreateInstruction {
account_num: 0,
token_count: 16,
serum3_count: 8,
perp_count: 8,
perp_oo_count: 8,
group,
owner,
payer,
},
let _quote_token = &tokens[0];
let base_token0 = &tokens[1];
let base_token1 = &tokens[2];
let _settle_token = &tokens[3];
let account_0 = create_funded_account(
&solana,
group,
owner,
0,
&context.users[1],
&mints[0..1],
initial_token_deposit,
0,
)
.await
.unwrap()
.account;
.await;
let account_1 = send_tx(
solana,
AccountCreateInstruction {
account_num: 1,
token_count: 16,
serum3_count: 8,
perp_count: 8,
perp_oo_count: 8,
group,
owner,
payer,
},
let account_1 = create_funded_account(
&solana,
group,
owner,
1,
&context.users[1],
&mints[0..1],
initial_token_deposit,
0,
)
.await
.unwrap()
.account;
//
// SETUP: Deposit user funds
//
{
let deposit_amount = initial_token_deposit;
send_tx(
solana,
TokenDepositInstruction {
amount: deposit_amount,
reduce_only: false,
account: account_0,
owner,
token_account: payer_mint_accounts[0],
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
.unwrap();
send_tx(
solana,
TokenDepositInstruction {
amount: deposit_amount,
reduce_only: false,
account: account_0,
owner,
token_account: payer_mint_accounts[1],
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
.unwrap();
}
{
let deposit_amount = initial_token_deposit;
send_tx(
solana,
TokenDepositInstruction {
amount: deposit_amount,
reduce_only: false,
account: account_1,
owner,
token_account: payer_mint_accounts[0],
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
.unwrap();
send_tx(
solana,
TokenDepositInstruction {
amount: deposit_amount,
reduce_only: false,
account: account_1,
owner,
token_account: payer_mint_accounts[1],
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
.unwrap();
}
.await;
//
// TEST: Create a perp market
@ -141,6 +64,7 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> {
admin,
payer,
perp_market_index: 0,
settle_token_index: 3,
quote_lot_size: 10,
base_lot_size: 100,
maint_base_asset_weight: 0.975,
@ -152,7 +76,7 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> {
taker_fee: 0.000,
settle_pnl_limit_factor: 0.2,
settle_pnl_limit_window_size_ts: 24 * 60 * 60,
..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, &tokens[0]).await
..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, &base_token0).await
},
)
.await
@ -171,6 +95,7 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> {
admin,
payer,
perp_market_index: 1,
settle_token_index: 3,
quote_lot_size: 10,
base_lot_size: 100,
maint_base_asset_weight: 0.975,
@ -182,7 +107,7 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> {
taker_fee: 0.000,
settle_pnl_limit_factor: 0.2,
settle_pnl_limit_window_size_ts: 24 * 60 * 60,
..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, &tokens[1]).await
..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, &base_token1).await
},
)
.await
@ -194,7 +119,7 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> {
};
// Set the initial oracle price
set_bank_stub_oracle_price(solana, group, &tokens[0], admin, 1000.0).await;
set_bank_stub_oracle_price(solana, group, &base_token0, admin, 1000.0).await;
//
// Place orders and create a position
@ -241,32 +166,19 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> {
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
assert_eq!(mango_account_0.perps[0].base_position_lots(), 1);
assert_eq!(
mango_account_0.perps[0].quote_position_native().round(),
-100_020
);
assert!(assert_equal(
mango_account_0.perps[0].quote_position_native(),
-100_020.0,
0.01
));
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
assert_eq!(mango_account_1.perps[0].base_position_lots(), -1);
assert_eq!(mango_account_1.perps[0].quote_position_native(), 100_000);
// Bank must be valid for quote currency
let result = send_tx(
solana,
PerpSettleFeesInstruction {
account: account_0,
perp_market,
settle_bank: tokens[1].bank,
max_settle_amount: u64::MAX,
},
)
.await;
assert_mango_error(
&result,
MangoError::InvalidBank.into(),
"Bank must be valid for quote currency".to_string(),
);
assert!(assert_equal(
mango_account_1.perps[0].quote_position_native(),
100_000.0,
0.01
));
// Cannot settle position that does not exist
let result = send_tx(
@ -274,7 +186,6 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> {
PerpSettleFeesInstruction {
account: account_1,
perp_market: perp_market_2,
settle_bank: tokens[0].bank,
max_settle_amount: u64::MAX,
},
)
@ -292,7 +203,6 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> {
PerpSettleFeesInstruction {
account: account_1,
perp_market: perp_market,
settle_bank: tokens[0].bank,
max_settle_amount: 0,
},
)
@ -321,7 +231,7 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> {
}
// Try and settle with high price
set_bank_stub_oracle_price(solana, group, &tokens[0], admin, 1200.0).await;
set_bank_stub_oracle_price(solana, group, base_token0, admin, 1200.0).await;
// Account must have a loss, should not settle anything and instead return early
send_tx(
@ -329,7 +239,6 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> {
PerpSettleFeesInstruction {
account: account_0,
perp_market,
settle_bank: tokens[0].bank,
max_settle_amount: u64::MAX,
},
)
@ -338,14 +247,20 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> {
// No change
{
let perp_market = solana.get_account::<PerpMarket>(perp_market).await;
assert_eq!(
get_pnl_native(&mango_account_0.perps[0], &perp_market, I80F48::from(1200)).round(),
19980
);
assert_eq!(
get_pnl_native(&mango_account_1.perps[0], &perp_market, I80F48::from(1200)),
-20000
);
assert!(assert_equal(
mango_account_0.perps[0]
.unsettled_pnl(&perp_market, I80F48::from(1200))
.unwrap(),
19980.0, // 1*100*(1200-1000) - (20 in fees)
0.01
));
assert!(assert_equal(
mango_account_1.perps[0]
.unsettled_pnl(&perp_market, I80F48::from(1200))
.unwrap(),
-20000.0,
0.01
));
}
// TODO: Difficult to test health due to fees being so small. Need alternative
@ -356,7 +271,6 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> {
// account: account_1,
// perp_market,
// oracle: tokens[0].oracle,
// settle_bank: tokens[0].bank,
// max_settle_amount: I80F48::MAX,
// },
// )
@ -369,25 +283,30 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> {
// );
// Change the oracle to a more reasonable price
set_bank_stub_oracle_price(solana, group, &tokens[0], admin, 1005.0).await;
set_bank_stub_oracle_price(solana, group, base_token0, admin, 1005.0).await;
let expected_pnl_0 = I80F48::from(480); // Less due to fees
let expected_pnl_0 = I80F48::from(500 - 20);
let expected_pnl_1 = I80F48::from(-500);
{
let perp_market = solana.get_account::<PerpMarket>(perp_market).await;
assert_eq!(
get_pnl_native(&mango_account_0.perps[0], &perp_market, I80F48::from(1005)).round(),
mango_account_0.perps[0]
.unsettled_pnl(&perp_market, I80F48::from_num(1005))
.unwrap()
.round(),
expected_pnl_0
);
assert_eq!(
get_pnl_native(&mango_account_1.perps[0], &perp_market, I80F48::from(1005)),
mango_account_1.perps[0]
.unsettled_pnl(&perp_market, I80F48::from_num(1005))
.unwrap(),
expected_pnl_1
);
}
// Check the fees accrued
let initial_fees = I80F48::from(20);
let initial_fees = I80F48::from_num(20.0);
{
let perp_market = solana.get_account::<PerpMarket>(perp_market).await;
assert_eq!(
@ -409,7 +328,6 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> {
PerpSettleFeesInstruction {
account: account_1,
perp_market,
settle_bank: tokens[0].bank,
max_settle_amount: partial_settle_amount,
},
)
@ -434,8 +352,8 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> {
);
assert_eq!(
mango_account_1.tokens[0].native(&bank).round(),
I80F48::from(initial_token_deposit - partial_settle_amount),
mango_account_1.tokens[1].native(&bank).round(),
-I80F48::from(partial_settle_amount),
"account 1 token native position decreased (loss) by max_settle_amount"
);
@ -463,7 +381,6 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> {
PerpSettleFeesInstruction {
account: account_1,
perp_market,
settle_bank: tokens[0].bank,
max_settle_amount: u64::MAX,
},
)
@ -488,8 +405,8 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> {
);
assert_eq!(
mango_account_1.tokens[0].native(&bank).round(),
I80F48::from(initial_token_deposit) - initial_fees,
mango_account_1.tokens[1].native(&bank).round(),
-initial_fees,
"account 1 token native position decreased (loss)"
);

View File

@ -189,8 +189,15 @@ async fn derive_health_check_remaining_account_metas(
.unwrap();
}
if let Some(affected_perp_market_index) = affected_perp_market_index {
let pm: PerpMarket = account_loader
.load(&get_perp_market_address_by_index(
account.fixed.group,
affected_perp_market_index,
))
.await
.unwrap();
adjusted_account
.ensure_perp_position(affected_perp_market_index, QUOTE_TOKEN_INDEX)
.ensure_perp_position(affected_perp_market_index, pm.settle_token_index)
.unwrap();
}
@ -3583,7 +3590,6 @@ pub struct PerpSettlePnlInstruction {
pub account_a: Pubkey,
pub account_b: Pubkey,
pub perp_market: Pubkey,
pub settle_bank: Pubkey,
}
#[async_trait::async_trait(?Send)]
impl ClientInstruction for PerpSettlePnlInstruction {
@ -3597,7 +3603,6 @@ impl ClientInstruction for PerpSettlePnlInstruction {
let instruction = Self::Instruction {};
let perp_market: PerpMarket = account_loader.load(&self.perp_market).await.unwrap();
let settle_bank: Bank = account_loader.load(&self.settle_bank).await.unwrap();
let account_a = account_loader
.load_mango_account(&self.account_a)
.await
@ -3616,6 +3621,12 @@ impl ClientInstruction for PerpSettlePnlInstruction {
0,
)
.await;
let settle_mint_info = get_mint_info_by_token_index(
&account_loader,
&account_a,
perp_market.settle_token_index,
)
.await;
let accounts = Self::Accounts {
group: perp_market.group,
@ -3625,8 +3636,8 @@ impl ClientInstruction for PerpSettlePnlInstruction {
account_a: self.account_a,
account_b: self.account_b,
oracle: perp_market.oracle,
settle_bank: self.settle_bank,
settle_oracle: settle_bank.oracle,
settle_bank: settle_mint_info.first_bank(),
settle_oracle: settle_mint_info.oracle,
};
let mut instruction = make_instruction(program_id, &accounts, &instruction);
@ -3679,7 +3690,6 @@ impl ClientInstruction for PerpForceClosePositionInstruction {
pub struct PerpSettleFeesInstruction {
pub account: Pubkey,
pub perp_market: Pubkey,
pub settle_bank: Pubkey,
pub max_settle_amount: u64,
}
#[async_trait::async_trait(?Send)]
@ -3696,7 +3706,6 @@ impl ClientInstruction for PerpSettleFeesInstruction {
};
let perp_market: PerpMarket = account_loader.load(&self.perp_market).await.unwrap();
let settle_bank: Bank = account_loader.load(&self.settle_bank).await.unwrap();
let account = account_loader
.load_mango_account(&self.account)
.await
@ -3709,14 +3718,17 @@ impl ClientInstruction for PerpSettleFeesInstruction {
Some(perp_market.perp_market_index),
)
.await;
let settle_mint_info =
get_mint_info_by_token_index(&account_loader, &account, perp_market.settle_token_index)
.await;
let accounts = Self::Accounts {
group: perp_market.group,
perp_market: self.perp_market,
account: self.account,
oracle: perp_market.oracle,
settle_bank: self.settle_bank,
settle_oracle: settle_bank.oracle,
settle_bank: settle_mint_info.first_bank(),
settle_oracle: settle_mint_info.oracle,
};
let mut instruction = make_instruction(program_id, &accounts, &instruction);
instruction.accounts.extend(health_check_metas);
@ -3819,17 +3831,9 @@ impl ClientInstruction for PerpLiqBaseOrPositivePnlInstruction {
)
.await;
let group = account_loader.load::<Group>(&group_key).await.unwrap();
let quote_mint_info = Pubkey::find_program_address(
&[
b"MintInfo".as_ref(),
group_key.as_ref(),
group.insurance_mint.as_ref(),
],
&program_id,
)
.0;
let quote_mint_info: MintInfo = account_loader.load(&quote_mint_info).await.unwrap();
let settle_mint_info =
get_mint_info_by_token_index(&account_loader, &liqee, perp_market.settle_token_index)
.await;
let accounts = Self::Accounts {
group: group_key,
@ -3838,9 +3842,9 @@ impl ClientInstruction for PerpLiqBaseOrPositivePnlInstruction {
liqor: self.liqor,
liqor_owner: self.liqor_owner.pubkey(),
liqee: self.liqee,
settle_bank: quote_mint_info.first_bank(),
settle_vault: quote_mint_info.first_vault(),
settle_oracle: quote_mint_info.oracle,
settle_bank: settle_mint_info.first_bank(),
settle_vault: settle_mint_info.first_vault(),
settle_oracle: settle_mint_info.oracle,
};
let mut instruction = make_instruction(program_id, &accounts, &instruction);
instruction.accounts.extend(health_check_metas);
@ -3862,8 +3866,8 @@ pub struct PerpLiqNegativePnlOrBankruptcyInstruction {
}
#[async_trait::async_trait(?Send)]
impl ClientInstruction for PerpLiqNegativePnlOrBankruptcyInstruction {
type Accounts = mango_v4::accounts::PerpLiqNegativePnlOrBankruptcy;
type Instruction = mango_v4::instruction::PerpLiqNegativePnlOrBankruptcy;
type Accounts = mango_v4::accounts::PerpLiqNegativePnlOrBankruptcyV2;
type Instruction = mango_v4::instruction::PerpLiqNegativePnlOrBankruptcyV2;
async fn to_instruction(
&self,
account_loader: impl ClientAccountLoader + 'async_trait,
@ -3895,16 +3899,11 @@ impl ClientInstruction for PerpLiqNegativePnlOrBankruptcyInstruction {
.await;
let group = account_loader.load::<Group>(&group_key).await.unwrap();
let quote_mint_info = Pubkey::find_program_address(
&[
b"MintInfo".as_ref(),
group_key.as_ref(),
group.insurance_mint.as_ref(),
],
&program_id,
)
.0;
let quote_mint_info: MintInfo = account_loader.load(&quote_mint_info).await.unwrap();
let settle_mint_info =
get_mint_info_by_token_index(&account_loader, &liqee, perp_market.settle_token_index)
.await;
let insurance_mint_info =
get_mint_info_by_token_index(&account_loader, &liqee, QUOTE_TOKEN_INDEX).await;
let accounts = Self::Accounts {
group: group_key,
@ -3913,10 +3912,13 @@ impl ClientInstruction for PerpLiqNegativePnlOrBankruptcyInstruction {
liqee: self.liqee,
perp_market: self.perp_market,
oracle: perp_market.oracle,
settle_bank: quote_mint_info.first_bank(),
settle_vault: quote_mint_info.first_vault(),
settle_oracle: quote_mint_info.oracle,
settle_bank: settle_mint_info.first_bank(),
settle_vault: settle_mint_info.first_vault(),
settle_oracle: settle_mint_info.oracle,
insurance_vault: group.insurance_vault,
insurance_bank: insurance_mint_info.first_bank(),
insurance_bank_vault: insurance_mint_info.first_vault(),
insurance_oracle: insurance_mint_info.oracle,
token_program: Token::id(),
};
let mut instruction = make_instruction(program_id, &accounts, &instruction);

View File

@ -2,7 +2,6 @@
use bytemuck::{bytes_of, Contiguous};
use fixed::types::I80F48;
use mango_v4::state::{PerpMarket, PerpPosition};
use solana_program::instruction::InstructionError;
use solana_program::program_error::ProgramError;
use solana_sdk::pubkey::Pubkey;
@ -81,17 +80,6 @@ impl Into<Keypair> for &TestKeypair {
}
}
pub fn get_pnl_native(
perp_position: &PerpPosition,
perp_market: &PerpMarket,
oracle_price: I80F48,
) -> I80F48 {
let contract_size = perp_market.base_lot_size;
let new_quote_pos =
I80F48::from_num(-perp_position.base_position_lots() * contract_size) * oracle_price;
perp_position.quote_position_native() - new_quote_pos
}
pub fn assert_mango_error<T>(
result: &Result<T, TransportError>,
expected_error: u32,

View File

@ -70,15 +70,11 @@ async function debugUser(
);
console.log(
'mangoAccount.getAssetsValue() ' +
toUiDecimalsForQuote(
mangoAccount.getAssetsValue(group, HealthType.init)!.toNumber(),
),
toUiDecimalsForQuote(mangoAccount.getAssetsValue(group)!.toNumber()),
);
console.log(
'mangoAccount.getLiabsValue() ' +
toUiDecimalsForQuote(
mangoAccount.getLiabsValue(group, HealthType.init)!.toNumber(),
),
toUiDecimalsForQuote(mangoAccount.getLiabsValue(group)!.toNumber()),
);
async function getMaxWithdrawWithBorrowForTokenUiWrapper(

View File

@ -3,7 +3,6 @@ import { Connection, Keypair, PublicKey } from '@solana/web3.js';
import { expect } from 'chai';
import fs from 'fs';
import { Group } from '../../src/accounts/group';
import { HealthType } from '../../src/accounts/mangoAccount';
import { PerpOrderSide, PerpOrderType } from '../../src/accounts/perp';
import { MangoClient } from '../../src/client';
import { MANGO_V4_ID } from '../../src/constants';
@ -330,15 +329,11 @@ async function main(): Promise<void> {
);
console.log(
'...mangoAccount.getAssetsVal() ' +
toUiDecimalsForQuote(
mangoAccount.getAssetsValue(group, HealthType.init)!.toNumber(),
),
toUiDecimalsForQuote(mangoAccount.getAssetsValue(group)!.toNumber()),
);
console.log(
'...mangoAccount.getLiabsVal() ' +
toUiDecimalsForQuote(
mangoAccount.getLiabsValue(group, HealthType.init)!.toNumber(),
),
toUiDecimalsForQuote(mangoAccount.getLiabsValue(group)!.toNumber()),
);
console.log(
'...mangoAccount.getMaxWithdrawWithBorrowForToken(group, "SOL") ' +

View File

@ -1,5 +1,5 @@
import { AnchorProvider, Wallet } from '@coral-xyz/anchor';
import { Connection, Keypair } from '@solana/web3.js';
import { Cluster, Connection, Keypair } from '@solana/web3.js';
import fs from 'fs';
import { MangoClient } from '../../src/client';
import { MANGO_V4_ID } from '../../src/constants';
@ -11,6 +11,8 @@ import { MANGO_V4_ID } from '../../src/constants';
// Use to close only a specific group by number. Use "all" to close all groups.
const GROUP_NUM = process.env.GROUP_NUM;
const CLUSTER = process.env.CLUSTER || 'mainnet-beta';
async function main() {
const options = AnchorProvider.defaultOptions();
const connection = new Connection(process.env.MB_CLUSTER_URL!, options);
@ -25,8 +27,8 @@ async function main() {
const adminProvider = new AnchorProvider(connection, adminWallet, options);
const client = await MangoClient.connect(
adminProvider,
'mainnet-beta',
MANGO_V4_ID['mainnet-beta'],
CLUSTER as Cluster,
MANGO_V4_ID[CLUSTER],
{
idsSource: 'get-program-accounts',
prioritizationFee: 5,

View File

@ -3,8 +3,8 @@ import { Connection, Keypair } from '@solana/web3.js';
import fs from 'fs';
import { HealthType } from '../../src/accounts/mangoAccount';
import {
MangoClient,
MANGO_V4_ID,
MangoClient,
toUiDecimalsForQuote,
} from '../../src/index';
@ -83,15 +83,11 @@ async function main() {
);
console.log(
'mangoAccount.getAssetsVal() ' +
toUiDecimalsForQuote(
mangoAccount.getAssetsValue(group, HealthType.init).toNumber(),
),
toUiDecimalsForQuote(mangoAccount.getAssetsValue(group).toNumber()),
);
console.log(
'mangoAccount.getLiabsVal() ' +
toUiDecimalsForQuote(
mangoAccount.getLiabsValue(group, HealthType.init).toNumber(),
),
toUiDecimalsForQuote(mangoAccount.getLiabsValue(group).toNumber()),
);
console.log(

View File

@ -0,0 +1,91 @@
import { AnchorProvider, Wallet } from '@coral-xyz/anchor';
import { Cluster, Connection, Keypair } from '@solana/web3.js';
import fs from 'fs';
import { MangoClient } from '../../src/client';
import { MANGO_V4_ID } from '../../src/constants';
//
// example script to close accounts - banks, markets, group etc. which require admin to be the signer
//
const GROUP_NUM = Number(process.env.GROUP_NUM || 200);
const CLUSTER = process.env.CLUSTER || 'mainnet-beta';
async function main() {
const options = AnchorProvider.defaultOptions();
const connection = new Connection(process.env.CLUSTER_URL!, options);
const admin = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(fs.readFileSync(process.env.PAYER_KEYPAIR!, 'utf-8')),
),
);
const adminWallet = new Wallet(admin);
console.log(`Admin ${adminWallet.publicKey.toBase58()}`);
const adminProvider = new AnchorProvider(connection, adminWallet, options);
const client = await MangoClient.connect(
adminProvider,
CLUSTER as Cluster,
MANGO_V4_ID[CLUSTER],
{
idsSource: 'get-program-accounts',
prioritizationFee: 5,
},
);
const groups = await (async () => {
return [
await client.getGroupForCreator(admin.publicKey, Number(GROUP_NUM)),
];
})();
for (const group of groups) {
console.log(`Group ${group.publicKey}`);
let sig;
// deregister all serum markets
for (const market of group.serum3MarketsMapByExternal.values()) {
sig = await client.serum3deregisterMarket(
group,
market.serumMarketExternal,
);
console.log(
`Deregistered serum market ${market.name}, sig https://explorer.solana.com/tx/${sig}`,
);
}
// close all perp markets
for (const market of group.perpMarketsMapByMarketIndex.values()) {
sig = await client.perpCloseMarket(group, market.perpMarketIndex);
console.log(
`Closed perp market ${market.name}, sig https://explorer.solana.com/tx/${sig}`,
);
}
// close all banks
for (const banks of group.banksMapByMint.values()) {
sig = await client.tokenDeregister(group, banks[0].mint);
console.log(
`Removed token ${banks[0].name}, sig https://explorer.solana.com/tx/${sig}`,
);
}
// close stub oracles
const stubOracles = await client.getStubOracle(group);
for (const stubOracle of stubOracles) {
sig = await client.stubOracleClose(group, stubOracle.publicKey);
console.log(
`Closed stub oracle ${stubOracle.publicKey}, sig https://explorer.solana.com/tx/${sig}`,
);
}
// finally, close the group
sig = await client.groupClose(group);
console.log(`Closed group, sig https://explorer.solana.com/tx/${sig}`);
}
process.exit();
}
main();

View File

@ -1,6 +1,7 @@
import { AnchorProvider, Wallet } from '@coral-xyz/anchor';
import {
AddressLookupTableProgram,
Cluster,
Connection,
Keypair,
PublicKey,
@ -16,33 +17,38 @@ import { MANGO_V4_ID } from '../../src/constants';
// default to group 1, to not conflict with the normal group
const GROUP_NUM = Number(process.env.GROUP_NUM || 200);
const CLUSTER = process.env.CLUSTER || 'mainnet-beta';
const MINTS = JSON.parse(process.env.MINTS || '').map((s) => new PublicKey(s));
const SERUM_MARKETS = JSON.parse(process.env.SERUM_MARKETS || '').map(
(s) => new PublicKey(s),
);
const MAINNET_MINTS = new Map([
['USDC', 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'],
['ETH', '7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs'],
['SOL', 'So11111111111111111111111111111111111111112'],
['MNGO', 'MangoCzJ36AjZyKwVj3VnYU4GTonjfVEnJmvvWaxLac'],
['USDC', MINTS[0]],
['ETH', MINTS[1]],
['SOL', MINTS[2]],
['MNGO', MINTS[3]],
]);
const STUB_PRICES = new Map([
['USDC', 1.0],
['ETH', 1200.0], // eth and usdc both have 6 decimals
['SOL', 0.015], // sol has 9 decimals, equivalent to $15 per SOL
['MNGO', 0.02], // same price/decimals as SOL for convenience
['MNGO', 0.02],
]);
// External markets are matched with those in https://github.com/blockworks-foundation/mango-client-v3/blob/main/src/ids.json
// and verified to have best liquidity for pair on https://openserum.io/
const MAINNET_SERUM3_MARKETS = new Map([
['ETH/USDC', 'FZxi3yWkE5mMjyaZj6utmYL54QQYfMCKMcLaQZq4UwnA'],
['SOL/USDC', '8BnEgHoWFysVcuFFX7QztDmzuH8r5ZFvyP3sYwn1XTh6'],
['ETH/USDC', SERUM_MARKETS[0]],
['SOL/USDC', SERUM_MARKETS[1]],
]);
const MIN_VAULT_TO_DEPOSITS_RATIO = 0.2;
const NET_BORROWS_WINDOW_SIZE_TS = 24 * 60 * 60;
const NET_BORROWS_LIMIT_NATIVE = 1 * Math.pow(10, 7) * Math.pow(10, 6);
async function main() {
async function main(): Promise<void> {
const options = AnchorProvider.defaultOptions();
options.commitment = 'processed';
options.preflightCommitment = 'finalized';
@ -50,9 +56,7 @@ async function main() {
const admin = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(
fs.readFileSync(process.env.MANGO_MAINNET_PAYER_KEYPAIR!, 'utf-8'),
),
JSON.parse(fs.readFileSync(process.env.PAYER_KEYPAIR!, 'utf-8')),
),
);
const adminWallet = new Wallet(admin);
@ -60,8 +64,8 @@ async function main() {
const adminProvider = new AnchorProvider(connection, adminWallet, options);
const client = await MangoClient.connect(
adminProvider,
'mainnet-beta',
MANGO_V4_ID['mainnet-beta'],
CLUSTER as Cluster,
MANGO_V4_ID[CLUSTER],
{
idsSource: 'get-program-accounts',
prioritizationFee: 100,
@ -81,8 +85,8 @@ async function main() {
console.log(`...registered group ${group.publicKey}`);
// stub oracles
let oracles = new Map();
for (let [name, mint] of MAINNET_MINTS) {
const oracles = new Map();
for (const [name, mint] of MAINNET_MINTS) {
console.log(`Creating stub oracle for ${name}...`);
const mintPk = new PublicKey(mint);
try {
@ -111,13 +115,13 @@ async function main() {
// register token 0
console.log(`Registering USDC...`);
const usdcMainnetMint = new PublicKey(MAINNET_MINTS.get('USDC')!);
const usdcMainnetOracle = oracles.get('USDC');
const usdcMint = new PublicKey(MAINNET_MINTS.get('USDC')!);
const usdcOracle = oracles.get('USDC');
try {
await client.tokenRegister(
group,
usdcMainnetMint,
usdcMainnetOracle,
usdcMint,
usdcOracle,
defaultOracleConfig,
0,
'USDC',
@ -140,13 +144,13 @@ async function main() {
// register token 1
console.log(`Registering ETH...`);
const ethMainnetMint = new PublicKey(MAINNET_MINTS.get('ETH')!);
const ethMainnetOracle = oracles.get('ETH');
const ethMint = new PublicKey(MAINNET_MINTS.get('ETH')!);
const ethOracle = oracles.get('ETH');
try {
await client.tokenRegister(
group,
ethMainnetMint,
ethMainnetOracle,
ethMint,
ethOracle,
defaultOracleConfig,
1,
'ETH',
@ -169,13 +173,13 @@ async function main() {
// register token 2
console.log(`Registering SOL...`);
const solMainnetMint = new PublicKey(MAINNET_MINTS.get('SOL')!);
const solMainnetOracle = oracles.get('SOL');
const solMint = new PublicKey(MAINNET_MINTS.get('SOL')!);
const solOracle = oracles.get('SOL');
try {
await client.tokenRegister(
group,
solMainnetMint,
solMainnetOracle,
solMint,
solOracle,
defaultOracleConfig,
2, // tokenIndex
'SOL',
@ -216,11 +220,11 @@ async function main() {
}
console.log('Registering MNGO-PERP market...');
const mngoMainnetOracle = oracles.get('MNGO');
const mngoOracle = oracles.get('MNGO');
try {
await client.perpCreateMarket(
group,
mngoMainnetOracle,
mngoOracle,
0,
'MNGO-PERP',
defaultOracleConfig,
@ -260,7 +264,10 @@ async function main() {
main();
async function createAndPopulateAlt(client: MangoClient, admin: Keypair) {
async function createAndPopulateAlt(
client: MangoClient,
admin: Keypair,
): Promise<void> {
let group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM);
const connection = client.program.provider.connection;
@ -289,9 +296,35 @@ async function createAndPopulateAlt(client: MangoClient, admin: Keypair) {
}
}
async function extendTable(addresses: PublicKey[]): Promise<void> {
await group.reloadAll(client);
const alt = await client.program.provider.connection.getAddressLookupTable(
group.addressLookupTables[0],
);
addresses = addresses.filter(
(newAddress) =>
alt.value?.state.addresses &&
alt.value?.state.addresses.findIndex((addressInALt) =>
addressInALt.equals(newAddress),
) === -1,
);
if (addresses.length === 0) {
return;
}
const extendIx = AddressLookupTableProgram.extendLookupTable({
lookupTable: group.addressLookupTables[0],
payer: admin.publicKey,
authority: admin.publicKey,
addresses,
});
const sig = await client.sendAndConfirmTransaction([extendIx]);
console.log(`https://explorer.solana.com/tx/${sig}`);
}
// Extend using mango v4 relevant pub keys
try {
let bankAddresses = Array.from(group.banksMapByMint.values())
const bankAddresses = Array.from(group.banksMapByMint.values())
.flat()
.map((bank) => [bank.publicKey, bank.oracle, bank.vault])
.flat()
@ -301,13 +334,13 @@ async function createAndPopulateAlt(client: MangoClient, admin: Keypair) {
.map((mintInfo) => mintInfo.publicKey),
);
let serum3MarketAddresses = Array.from(
const serum3MarketAddresses = Array.from(
group.serum3MarketsMapByExternal.values(),
)
.flat()
.map((serum3Market) => serum3Market.publicKey);
let serum3ExternalMarketAddresses = Array.from(
const serum3ExternalMarketAddresses = Array.from(
group.serum3ExternalMarketsMap.values(),
)
.flat()
@ -318,7 +351,7 @@ async function createAndPopulateAlt(client: MangoClient, admin: Keypair) {
])
.flat();
let perpMarketAddresses = Array.from(
const perpMarketAddresses = Array.from(
group.perpMarketsMapByMarketIndex.values(),
)
.flat()
@ -331,33 +364,6 @@ async function createAndPopulateAlt(client: MangoClient, admin: Keypair) {
])
.flat();
async function extendTable(addresses: PublicKey[]) {
await group.reloadAll(client);
const alt =
await client.program.provider.connection.getAddressLookupTable(
group.addressLookupTables[0],
);
addresses = addresses.filter(
(newAddress) =>
alt.value?.state.addresses &&
alt.value?.state.addresses.findIndex((addressInALt) =>
addressInALt.equals(newAddress),
) === -1,
);
if (addresses.length === 0) {
return;
}
const extendIx = AddressLookupTableProgram.extendLookupTable({
lookupTable: group.addressLookupTables[0],
payer: admin.publicKey,
authority: admin.publicKey,
addresses,
});
const sig = await client.sendAndConfirmTransaction([extendIx]);
console.log(`https://explorer.solana.com/tx/${sig}`);
}
console.log(`ALT: extending using mango v4 relevant public keys`);
await extendTable(bankAddresses);
await extendTable(serum3MarketAddresses);

View File

@ -0,0 +1,212 @@
import { BN, AnchorProvider, Wallet } from '@coral-xyz/anchor';
import {
Transaction,
SystemProgram,
AddressLookupTableProgram,
Connection,
Keypair,
PublicKey,
sendAndConfirmTransaction,
} from '@solana/web3.js';
import * as splToken from '@solana/spl-token';
import * as serum from '@project-serum/serum';
import fs from 'fs';
import { MangoClient } from '../../src/client';
import { MANGO_V4_ID, OPENBOOK_PROGRAM_ID } from '../../src/constants';
import { connect } from 'http2';
import { generateSerum3MarketExternalVaultSignerAddress } from '../../src/accounts/serum3';
//
// Script which creates three mints and two serum3 markets relating them
//
function getVaultOwnerAndNonce(
market: PublicKey,
programId: PublicKey,
): [PublicKey, BN] {
const nonce = new BN(0);
// eslint-disable-next-line no-constant-condition
while (true) {
try {
const vaultOwner = PublicKey.createProgramAddressSync(
[market.toBuffer(), nonce.toArrayLike(Buffer, 'le', 8)],
programId,
);
return [vaultOwner, nonce];
} catch (e) {
nonce.iaddn(1);
}
}
}
async function main(): Promise<void> {
Error.stackTraceLimit = 10000;
const options = AnchorProvider.defaultOptions();
options.commitment = 'processed';
options.preflightCommitment = 'finalized';
const connection = new Connection(process.env.CLUSTER_URL!, options);
const admin = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(fs.readFileSync(process.env.PAYER_KEYPAIR!, 'utf-8')),
),
);
console.log(`Admin ${admin.publicKey.toBase58()}`);
// Make mints
const mints = await Promise.all(
Array(4)
.fill(null)
.map(() =>
splToken.createMint(connection, admin, admin.publicKey, null, 6),
),
);
// Mint some tokens to the admin
for (const mint of mints) {
const tokenAccount = await splToken.createAssociatedTokenAccountIdempotent(
connection,
admin,
mint,
admin.publicKey,
);
await splToken.mintTo(
connection,
admin,
mint,
tokenAccount,
admin,
1000 * 1e6,
);
}
//const mints = [new PublicKey('5aMD1uEcWnXnptwmyfxmTWHzx3KeMsZ7jmiJAZ3eiAdH'), new PublicKey('FijXcDUkgTiMsghQVpjRDBdUPtkrJfQdfRZkr6zLkdkW'), new PublicKey('3tVDfiFQAAT3rqLNMXUaH2p5X5R4fjz8LYEvFEQ9fDYB')]
// Make serum markets
const serumMarkets: PublicKey[] = [];
const quoteMint = mints[0];
for (const baseMint of mints.slice(1, 3)) {
const feeRateBps = 0.25; // don't think this does anything
const quoteDustThreshold = 100;
const baseLotSize = 1000;
const quoteLotSize = 1000;
const openbookProgramId = OPENBOOK_PROGRAM_ID.devnet;
const market = Keypair.generate();
const requestQueue = Keypair.generate();
const eventQueue = Keypair.generate();
const bids = Keypair.generate();
const asks = Keypair.generate();
const baseVault = Keypair.generate();
const quoteVault = Keypair.generate();
const [vaultOwner, vaultSignerNonce] = getVaultOwnerAndNonce(
market.publicKey,
openbookProgramId,
);
await splToken.createAccount(
connection,
admin,
baseMint,
vaultOwner,
baseVault,
);
await splToken.createAccount(
connection,
admin,
quoteMint,
vaultOwner,
quoteVault,
);
const tx = new Transaction();
tx.add(
SystemProgram.createAccount({
fromPubkey: admin.publicKey,
newAccountPubkey: market.publicKey,
lamports: await connection.getMinimumBalanceForRentExemption(
serum.Market.getLayout(openbookProgramId).span,
),
space: serum.Market.getLayout(openbookProgramId).span,
programId: openbookProgramId,
}),
SystemProgram.createAccount({
fromPubkey: admin.publicKey,
newAccountPubkey: requestQueue.publicKey,
lamports: await connection.getMinimumBalanceForRentExemption(5120 + 12),
space: 5120 + 12,
programId: openbookProgramId,
}),
SystemProgram.createAccount({
fromPubkey: admin.publicKey,
newAccountPubkey: eventQueue.publicKey,
lamports: await connection.getMinimumBalanceForRentExemption(
262144 + 12,
),
space: 262144 + 12,
programId: openbookProgramId,
}),
SystemProgram.createAccount({
fromPubkey: admin.publicKey,
newAccountPubkey: bids.publicKey,
lamports: await connection.getMinimumBalanceForRentExemption(
65536 + 12,
),
space: 65536 + 12,
programId: openbookProgramId,
}),
SystemProgram.createAccount({
fromPubkey: admin.publicKey,
newAccountPubkey: asks.publicKey,
lamports: await connection.getMinimumBalanceForRentExemption(
65536 + 12,
),
space: 65536 + 12,
programId: openbookProgramId,
}),
serum.DexInstructions.initializeMarket({
market: market.publicKey,
requestQueue: requestQueue.publicKey,
eventQueue: eventQueue.publicKey,
bids: bids.publicKey,
asks: asks.publicKey,
baseVault: baseVault.publicKey,
quoteVault: quoteVault.publicKey,
baseMint,
quoteMint,
baseLotSize: new BN(baseLotSize),
quoteLotSize: new BN(quoteLotSize),
feeRateBps,
vaultSignerNonce,
quoteDustThreshold: new BN(quoteDustThreshold),
programId: openbookProgramId,
authority: undefined,
}),
);
await sendAndConfirmTransaction(connection, tx, [
admin,
market,
requestQueue,
eventQueue,
bids,
asks,
]);
serumMarkets.push(market.publicKey);
}
console.log(
"MINTS='[" + mints.map((pk) => '"' + pk.toBase58() + '"').join(',') + "]'",
);
console.log(
"SERUM_MARKETS='[" +
serumMarkets.map((pk) => '"' + pk.toBase58() + '"').join(',') +
"]'",
);
process.exit();
}
main();

View File

@ -1,5 +1,6 @@
import { AnchorProvider, BN, Wallet } from '@coral-xyz/anchor';
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
import { Cluster, Connection, Keypair, PublicKey } from '@solana/web3.js';
import { assert } from 'console';
import fs from 'fs';
import { Bank } from '../../src/accounts/bank';
import { MangoAccount } from '../../src/accounts/mangoAccount';
@ -26,6 +27,7 @@ import { MANGO_V4_ID } from '../../src/constants';
//
const GROUP_NUM = Number(process.env.GROUP_NUM || 200);
const CLUSTER = process.env.CLUSTER || 'mainnet-beta';
// native prices
const PRICES = {
@ -63,17 +65,15 @@ async function main() {
const admin = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(
fs.readFileSync(process.env.MANGO_MAINNET_PAYER_KEYPAIR!, 'utf-8'),
),
JSON.parse(fs.readFileSync(process.env.PAYER_KEYPAIR!, 'utf-8')),
),
);
const userWallet = new Wallet(admin);
const userProvider = new AnchorProvider(connection, userWallet, options);
const client = await MangoClient.connect(
userProvider,
'mainnet-beta',
MANGO_V4_ID['mainnet-beta'],
CLUSTER as Cluster,
MANGO_V4_ID[CLUSTER],
{
idsSource: 'get-program-accounts',
prioritizationFee: 100,
@ -136,12 +136,12 @@ async function main() {
// create account
console.log(`Creating mangoaccount...`);
let mangoAccount = await createMangoAccount(name);
const mangoAccount = await createMangoAccount(name);
console.log(
`...created mangoAccount ${mangoAccount.publicKey} for ${name}`,
);
for (let [assetName, assetAmount] of assets) {
for (const [assetName, assetAmount] of assets) {
const assetMint = new PublicKey(MAINNET_MINTS.get(assetName)!);
await client.tokenDepositNative(
group,
@ -152,7 +152,7 @@ async function main() {
await mangoAccount.reload(client);
}
for (let [liabName, liabAmount] of liabs) {
for (const [liabName, liabAmount] of liabs) {
const liabMint = new PublicKey(MAINNET_MINTS.get(liabName)!);
// temporarily drop the borrowed token value, so the borrow goes through
@ -190,7 +190,7 @@ async function main() {
const name = 'LIQTEST, serum orders';
console.log(`Creating mangoaccount...`);
let mangoAccount = await createMangoAccount(name);
const mangoAccount = await createMangoAccount(name);
console.log(
`...created mangoAccount ${mangoAccount.publicKey} for ${name}`,
);
@ -251,7 +251,7 @@ async function main() {
const name = 'LIQTEST, perp orders';
console.log(`Creating mangoaccount...`);
let mangoAccount = await createMangoAccount(name);
const mangoAccount = await createMangoAccount(name);
console.log(
`...created mangoAccount ${mangoAccount.publicKey} for ${name}`,
);
@ -274,7 +274,9 @@ async function main() {
await client.perpPlaceOrder(
group,
mangoAccount,
group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex!,
assertNotUndefined(
group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex,
),
PerpOrderSide.bid,
0.001, // ui price that won't get hit
3.0, // ui base quantity, 30 base lots, 3.0 MNGO, $0.06
@ -295,7 +297,7 @@ async function main() {
const name = 'LIQTEST, perp base pos';
console.log(`Creating mangoaccount...`);
let mangoAccount = await createMangoAccount(name);
const mangoAccount = await createMangoAccount(name);
console.log(
`...created mangoAccount ${mangoAccount.publicKey} for ${name}`,
);
@ -317,7 +319,9 @@ async function main() {
await client.perpPlaceOrder(
group,
fundingAccount,
group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex!,
assertNotUndefined(
group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex,
),
PerpOrderSide.ask,
0.03,
1.1, // ui base quantity, 11 base lots, $0.022 value, gain $0.033
@ -332,7 +336,9 @@ async function main() {
await client.perpPlaceOrder(
group,
mangoAccount,
group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex!,
assertNotUndefined(
group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex,
),
PerpOrderSide.bid,
0.03,
1.1, // ui base quantity, 11 base lots, $0.022 value, cost $0.033
@ -346,7 +352,9 @@ async function main() {
await client.perpConsumeAllEvents(
group,
group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex!,
assertNotUndefined(
group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex,
),
);
} finally {
await setBankPrice(collateralBank, PRICES['SOL']);
@ -358,7 +366,7 @@ async function main() {
const name = 'LIQTEST, perp positive pnl';
console.log(`Creating mangoaccount...`);
let mangoAccount = await createMangoAccount(name);
const mangoAccount = await createMangoAccount(name);
console.log(
`...created mangoAccount ${mangoAccount.publicKey} for ${name}`,
);
@ -464,7 +472,7 @@ async function main() {
const name = 'LIQTEST, perp negative pnl';
console.log(`Creating mangoaccount...`);
let mangoAccount = await createMangoAccount(name);
const mangoAccount = await createMangoAccount(name);
console.log(
`...created mangoAccount ${mangoAccount.publicKey} for ${name}`,
);
@ -558,4 +566,11 @@ async function main() {
process.exit();
}
function assertNotUndefined<T>(value: T | undefined): T {
if (value === undefined) {
throw new Error('Value was undefined');
}
return value;
}
main();

View File

@ -1,18 +1,18 @@
import { AnchorProvider, Wallet } from '@coral-xyz/anchor';
import { Connection, Keypair } from '@solana/web3.js';
import { Cluster, Connection, Keypair } from '@solana/web3.js';
import fs from 'fs';
import { MangoClient } from '../../src/client';
import { MANGO_V4_ID } from '../../src/constants';
//
// This script tries to withdraw all positive balances for all accounts
// by MANGO_MAINNET_PAYER_KEYPAIR in the group.
// by PAYER_KEYPAIR in the group.
//
const GROUP_NUM = Number(process.env.GROUP_NUM || 200);
const CLUSTER = process.env.CLUSTER || 'mainnet-beta';
const CLUSTER_URL = process.env.CLUSTER_URL;
const MANGO_MAINNET_PAYER_KEYPAIR =
process.env.MANGO_MAINNET_PAYER_KEYPAIR || '';
const PAYER_KEYPAIR = process.env.PAYER_KEYPAIR || '';
async function main() {
const options = AnchorProvider.defaultOptions();
@ -21,16 +21,14 @@ async function main() {
const connection = new Connection(CLUSTER_URL!, options);
const admin = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(fs.readFileSync(MANGO_MAINNET_PAYER_KEYPAIR, 'utf-8')),
),
Buffer.from(JSON.parse(fs.readFileSync(PAYER_KEYPAIR, 'utf-8'))),
);
const userWallet = new Wallet(admin);
const userProvider = new AnchorProvider(connection, userWallet, options);
const client = await MangoClient.connect(
userProvider,
'mainnet-beta',
MANGO_V4_ID['mainnet-beta'],
CLUSTER as Cluster,
MANGO_V4_ID[CLUSTER],
{
idsSource: 'get-program-accounts',
prioritizationFee: 100,
@ -49,8 +47,8 @@ async function main() {
console.log(group.toString());
let accounts = await client.getMangoAccountsForOwner(group, admin.publicKey);
for (let account of accounts) {
for (let serumOrders of account.serum3Active()) {
for (const account of accounts) {
for (const serumOrders of account.serum3Active()) {
const serumMarket = group.getSerum3MarketByMarketIndex(
serumOrders.marketIndex,
)!;
@ -63,7 +61,7 @@ async function main() {
await client.serum3CloseOpenOrders(group, account, serumExternal);
}
for (let perpPosition of account.perpActive()) {
for (const perpPosition of account.perpActive()) {
const perpMarket = group.findPerpMarket(perpPosition.marketIndex)!;
console.log(
`closing perp orders on: ${account} for market ${perpMarket.name}`,
@ -78,7 +76,7 @@ async function main() {
}
accounts = await client.getMangoAccountsForOwner(group, admin.publicKey);
for (let account of accounts) {
for (const account of accounts) {
// close account
try {
console.log(`closing account: ${account}`);

View File

@ -40,6 +40,11 @@ export interface BankForHealth {
scaledInitAssetWeight(price: I80F48): I80F48;
scaledInitLiabWeight(price: I80F48): I80F48;
nativeDeposits(): I80F48;
nativeBorrows(): I80F48;
depositWeightScaleStartQuote: number;
borrowWeightScaleStartQuote: number;
}
export class Bank implements BankForHealth {

View File

@ -17,6 +17,10 @@ function mockBankAndOracle(
initWeight: number,
price: number,
stablePrice: number,
deposits = 0,
borrows = 0,
borrowWeightScaleStartQuote = Number.MAX_SAFE_INTEGER,
depositWeightScaleStartQuote = Number.MAX_SAFE_INTEGER,
): BankForHealth {
return {
tokenIndex,
@ -26,13 +30,40 @@ function mockBankAndOracle(
initLiabWeight: I80F48.fromNumber(1 + initWeight),
price: I80F48.fromNumber(price),
stablePriceModel: { stablePrice: stablePrice } as StablePriceModel,
scaledInitAssetWeight: () => I80F48.fromNumber(1 - initWeight),
scaledInitLiabWeight: () => I80F48.fromNumber(1 + initWeight),
scaledInitAssetWeight: (price: I80F48): I80F48 => {
const depositsQuote = I80F48.fromNumber(deposits).mul(price);
if (
depositWeightScaleStartQuote >= Number.MAX_SAFE_INTEGER ||
depositsQuote.lte(I80F48.fromNumber(depositWeightScaleStartQuote))
) {
return I80F48.fromNumber(1 - initWeight);
}
return I80F48.fromNumber(1 - initWeight).mul(
I80F48.fromNumber(depositWeightScaleStartQuote).div(depositsQuote),
);
},
scaledInitLiabWeight: (price: I80F48): I80F48 => {
const borrowsQuote = I80F48.fromNumber(borrows).mul(price);
if (
borrowWeightScaleStartQuote >= Number.MAX_SAFE_INTEGER ||
borrowsQuote.lte(I80F48.fromNumber(borrowWeightScaleStartQuote))
) {
return I80F48.fromNumber(1 + initWeight);
}
return I80F48.fromNumber(1 + initWeight).mul(
borrowsQuote.div(I80F48.fromNumber(borrowWeightScaleStartQuote)),
);
},
nativeDeposits: () => I80F48.fromNumber(deposits),
nativeBorrows: () => I80F48.fromNumber(borrows),
borrowWeightScaleStartQuote: borrowWeightScaleStartQuote,
depositWeightScaleStartQuote: depositWeightScaleStartQuote,
};
}
function mockPerpMarket(
perpMarketIndex: number,
settleTokenIndex: number,
maintBaseWeight: number,
initBaseWeight: number,
baseLotSize: number,
@ -40,6 +71,7 @@ function mockPerpMarket(
): PerpMarket {
return {
perpMarketIndex,
settleTokenIndex: settleTokenIndex as TokenIndex,
maintBaseAssetWeight: I80F48.fromNumber(1 - maintBaseWeight),
initBaseAssetWeight: I80F48.fromNumber(1 - initBaseWeight),
maintBaseLiabWeight: I80F48.fromNumber(1 + maintBaseWeight),
@ -58,7 +90,7 @@ function mockPerpMarket(
describe('Health Cache', () => {
it('test_health0', () => {
const sourceBank: BankForHealth = mockBankAndOracle(
1 as TokenIndex,
0 as TokenIndex,
0.1,
0.2,
1,
@ -90,7 +122,7 @@ describe('Health Cache', () => {
} as any as OpenOrders,
);
const pM = mockPerpMarket(9, 0.1, 0.2, 10, targetBank.price.toNumber());
const pM = mockPerpMarket(9, 0, 0.1, 0.2, 10, targetBank.price.toNumber());
const pp = new PerpPosition(
pM.perpMarketIndex,
0,
@ -119,14 +151,16 @@ describe('Health Cache', () => {
const hc = new HealthCache([ti1, ti2], [si1], [pi1]);
// for bank1/oracle1, including open orders (scenario: bids execute)
const health1 = (100.0 + 1.0 + 2.0 + (20.0 + 15.0 * 5.0)) * 0.8;
// for bank2/oracle2
const health2 = (-10.0 + 3.0) * 5.0 * 1.5;
// for perp (scenario: bids execute)
const health3 =
// for bank1/oracle1
// including open orders (scenario: bids execute)
const serum1 = 1.0 + (20.0 + 15.0 * 5.0);
// and perp (scenario: bids execute)
const perp1 =
(3.0 + 7.0 + 1.0) * 10.0 * 5.0 * 0.8 +
(-310.0 + 2.0 * 100.0 - 7.0 * 10.0 * 5.0);
const health1 = (100.0 + serum1 + perp1) * 0.8;
// for bank2/oracle2
const health2 = (-10.0 + 3.0) * 5.0 * 1.5;
const health = hc.health(HealthType.init).toNumber();
console.log(
@ -137,7 +171,7 @@ describe('Health Cache', () => {
)}, case "test that includes all the side values (like referrer_rebates_accrued)"`,
);
expect(health - (health1 + health2 + health3)).lessThan(0.0000001);
expect(health - (health1 + health2)).lessThan(0.0000001);
});
it('test_health1', (done) => {
@ -146,24 +180,36 @@ describe('Health Cache', () => {
token1: number;
token2: number;
token3: number;
bs1: [number, number];
bs2: [number, number];
bs3: [number, number];
oo12: [number, number];
oo13: [number, number];
perp1: [number, number, number, number];
expectedHealth: number;
}): void {
const bank1: BankForHealth = mockBankAndOracle(
1 as TokenIndex,
0 as TokenIndex,
0.1,
0.2,
1,
1,
fixture.bs1[0],
fixture.bs1[0],
fixture.bs1[1],
fixture.bs1[1],
);
const bank2: BankForHealth = mockBankAndOracle(
4 as TokenIndex,
0.3,
0.5,
5,
5,
fixture.bs2[0],
fixture.bs2[0],
fixture.bs2[1],
fixture.bs2[1],
);
const bank3: BankForHealth = mockBankAndOracle(
5 as TokenIndex,
@ -171,6 +217,10 @@ describe('Health Cache', () => {
0.5,
10,
10,
fixture.bs3[0],
fixture.bs3[0],
fixture.bs3[1],
fixture.bs3[1],
);
const ti1 = TokenInfo.fromBank(bank1, I80F48.fromNumber(fixture.token1));
@ -207,7 +257,7 @@ describe('Health Cache', () => {
} as any as OpenOrders,
);
const pM = mockPerpMarket(9, 0.1, 0.2, 10, bank2.price.toNumber());
const pM = mockPerpMarket(9, 0, 0.1, 0.2, 10, bank2.price.toNumber());
const pp = new PerpPosition(
pM.perpMarketIndex,
0,
@ -252,17 +302,23 @@ describe('Health Cache', () => {
token1: 100,
token2: -10,
token3: 0,
bs1: [0, Number.MAX_SAFE_INTEGER],
bs2: [0, Number.MAX_SAFE_INTEGER],
bs3: [0, Number.MAX_SAFE_INTEGER],
oo12: [20, 15],
oo13: [0, 0],
perp1: [3, -131, 7, 11],
expectedHealth:
// for token1, including open orders (scenario: bids execute)
(100.0 + (20.0 + 15.0 * basePrice)) * 0.8 -
// for token1
0.8 *
(100.0 +
// including open orders (scenario: bids execute)
(20.0 + 15.0 * basePrice) +
// including perp (scenario: bids execute)
(3.0 + 7.0) * baseLotsToQuote * 0.8 +
(-131.0 - 7.0 * baseLotsToQuote)) -
// for token2
10.0 * basePrice * 1.5 +
// for perp (scenario: bids execute)
(3.0 + 7.0) * baseLotsToQuote * 0.8 +
(-131.0 - 7.0 * baseLotsToQuote),
10.0 * basePrice * 1.5,
});
testFixture({
@ -270,17 +326,21 @@ describe('Health Cache', () => {
token1: -100,
token2: 10,
token3: 0,
bs1: [0, Number.MAX_SAFE_INTEGER],
bs2: [0, Number.MAX_SAFE_INTEGER],
bs3: [0, Number.MAX_SAFE_INTEGER],
oo12: [20, 15],
oo13: [0, 0],
perp1: [-10, -131, 7, 11],
expectedHealth:
// for token1
-100.0 * 1.2 +
1.2 *
(-100.0 +
// for perp (scenario: asks execute)
(-10.0 - 11.0) * baseLotsToQuote * 1.2 +
(-131.0 + 11.0 * baseLotsToQuote)) +
// for token2, including open orders (scenario: asks execute)
(10.0 * basePrice + (20.0 + 15.0 * basePrice)) * 0.5 +
// for perp (scenario: asks execute)
(-10.0 - 11.0) * baseLotsToQuote * 1.2 +
(-131.0 + 11.0 * baseLotsToQuote),
(10.0 * basePrice + (20.0 + 15.0 * basePrice)) * 0.5,
});
testFixture({
@ -288,10 +348,13 @@ describe('Health Cache', () => {
token1: 0,
token2: 0,
token3: 0,
bs1: [0, Number.MAX_SAFE_INTEGER],
bs2: [0, Number.MAX_SAFE_INTEGER],
bs3: [0, Number.MAX_SAFE_INTEGER],
oo12: [0, 0],
oo13: [0, 0],
perp1: [-1, 100, 0, 0],
expectedHealth: 0.95 * (100.0 - 1.2 * 1.0 * baseLotsToQuote),
expectedHealth: 0.8 * 0.95 * (100.0 - 1.2 * 1.0 * baseLotsToQuote),
});
testFixture({
@ -299,10 +362,13 @@ describe('Health Cache', () => {
token1: 0,
token2: 0,
token3: 0,
bs1: [0, Number.MAX_SAFE_INTEGER],
bs2: [0, Number.MAX_SAFE_INTEGER],
bs3: [0, Number.MAX_SAFE_INTEGER],
oo12: [0, 0],
oo13: [0, 0],
perp1: [1, -100, 0, 0],
expectedHealth: -100.0 + 0.8 * 1.0 * baseLotsToQuote,
expectedHealth: 1.2 * (-100.0 + 0.8 * 1.0 * baseLotsToQuote),
});
testFixture({
@ -310,10 +376,13 @@ describe('Health Cache', () => {
token1: 0,
token2: 0,
token3: 0,
bs1: [0, Number.MAX_SAFE_INTEGER],
bs2: [0, Number.MAX_SAFE_INTEGER],
bs3: [0, Number.MAX_SAFE_INTEGER],
oo12: [0, 0],
oo13: [0, 0],
perp1: [10, 100, 0, 0],
expectedHealth: 0.95 * (100.0 + 0.8 * 10.0 * baseLotsToQuote),
expectedHealth: 0.8 * 0.95 * (100.0 + 0.8 * 10.0 * baseLotsToQuote),
});
testFixture({
@ -321,10 +390,13 @@ describe('Health Cache', () => {
token1: 0,
token2: 0,
token3: 0,
bs1: [0, Number.MAX_SAFE_INTEGER],
bs2: [0, Number.MAX_SAFE_INTEGER],
bs3: [0, Number.MAX_SAFE_INTEGER],
oo12: [0, 0],
oo13: [0, 0],
perp1: [30, -100, 0, 0],
expectedHealth: 0.95 * (-100.0 + 0.8 * 30.0 * baseLotsToQuote),
expectedHealth: 0.8 * 0.95 * (-100.0 + 0.8 * 30.0 * baseLotsToQuote),
});
testFixture({
@ -332,6 +404,9 @@ describe('Health Cache', () => {
token1: -100,
token2: -10,
token3: -10,
bs1: [0, Number.MAX_SAFE_INTEGER],
bs2: [0, Number.MAX_SAFE_INTEGER],
bs3: [0, Number.MAX_SAFE_INTEGER],
oo12: [1, 1],
oo13: [1, 1],
perp1: [0, 0, 0, 0],
@ -351,6 +426,9 @@ describe('Health Cache', () => {
token1: -14,
token2: -10,
token3: -10,
bs1: [0, Number.MAX_SAFE_INTEGER],
bs2: [0, Number.MAX_SAFE_INTEGER],
bs3: [0, Number.MAX_SAFE_INTEGER],
oo12: [1, 1],
oo13: [1, 1],
perp1: [0, 0, 0, 0],
@ -371,6 +449,9 @@ describe('Health Cache', () => {
token1: -100,
token2: -100,
token3: -1,
bs1: [0, Number.MAX_SAFE_INTEGER],
bs2: [0, Number.MAX_SAFE_INTEGER],
bs3: [0, Number.MAX_SAFE_INTEGER],
oo12: [0, 0],
oo13: [10, 1],
perp1: [0, 0, 0, 0],
@ -389,6 +470,9 @@ describe('Health Cache', () => {
token1: -100,
token2: -100,
token3: -1,
bs1: [0, Number.MAX_SAFE_INTEGER],
bs2: [0, Number.MAX_SAFE_INTEGER],
bs3: [0, Number.MAX_SAFE_INTEGER],
oo12: [100, 0],
oo13: [10, 1],
perp1: [0, 0, 0, 0],
@ -404,6 +488,75 @@ describe('Health Cache', () => {
20.0 * 0.8,
});
testFixture({
name: '10, checking collateral limit',
token1: 100,
token2: 100,
token3: 100,
bs1: [100, 1000],
bs2: [1500, 5000],
bs3: [10000, 10000],
oo12: [0, 0],
oo13: [0, 0],
perp1: [0, 0, 0, 0],
expectedHealth:
// token1
0.8 * 100.0 +
// token2
0.5 * 100.0 * 5.0 * (5000.0 / (1500.0 * 5.0)) +
// token3
0.5 * 100.0 * 10.0 * (10000.0 / (10000.0 * 10.0)),
});
testFixture({
name: '11, checking borrow limit',
token1: -100,
token2: -100,
token3: -100,
bs1: [100, 1000],
bs2: [1500, 5000],
bs3: [10000, 10000],
oo12: [0, 0],
oo13: [0, 0],
perp1: [0, 0, 0, 0],
expectedHealth:
// token1
-1.2 * 100.0 -
// token2
1.5 * 100.0 * 5.0 * ((1500.0 * 5.0) / 5000.0) -
// token3
1.5 * 100.0 * 10.0 * ((10000.0 * 10.0) / 10000.0),
});
testFixture({
name: '12, positive perp health offsets token borrow',
token1: -100,
token2: 0,
token3: 0,
bs1: [0, Number.MAX_SAFE_INTEGER],
bs2: [0, Number.MAX_SAFE_INTEGER],
bs3: [0, Number.MAX_SAFE_INTEGER],
oo12: [0, 0],
oo13: [0, 0],
perp1: [1, 100, 0, 0],
expectedHealth:
0.8 * (-100.0 + 0.95 * (100.0 + 0.8 * 1.0 * baseLotsToQuote)),
});
testFixture({
name: '13, negative perp health offsets token deposit',
token1: 100,
token2: 0,
token3: 0,
bs1: [0, Number.MAX_SAFE_INTEGER],
bs2: [0, Number.MAX_SAFE_INTEGER],
bs3: [0, Number.MAX_SAFE_INTEGER],
oo12: [0, 0],
oo13: [0, 0],
perp1: [-1, -100, 0, 0],
expectedHealth: 1.2 * (100.0 - 100.0 - 1.2 * 1.0 * baseLotsToQuote),
});
done();
});
@ -459,8 +612,8 @@ describe('Health Cache', () => {
function valueForAmount(amount: I80F48): I80F48 {
// adjust token balance
const clonedHcClone: HealthCache = cloneDeep(clonedHc);
clonedHc.tokenInfos[source].balanceNative.isub(amount);
clonedHc.tokenInfos[target].balanceNative.iadd(amount.mul(swapPrice));
clonedHc.tokenInfos[source].balanceSpot.isub(amount);
clonedHc.tokenInfos[target].balanceSpot.iadd(amount.mul(swapPrice));
return maxSwapFn(clonedHcClone);
}
@ -509,7 +662,7 @@ describe('Health Cache', () => {
console.log(' - test 0');
// adjust by usdc
const clonedHc: HealthCache = cloneDeep(hc);
clonedHc.tokenInfos[1].balanceNative.iadd(
clonedHc.tokenInfos[1].balanceSpot.iadd(
I80F48.fromNumber(100).div(clonedHc.tokenInfos[1].prices.oracle),
);
@ -559,10 +712,10 @@ describe('Health Cache', () => {
console.log(' - test 1');
const clonedHc: HealthCache = cloneDeep(hc);
// adjust by usdc
clonedHc.tokenInfos[0].balanceNative.iadd(
clonedHc.tokenInfos[0].balanceSpot.iadd(
I80F48.fromNumber(-20).div(clonedHc.tokenInfos[0].prices.oracle),
);
clonedHc.tokenInfos[1].balanceNative.iadd(
clonedHc.tokenInfos[1].balanceSpot.iadd(
I80F48.fromNumber(100).div(clonedHc.tokenInfos[1].prices.oracle),
);
@ -608,10 +761,10 @@ describe('Health Cache', () => {
console.log(' - test 2');
const clonedHc: HealthCache = cloneDeep(hc);
// adjust by usdc
clonedHc.tokenInfos[0].balanceNative.iadd(
clonedHc.tokenInfos[0].balanceSpot.iadd(
I80F48.fromNumber(-50).div(clonedHc.tokenInfos[0].prices.oracle),
);
clonedHc.tokenInfos[1].balanceNative.iadd(
clonedHc.tokenInfos[1].balanceSpot.iadd(
I80F48.fromNumber(100).div(clonedHc.tokenInfos[1].prices.oracle),
);
// possible even though the init ratio is <100
@ -630,13 +783,13 @@ describe('Health Cache', () => {
console.log(' - test 3');
const clonedHc: HealthCache = cloneDeep(hc);
// adjust by usdc
clonedHc.tokenInfos[0].balanceNative.iadd(
clonedHc.tokenInfos[0].balanceSpot.iadd(
I80F48.fromNumber(-30).div(clonedHc.tokenInfos[0].prices.oracle),
);
clonedHc.tokenInfos[1].balanceNative.iadd(
clonedHc.tokenInfos[1].balanceSpot.iadd(
I80F48.fromNumber(100).div(clonedHc.tokenInfos[1].prices.oracle),
);
clonedHc.tokenInfos[2].balanceNative.iadd(
clonedHc.tokenInfos[2].balanceSpot.iadd(
I80F48.fromNumber(-30).div(clonedHc.tokenInfos[2].prices.oracle),
);
@ -659,13 +812,13 @@ describe('Health Cache', () => {
console.log(' - test 4');
const clonedHc: HealthCache = cloneDeep(hc);
// adjust by usdc
clonedHc.tokenInfos[0].balanceNative.iadd(
clonedHc.tokenInfos[0].balanceSpot.iadd(
I80F48.fromNumber(100).div(clonedHc.tokenInfos[0].prices.oracle),
);
clonedHc.tokenInfos[1].balanceNative.iadd(
clonedHc.tokenInfos[1].balanceSpot.iadd(
I80F48.fromNumber(-2).div(clonedHc.tokenInfos[1].prices.oracle),
);
clonedHc.tokenInfos[2].balanceNative.iadd(
clonedHc.tokenInfos[2].balanceSpot.iadd(
I80F48.fromNumber(-65).div(clonedHc.tokenInfos[2].prices.oracle),
);
@ -715,13 +868,13 @@ describe('Health Cache', () => {
];
// adjust by usdc
clonedHc.tokenInfos[0].balanceNative.iadd(
clonedHc.tokenInfos[0].balanceSpot.iadd(
I80F48.fromNumber(-20).div(clonedHc.tokenInfos[0].prices.oracle),
);
clonedHc.tokenInfos[1].balanceNative.iadd(
clonedHc.tokenInfos[1].balanceSpot.iadd(
I80F48.fromNumber(-40).div(clonedHc.tokenInfos[1].prices.oracle),
);
clonedHc.tokenInfos[2].balanceNative.iadd(
clonedHc.tokenInfos[2].balanceSpot.iadd(
I80F48.fromNumber(120).div(clonedHc.tokenInfos[2].prices.oracle),
);
@ -785,10 +938,10 @@ describe('Health Cache', () => {
const clonedHc: HealthCache = cloneDeep(hc);
// adjust by usdc
clonedHc.tokenInfos[0].balanceNative.iadd(
clonedHc.tokenInfos[0].balanceSpot.iadd(
I80F48.fromNumber(-20).div(clonedHc.tokenInfos[0].prices.oracle),
);
clonedHc.tokenInfos[1].balanceNative.iadd(
clonedHc.tokenInfos[1].balanceSpot.iadd(
I80F48.fromNumber(20).div(clonedHc.tokenInfos[1].prices.oracle),
);
expect(clonedHc.health(HealthType.init).toNumber() < 0);
@ -813,10 +966,10 @@ describe('Health Cache', () => {
const clonedHc: HealthCache = cloneDeep(hc);
// adjust by usdc
clonedHc.tokenInfos[0].balanceNative.iadd(
clonedHc.tokenInfos[0].balanceSpot.iadd(
I80F48.fromNumber(-20).div(clonedHc.tokenInfos[0].prices.oracle),
);
clonedHc.tokenInfos[1].balanceNative.iadd(
clonedHc.tokenInfos[1].balanceSpot.iadd(
I80F48.fromNumber(10).div(clonedHc.tokenInfos[1].prices.oracle),
);
expect(clonedHc.health(HealthType.init).toNumber() < 0);
@ -841,7 +994,7 @@ describe('Health Cache', () => {
const clonedHc: HealthCache = cloneDeep(hc);
// adjust by usdc
clonedHc.tokenInfos[0].balanceNative.iadd(
clonedHc.tokenInfos[0].balanceSpot.iadd(
I80F48.fromNumber(10).div(clonedHc.tokenInfos[0].prices.oracle),
);
clonedHc.tokenInfos[1].initAssetWeight = ZERO_I80F48();
@ -876,9 +1029,13 @@ describe('Health Cache', () => {
it('test_max_perp', (done) => {
const baseLotSize = 100;
const b0 = mockBankAndOracle(0 as TokenIndex, 0.0, 0.0, 1, 1);
const p0 = mockPerpMarket(0, 0.3, 0.3, baseLotSize, 2);
const b1 = mockBankAndOracle(1 as TokenIndex, 0.2, 0.2, 1.5, 1.5);
const p0 = mockPerpMarket(0, 1, 0.3, 0.3, baseLotSize, 2);
const hc = new HealthCache(
[TokenInfo.fromBank(b0, I80F48.fromNumber(0))],
[
TokenInfo.fromBank(b0, I80F48.fromNumber(0)),
TokenInfo.fromBank(b1, I80F48.fromNumber(0)),
],
[],
[PerpInfo.emptyFromPerpMarket(p0)],
);
@ -902,7 +1059,7 @@ describe('Health Cache', () => {
ratio: number,
priceFactor: number,
): number[] {
const prices = hc.perpInfos[0].prices;
const prices = hc.perpInfos[0].basePrices;
const tradePrice = I80F48.fromNumber(priceFactor).mul(prices.oracle);
const baseLots0 = hc
.getMaxPerpForHealthRatio(
@ -951,25 +1108,33 @@ describe('Health Cache', () => {
console.log(
`checking for price_factor: ${priceFactor}, target ratio ${ratio}: actual ratio: ${actualRatio}, plus ratio: ${plusRatio}, base_lots: ${baseLots}`,
);
expect(ratio).lessThan(actualRatio);
expect(ratio).lessThanOrEqual(actualRatio);
expect(plusRatio - 0.1).lessThanOrEqual(ratio);
}
// adjust token
hc.tokenInfos[0].balanceNative.iadd(I80F48.fromNumber(3000));
for (const existing of [-5, 0, 3]) {
const hcClone: HealthCache = cloneDeep(hc);
hcClone.perpInfos[0].baseLots.iadd(new BN(existing));
hcClone.perpInfos[0].quote.isub(
I80F48.fromNumber(existing * baseLotSize * 2),
hc.tokenInfos[0].balanceSpot.iadd(I80F48.fromNumber(3000));
for (const existingSettle of [-500, 0, 300]) {
const hcClone1: HealthCache = cloneDeep(hc);
hcClone1.tokenInfos[1].balanceSpot.iadd(
I80F48.fromNumber(existingSettle),
);
for (const side of [PerpOrderSide.bid, PerpOrderSide.ask]) {
console.log(
`existing ${existing} ${side === PerpOrderSide.bid ? 'bid' : 'ask'}`,
for (const existing of [-5, 0, 3]) {
const hcClone2: HealthCache = cloneDeep(hcClone1);
hcClone2.perpInfos[0].baseLots.iadd(new BN(existing));
hcClone2.perpInfos[0].quote.isub(
I80F48.fromNumber(existing * baseLotSize * 2),
);
for (const priceFactor of [0.8, 1.0, 1.1]) {
for (const ratio of range(1, 101, 1)) {
checkMaxTrade(hcClone, side, ratio, priceFactor);
for (const side of [PerpOrderSide.bid, PerpOrderSide.ask]) {
console.log(
`lots ${existing}, settle ${existingSettle}, side ${
side === PerpOrderSide.bid ? 'bid' : 'ask'
}`,
);
for (const priceFactor of [0.8, 1.0, 1.1]) {
for (const ratio of range(1, 101, 1)) {
checkMaxTrade(hcClone2, side, ratio, priceFactor);
}
}
}
}
@ -989,4 +1154,6 @@ describe('Health Cache', () => {
done();
});
// TODO: test_assets_and_borrows
});

View File

@ -3,7 +3,6 @@ import { OpenOrders } from '@project-serum/serum';
import { PublicKey } from '@solana/web3.js';
import cloneDeep from 'lodash/cloneDeep';
import {
HUNDRED_I80F48,
I80F48,
I80F48Dto,
MAX_I80F48,
@ -42,6 +41,50 @@ import { MarketIndex, Serum3Market, Serum3Side } from './serum3';
// ██████████████████████████████████████████
// warning: this code is copy pasta from rust, keep in sync with health.rs
function spotAmountTakenForHealthZero(
health: I80F48,
startingSpot: I80F48,
assetWeightedPrice: I80F48,
liabWeightedPrice: I80F48,
): I80F48 {
if (health.lte(ZERO_I80F48())) {
return ZERO_I80F48();
}
let takenSpot = ZERO_I80F48();
if (startingSpot.gt(ZERO_I80F48())) {
if (assetWeightedPrice.gt(ZERO_I80F48())) {
const assetMax = health.div(assetWeightedPrice);
if (assetMax.lte(startingSpot)) {
return assetMax;
}
}
takenSpot = startingSpot;
health.isub(startingSpot.mul(assetWeightedPrice));
}
if (health.gt(ZERO_I80F48())) {
if (liabWeightedPrice.lte(ZERO_I80F48())) {
throw new Error('LiabWeightedPrice must be greater than 0!');
}
takenSpot.iadd(health.div(liabWeightedPrice));
}
return takenSpot;
}
function spotAmountGivenForHealthZero(
health: I80F48,
startingSpot: I80F48,
assetWeightedPrice: I80F48,
liabWeightedPrice: I80F48,
): I80F48 {
return spotAmountTakenForHealthZero(
health.neg(),
startingSpot.neg(),
liabWeightedPrice,
assetWeightedPrice,
);
}
export class HealthCache {
constructor(
public tokenInfos: TokenInfo[],
@ -113,14 +156,14 @@ export class HealthCache {
);
}
computeSerum3Reservations(healthType: HealthType): {
tokenMaxReserved: I80F48[];
computeSerum3Reservations(healthType: HealthType | undefined): {
tokenMaxReserved: TokenMaxReserved[];
serum3Reserved: Serum3Reserved[];
} {
// For each token, compute the sum of serum-reserved amounts over all markets.
const tokenMaxReserved = new Array(this.tokenInfos.length)
.fill(null)
.map((ignored) => ZERO_I80F48());
.map((ignored) => new TokenMaxReserved(ZERO_I80F48()));
// For each serum market, compute what happened if reserved_base was converted to quote
// or reserved_quote was converted to base.
@ -145,9 +188,9 @@ export class HealthCache {
);
const baseMaxReserved = tokenMaxReserved[info.baseIndex];
baseMaxReserved.iadd(allReservedAsBase);
baseMaxReserved.maxSerumReserved.iadd(allReservedAsBase);
const quoteMaxReserved = tokenMaxReserved[info.quoteIndex];
quoteMaxReserved.iadd(allReservedAsQuote);
quoteMaxReserved.maxSerumReserved.iadd(allReservedAsQuote);
serum3Reserved.push(
new Serum3Reserved(allReservedAsBase, allReservedAsQuote),
@ -160,10 +203,47 @@ export class HealthCache {
};
}
public health(healthType: HealthType): I80F48 {
effectiveTokenBalances(healthType: HealthType | undefined): TokenBalance[] {
return this.effectiveTokenBalancesInternal(healthType, false);
}
effectiveTokenBalancesInternal(
healthType: HealthType | undefined,
ignoreNegativePerp: boolean,
): TokenBalance[] {
const tokenBalances = new Array(this.tokenInfos.length)
.fill(null)
.map((ignored) => new TokenBalance(ZERO_I80F48()));
for (const perpInfo of this.perpInfos) {
const settleTokenIndex = this.findTokenInfoIndex(
perpInfo.settleTokenIndex,
);
const perpSettleToken = tokenBalances[settleTokenIndex];
const healthUnsettled = perpInfo.healthUnsettledPnl(healthType);
if (!ignoreNegativePerp || healthUnsettled.gt(ZERO_I80F48())) {
perpSettleToken.spotAndPerp.iadd(healthUnsettled);
}
}
for (const index of this.tokenInfos.keys()) {
const tokenInfo = this.tokenInfos[index];
const tokenBalance = tokenBalances[index];
tokenBalance.spotAndPerp.iadd(tokenInfo.balanceSpot);
}
return tokenBalances;
}
healthSum(healthType: HealthType, tokenBalances: TokenBalance[]): I80F48 {
const health = ZERO_I80F48();
for (const tokenInfo of this.tokenInfos) {
const contrib = tokenInfo.healthContribution(healthType);
for (const index of this.tokenInfos.keys()) {
const tokenInfo = this.tokenInfos[index];
const tokenBalance = tokenBalances[index];
const contrib = tokenInfo.healthContribution(
healthType,
tokenBalance.spotAndPerp,
);
// console.log(` - ti ${contrib}`);
health.iadd(contrib);
}
@ -172,156 +252,136 @@ export class HealthCache {
const contrib = serum3Info.healthContribution(
healthType,
this.tokenInfos,
tokenBalances,
res.tokenMaxReserved,
res.serum3Reserved[index],
);
// console.log(` - si ${contrib}`);
health.iadd(contrib);
}
for (const perpInfo of this.perpInfos) {
const contrib = perpInfo.healthContribution(healthType);
// console.log(` - pi ${contrib}`);
health.iadd(contrib);
}
return health;
}
// Note: only considers positive perp pnl contributions, see program code for more reasoning
public perpSettleHealth(): I80F48 {
const health = ZERO_I80F48();
for (const tokenInfo of this.tokenInfos) {
const contrib = tokenInfo.healthContribution(HealthType.maint);
// console.log(` - ti ${contrib}`);
health.iadd(contrib);
}
const res = this.computeSerum3Reservations(HealthType.maint);
for (const [index, serum3Info] of this.serum3Infos.entries()) {
const contrib = serum3Info.healthContribution(
HealthType.maint,
this.tokenInfos,
res.tokenMaxReserved,
res.serum3Reserved[index],
);
// console.log(` - si ${contrib}`);
health.iadd(contrib);
}
for (const perpInfo of this.perpInfos) {
const positiveContrib = perpInfo
.healthContribution(HealthType.maint)
.max(ZERO_I80F48());
// console.log(` - pi ${positiveContrib}`);
health.iadd(positiveContrib);
}
return health;
public health(healthType: HealthType): I80F48 {
const tokenBalances = this.effectiveTokenBalancesInternal(
healthType,
false,
);
return this.healthSum(healthType, tokenBalances);
}
// An undefined HealthType will use an asset and liab weight of 1
public assets(healthType?: HealthType): I80F48 {
const assets = ZERO_I80F48();
public perpMaxSettle(settleTokenIndex: TokenIndex): I80F48 {
const healthType = HealthType.maint;
const tokenBalances = this.effectiveTokenBalancesInternal(healthType, true);
const perpSettleHealth = this.healthSum(healthType, tokenBalances);
const tokenInfoIndex = this.findTokenInfoIndex(settleTokenIndex);
const tokenInfo = this.tokenInfos[tokenInfoIndex];
return spotAmountTakenForHealthZero(
perpSettleHealth,
tokenBalances[tokenInfoIndex].spotAndPerp,
tokenInfo.assetWeightedPrice(healthType),
tokenInfo.liabWeightedPrice(healthType),
);
}
healthAssetsAndLiabsStableAssets(healthType: HealthType): {
assets: I80F48;
liabs: I80F48;
} {
return this.healthAssetsAndLiabs(healthType, true);
}
healthAssetsAndLiabsStableLiabs(healthType: HealthType): {
assets: I80F48;
liabs: I80F48;
} {
return this.healthAssetsAndLiabs(healthType, false);
}
public healthAssetsAndLiabs(
healthType: HealthType | undefined,
stableAssets: boolean,
): { assets: I80F48; liabs: I80F48 } {
const totalAssets = ZERO_I80F48();
const totalLiabs = ZERO_I80F48();
for (const tokenInfo of this.tokenInfos) {
const contrib = tokenInfo.healthContribution(healthType);
if (contrib.isPos()) {
assets.iadd(contrib);
const assetBalance = ZERO_I80F48();
const liabBalance = ZERO_I80F48();
if (tokenInfo.balanceSpot.isPos()) {
assetBalance.iadd(tokenInfo.balanceSpot);
} else {
liabBalance.isub(tokenInfo.balanceSpot);
}
for (const perpInfo of this.perpInfos) {
if (perpInfo.settleTokenIndex != tokenInfo.tokenIndex) {
continue;
}
const healthUnsettled = perpInfo.healthUnsettledPnl(healthType);
if (healthUnsettled.isPos()) {
assetBalance.iadd(healthUnsettled);
} else {
liabBalance.isub(healthUnsettled);
}
}
if (stableAssets) {
const assetWeightedPrice = tokenInfo.assetWeightedPrice(healthType);
const assets = assetBalance.mul(assetWeightedPrice);
totalAssets.iadd(assets);
if (assetBalance.gte(liabBalance)) {
totalLiabs.iadd(liabBalance.mul(assetWeightedPrice));
} else {
const liabWeightedPrice = tokenInfo.liabWeightedPrice(healthType);
totalLiabs.iadd(
assets.add(liabBalance.sub(assetBalance).mul(liabWeightedPrice)),
);
}
} else {
const liabWeightedPrice = tokenInfo.liabWeightedPrice(healthType);
const liabs = liabBalance.mul(liabWeightedPrice);
totalLiabs.iadd(liabs);
if (assetBalance.gte(liabBalance)) {
const assetWeightedPrice = tokenInfo.assetWeightedPrice(healthType);
totalAssets.iadd(
liabs.add(assetBalance.sub(liabBalance).mul(assetWeightedPrice)),
);
} else {
totalAssets.iadd(assetBalance.mul(liabWeightedPrice));
}
}
}
const res = this.computeSerum3Reservations(HealthType.maint);
const tokenBalances = this.effectiveTokenBalances(healthType);
const res = this.computeSerum3Reservations(healthType);
for (const [index, serum3Info] of this.serum3Infos.entries()) {
const contrib = serum3Info.healthContribution(
healthType,
this.tokenInfos,
tokenBalances,
res.tokenMaxReserved,
res.serum3Reserved[index],
);
if (contrib.isPos()) {
assets.iadd(contrib);
totalAssets.iadd(contrib);
} else {
totalLiabs.iadd(contrib);
}
}
for (const perpInfo of this.perpInfos) {
const contrib = perpInfo.healthContribution(healthType);
if (contrib.isPos()) {
assets.iadd(contrib);
}
}
return assets;
}
// An undefined HealthType will use an asset and liab weight of 1
public liabs(healthType?: HealthType): I80F48 {
const liabs = ZERO_I80F48();
for (const tokenInfo of this.tokenInfos) {
const contrib = tokenInfo.healthContribution(healthType);
if (contrib.isNeg()) {
liabs.isub(contrib);
}
}
const res = this.computeSerum3Reservations(HealthType.maint);
for (const [index, serum3Info] of this.serum3Infos.entries()) {
const contrib = serum3Info.healthContribution(
healthType,
this.tokenInfos,
res.tokenMaxReserved,
res.serum3Reserved[index],
);
if (contrib.isNeg()) {
liabs.isub(contrib);
}
}
for (const perpInfo of this.perpInfos) {
const contrib = perpInfo.healthContribution(healthType);
if (contrib.isNeg()) {
liabs.isub(contrib);
}
}
return liabs;
return { assets: totalAssets, liabs: totalLiabs };
}
public healthRatio(healthType: HealthType): I80F48 {
const assets = ZERO_I80F48();
const liabs = ZERO_I80F48();
for (const tokenInfo of this.tokenInfos) {
const contrib = tokenInfo.healthContribution(healthType);
// console.log(` - ti contrib ${contrib.toLocaleString()}`);
if (contrib.isPos()) {
assets.iadd(contrib);
} else {
liabs.isub(contrib);
}
}
const res = this.computeSerum3Reservations(HealthType.maint);
for (const [index, serum3Info] of this.serum3Infos.entries()) {
const contrib = serum3Info.healthContribution(
healthType,
this.tokenInfos,
res.tokenMaxReserved,
res.serum3Reserved[index],
);
// console.log(` - si contrib ${contrib.toLocaleString()}`);
if (contrib.isPos()) {
assets.iadd(contrib);
} else {
liabs.isub(contrib);
}
}
for (const perpInfo of this.perpInfos) {
const contrib = perpInfo.healthContribution(healthType);
// console.log(` - pi contrib ${contrib.toLocaleString()}`);
if (contrib.isPos()) {
assets.iadd(contrib);
} else {
liabs.isub(contrib);
}
}
// console.log(
// ` - assets ${assets.toLocaleString()}, liabs ${liabs.toLocaleString()}`,
// );
if (liabs.gt(I80F48.fromNumber(0.001))) {
return HUNDRED_I80F48().mul(assets.sub(liabs).div(liabs));
} else {
return MAX_I80F48();
const res = this.healthAssetsAndLiabsStableLiabs(healthType);
const hundred = I80F48.fromNumber(100);
// console.log(`assets ${res.assets}`);
// console.log(`liabs ${res.liabs}`);
if (res.liabs.gt(I80F48.fromNumber(0.001))) {
return hundred.mul(res.assets.sub(res.liabs)).div(res.liabs);
}
return MAX_I80F48();
}
findTokenInfoIndex(tokenIndex: TokenIndex): number {
@ -352,7 +412,7 @@ export class HealthCache {
const bank: Bank = group.getFirstBankByMint(change.mintPk);
const changeIndex = adjustedCache.getOrCreateTokenInfoIndex(bank);
// TODO: this will no longer work as easily because of the health weight changes
adjustedCache.tokenInfos[changeIndex].balanceNative.iadd(
adjustedCache.tokenInfos[changeIndex].balanceSpot.iadd(
change.nativeTokenAmount,
);
}
@ -402,8 +462,8 @@ export class HealthCache {
const quoteEntry = this.tokenInfos[quoteEntryIndex];
// Apply it to the tokens
baseEntry.balanceNative.iadd(freeBaseChange);
quoteEntry.balanceNative.iadd(freeQuoteChange);
baseEntry.balanceSpot.iadd(freeBaseChange);
quoteEntry.balanceSpot.iadd(freeQuoteChange);
// Apply it to the serum3 info
const index = this.getOrCreateSerum3InfoIndex(
@ -430,9 +490,7 @@ export class HealthCache {
// essentially simulating a place order
// Reduce token balance for quote
adjustedCache.tokenInfos[quoteIndex].balanceNative.isub(
bidNativeQuoteAmount,
);
adjustedCache.tokenInfos[quoteIndex].balanceSpot.isub(bidNativeQuoteAmount);
// Increase reserved in Serum3Info for quote
adjustedCache.adjustSerum3Reserved(
@ -461,7 +519,7 @@ export class HealthCache {
// essentially simulating a place order
// Reduce token balance for base
adjustedCache.tokenInfos[baseIndex].balanceNative.isub(askNativeBaseAmount);
adjustedCache.tokenInfos[baseIndex].balanceSpot.isub(askNativeBaseAmount);
// Increase reserved in Serum3Info for base
adjustedCache.adjustSerum3Reserved(
@ -528,32 +586,6 @@ export class HealthCache {
return clonedHealthCache.healthRatio(healthType);
}
public logHealthCache(debug: string): void {
if (debug) console.log(debug);
for (const token of this.tokenInfos) {
console.log(` ${token.toString()}`);
}
const res = this.computeSerum3Reservations(HealthType.maint);
for (const [index, serum3Info] of this.serum3Infos.entries()) {
console.log(
` ${serum3Info.toString(
this.tokenInfos,
res.tokenMaxReserved,
res.serum3Reserved[index],
)}`,
);
}
console.log(
` assets ${this.assets(HealthType.init)}, liabs ${this.liabs(
HealthType.init,
)}, `,
);
console.log(` health(HealthType.init) ${this.health(HealthType.init)}`);
console.log(
` healthRatio(HealthType.init) ${this.healthRatio(HealthType.init)}`,
);
}
private static scanRightUntilLessThan(
start: I80F48,
target: I80F48,
@ -789,8 +821,14 @@ export class HealthCache {
const target = healthCacheClone.tokenInfos[targetIndex];
const res = healthCacheClone.computeSerum3Reservations(HealthType.init);
const sourceReserved = res.tokenMaxReserved[sourceIndex];
const targetReserved = res.tokenMaxReserved[targetIndex];
const sourceReserved = res.tokenMaxReserved[sourceIndex].maxSerumReserved;
const targetReserved = res.tokenMaxReserved[targetIndex].maxSerumReserved;
const tokenBalances = healthCacheClone.effectiveTokenBalances(
HealthType.init,
);
const sourceBalance = tokenBalances[sourceIndex].spotAndPerp;
const targetBalance = tokenBalances[targetIndex].spotAndPerp;
// If the price is sufficiently good, then health will just increase from swapping:
// once we've swapped enough, swapping x reduces health by x * source_liab_weight and
@ -819,10 +857,8 @@ export class HealthCache {
// adjustedCache.logHealthCache('beforeSwap', adjustedCache);
// TODO: make a copy of the bank, apply amount, recompute weights,
// and set the new weights on the tokenInfos
adjustedCache.tokenInfos[sourceIndex].balanceNative.isub(amount);
adjustedCache.tokenInfos[targetIndex].balanceNative.iadd(
amount.mul(price),
);
adjustedCache.tokenInfos[sourceIndex].balanceSpot.isub(amount);
adjustedCache.tokenInfos[targetIndex].balanceSpot.iadd(amount.mul(price));
// adjustedCache.logHealthCache('afterSwap', adjustedCache);
return adjustedCache;
}
@ -840,10 +876,10 @@ export class HealthCache {
// The first thing we do is to find this maximum.
// The largest amount that the maximum could be at
const rightmost = source.balanceNative
const rightmost = sourceBalance
.abs()
.add(sourceReserved)
.max(target.balanceNative.abs().add(targetReserved).div(price));
.max(targetBalance.abs().add(targetReserved).div(price));
const [amountForMaxValue, maxValue] = HealthCache.findMaximum(
ZERO_I80F48(),
rightmost,
@ -940,10 +976,10 @@ export class HealthCache {
// and when its a bid, then quote->bid
let zeroAmount;
if (side == Serum3Side.ask) {
const quoteBorrows = quote.balanceNative.lt(ZERO_I80F48())
? quote.balanceNative.abs().mul(quote.prices.liab(HealthType.init))
const quoteBorrows = quote.balanceSpot.lt(ZERO_I80F48())
? quote.balanceSpot.abs().mul(quote.prices.liab(HealthType.init))
: ZERO_I80F48();
const max = base.balanceNative
const max = base.balanceSpot
.mul(base.prices.asset(HealthType.init))
.max(quoteBorrows);
zeroAmount = max.add(
@ -958,10 +994,10 @@ export class HealthCache {
// console.log(` - quoteBorrows ${quoteBorrows.toLocaleString()}`);
// console.log(` - max ${max.toLocaleString()}`);
} else {
const baseBorrows = base.balanceNative.lt(ZERO_I80F48())
? base.balanceNative.abs().mul(base.prices.liab(HealthType.init))
const baseBorrows = base.balanceSpot.lt(ZERO_I80F48())
? base.balanceSpot.abs().mul(base.prices.liab(HealthType.init))
: ZERO_I80F48();
const max = quote.balanceNative
const max = quote.balanceSpot
.mul(quote.prices.asset(HealthType.init))
.max(baseBorrows);
zeroAmount = max.add(
@ -992,10 +1028,10 @@ export class HealthCache {
// TODO: there should also be some issue with oracle vs stable price here;
// probably better to pass in not the quote amount but the base or quote native amount
side === Serum3Side.ask
? adjustedCache.tokenInfos[baseIndex].balanceNative.isub(
? adjustedCache.tokenInfos[baseIndex].balanceSpot.isub(
amount.div(base.prices.oracle),
)
: adjustedCache.tokenInfos[quoteIndex].balanceNative.isub(
: adjustedCache.tokenInfos[quoteIndex].balanceSpot.isub(
amount.div(quote.prices.oracle),
);
adjustedCache.adjustSerum3Reserved(
@ -1049,21 +1085,25 @@ export class HealthCache {
const perpInfoIndex = healthCacheClone.getOrCreatePerpInfoIndex(perpMarket);
const perpInfo = healthCacheClone.perpInfos[perpInfoIndex];
const prices = perpInfo.prices;
const prices = perpInfo.basePrices;
const baseLotSize = I80F48.fromI64(perpMarket.baseLotSize);
// If the price is sufficiently good then health will just increase from trading
const settleInfoIndex = this.findTokenInfoIndex(perpInfo.settleTokenIndex);
const settleInfo = this.tokenInfos[settleInfoIndex];
const finalHealthSlope =
direction == 1
? perpInfo.initBaseAssetWeight
.mul(prices.asset(HealthType.init))
.sub(price)
: price.sub(
perpInfo.initBaseLiabWeight.mul(prices.liab(HealthType.init)),
);
: perpInfo.initBaseLiabWeight
.neg()
.mul(prices.liab(HealthType.init))
.add(price);
if (finalHealthSlope.gte(ZERO_I80F48())) {
return MAX_I80F48();
}
finalHealthSlope.imul(settleInfo.liabWeightedPrice(HealthType.init));
function cacheAfterTrade(baseLots: BN): HealthCache {
const adjustedCache: HealthCache = cloneDeep(healthCacheClone);
@ -1120,23 +1160,30 @@ export class HealthCache {
// We do this by looking at the starting health and the health slope per
// traded base lot (finalHealthSlope).
const startCache = cacheAfterTrade(new BN(case1Start.toNumber()));
startCache.perpInfos[perpInfoIndex].initOverallAssetWeight = ONE_I80F48();
const settleInfo = startCache.tokenInfos[settleInfoIndex];
settleInfo.initAssetWeight = settleInfo.initLiabWeight;
settleInfo.initScaledAssetWeight = settleInfo.initScaledLiabWeight;
const startHealth = startCache.health(HealthType.init);
if (startHealth.lte(ZERO_I80F48())) {
return ZERO_I80F48();
}
// The perp market's contribution to the health above may be capped. But we need to trade
// enough to fully reduce any positive-pnl buffer. Thus get the uncapped health:
const perpInfo = startCache.perpInfos[perpInfoIndex];
const startHealthUncapped = startHealth
.sub(perpInfo.healthContribution(HealthType.init))
.add(perpInfo.unweightedHealthContribution(HealthType.init));
const zeroHealthAmount = case1Start
.sub(startHealthUncapped.div(finalHealthSlope).div(baseLotSize))
.sub(
startHealth.div(
finalHealthSlope.mul(baseLotSize).mul(I80F48.fromNumber(0.99)),
),
)
.add(ONE_I80F48());
const zeroHealthRatio = healthRatioAfterTradeTrunc(zeroHealthAmount);
// console.log(`case1Start ${case1Start}`);
// console.log(`case1StartRatio ${case1StartRatio}`);
// console.log(`zeroHealthAmount ${zeroHealthAmount}`);
// console.log(`zeroHealthRatio ${zeroHealthRatio}`);
// console.log(`minRatio ${minRatio}`);
baseLots = HealthCache.binaryApproximationSearch(
case1Start,
case1StartRatio,
@ -1197,7 +1244,7 @@ export class TokenInfo {
public initLiabWeight: I80F48,
public initScaledLiabWeight: I80F48,
public prices: Prices,
public balanceNative: I80F48,
public balanceSpot: I80F48,
) {}
static fromDto(dto: TokenInfoDto): TokenInfo {
@ -1213,7 +1260,7 @@ export class TokenInfo {
I80F48.from(dto.prices.oracle),
I80F48.from(dto.prices.stable),
),
I80F48.from(dto.balanceNative),
I80F48.from(dto.balanceSpot),
);
}
@ -1238,48 +1285,66 @@ export class TokenInfo {
);
}
assetWeight(healthType: HealthType): I80F48 {
assetWeight(healthType: HealthType | undefined): I80F48 {
if (healthType == HealthType.init) {
return this.initScaledAssetWeight;
} else if (healthType == HealthType.liquidationEnd) {
return this.initAssetWeight;
}
// healthType == HealthType.maint
return this.maintAssetWeight;
if (healthType == HealthType.maint) {
return this.maintAssetWeight;
}
return I80F48.fromNumber(1);
}
liabWeight(healthType: HealthType): I80F48 {
assetWeightedPrice(healthType: HealthType | undefined): I80F48 {
return this.assetWeight(healthType).mul(this.prices.asset(healthType));
}
liabWeight(healthType: HealthType | undefined): I80F48 {
if (healthType == HealthType.init) {
return this.initScaledLiabWeight;
} else if (healthType == HealthType.liquidationEnd) {
return this.initLiabWeight;
}
// healthType == HealthType.maint
return this.maintLiabWeight;
if (healthType == HealthType.maint) {
return this.maintLiabWeight;
}
return I80F48.fromNumber(1);
}
healthContribution(healthType?: HealthType): I80F48 {
let weight, price;
liabWeightedPrice(healthType: HealthType | undefined): I80F48 {
return this.liabWeight(healthType).mul(this.prices.liab(healthType));
}
healthContribution(
healthType: HealthType | undefined,
balance: I80F48,
): I80F48 {
if (healthType === undefined) {
return this.balanceNative.mul(this.prices.oracle);
return balance.mul(this.prices.oracle);
}
if (this.balanceNative.isNeg()) {
weight = this.liabWeight(healthType);
price = this.prices.liab(healthType);
} else {
weight = this.assetWeight(healthType);
price = this.prices.asset(healthType);
}
return this.balanceNative.mul(weight).mul(price);
// console.log(`balance ${balance}`);
return balance.isNeg()
? balance.mul(this.liabWeightedPrice(healthType))
: balance.mul(this.assetWeightedPrice(healthType));
}
toString(): string {
toString(balance: I80F48): string {
return ` tokenIndex: ${this.tokenIndex}, balanceNative: ${
this.balanceNative
}, initHealth ${this.healthContribution(HealthType.init)}`;
this.balanceSpot
}, initHealth ${this.healthContribution(HealthType.init, balance)}`;
}
}
class TokenBalance {
constructor(public spotAndPerp: I80F48) {}
}
class TokenMaxReserved {
constructor(public maxSerumReserved: I80F48) {}
}
export class Serum3Reserved {
constructor(
public allReservedAsBase: I80F48,
@ -1332,11 +1397,9 @@ export class Serum3Info {
const baseFree = I80F48.fromI64(oo.baseTokenFree);
// NOTE: referrerRebatesAccrued is not declared on oo class, but the layout
// is aware of it
const quoteFree = I80F48.fromI64(
oo.quoteTokenFree.add((oo as any).referrerRebatesAccrued),
);
baseInfo.balanceNative.iadd(baseFree);
quoteInfo.balanceNative.iadd(quoteFree);
const quoteFree = I80F48.fromI64(oo.quoteTokenFree);
baseInfo.balanceSpot.iadd(baseFree);
quoteInfo.balanceSpot.iadd(quoteFree);
// track the reserved amounts
const reservedBase = I80F48.fromI64(
@ -1359,7 +1422,8 @@ export class Serum3Info {
healthContribution(
healthType: HealthType | undefined,
tokenInfos: TokenInfo[],
tokenMaxReserved: I80F48[],
tokenBalances: TokenBalance[],
tokenMaxReserved: TokenMaxReserved[],
marketReserved: Serum3Reserved,
): I80F48 {
if (
@ -1378,12 +1442,13 @@ export class Serum3Info {
// token info?
const computeHealthEffect = function (
tokenInfo: TokenInfo,
tokenMaxReserved: I80F48,
balance: TokenBalance,
maxReserved: TokenMaxReserved,
marketReserved: I80F48,
): I80F48 {
// This balance includes all possible reserved funds from markets that relate to the
// token, including this market itself: `tokenMaxReserved` is already included in `maxBalance`.
const maxBalance = tokenInfo.balanceNative.add(tokenMaxReserved);
const maxBalance = balance.spotAndPerp.add(maxReserved.maxSerumReserved);
// Assuming `reserved` was added to `max_balance` last (because that gives the smallest
// health effects): how much did health change because of it?
@ -1418,12 +1483,14 @@ export class Serum3Info {
const healthBase = computeHealthEffect(
baseInfo,
baseMaxReserved,
tokenBalances[this.baseIndex],
tokenMaxReserved[this.baseIndex],
marketReserved.allReservedAsBase,
);
const healthQuote = computeHealthEffect(
quoteInfo,
quoteMaxReserved,
tokenBalances[this.quoteIndex],
tokenMaxReserved[this.quoteIndex],
marketReserved.allReservedAsQuote,
);
@ -1435,7 +1502,8 @@ export class Serum3Info {
toString(
tokenInfos: TokenInfo[],
tokenMaxReserved: I80F48[],
tokenBalances: TokenBalance[],
tokenMaxReserved: TokenMaxReserved[],
marketReserved: Serum3Reserved,
): string {
return ` marketIndex: ${this.marketIndex}, baseIndex: ${
@ -1447,6 +1515,7 @@ export class Serum3Info {
}, initHealth ${this.healthContribution(
HealthType.init,
tokenInfos,
tokenBalances,
tokenMaxReserved,
marketReserved,
)}`;
@ -1456,6 +1525,7 @@ export class Serum3Info {
export class PerpInfo {
constructor(
public perpMarketIndex: number,
public settleTokenIndex: TokenIndex,
public maintBaseAssetWeight: I80F48,
public initBaseAssetWeight: I80F48,
public maintBaseLiabWeight: I80F48,
@ -1467,13 +1537,14 @@ export class PerpInfo {
public bidsBaseLots: BN,
public asksBaseLots: BN,
public quote: I80F48,
public prices: Prices,
public basePrices: Prices,
public hasOpenOrders: boolean,
) {}
static fromDto(dto: PerpInfoDto): PerpInfo {
return new PerpInfo(
dto.perpMarketIndex,
dto.settleTokenIndex as TokenIndex,
I80F48.from(dto.maintBaseAssetWeight),
I80F48.from(dto.initBaseAssetWeight),
I80F48.from(dto.maintBaseLiabWeight),
@ -1511,6 +1582,7 @@ export class PerpInfo {
return new PerpInfo(
perpMarket.perpMarketIndex,
perpMarket.settleTokenIndex,
perpMarket.maintBaseAssetWeight,
perpMarket.initBaseAssetWeight,
perpMarket.maintBaseLiabWeight,
@ -1530,19 +1602,65 @@ export class PerpInfo {
);
}
healthContribution(healthType: HealthType | undefined): I80F48 {
const contrib = this.unweightedHealthContribution(healthType);
if (contrib.gt(ZERO_I80F48())) {
const assetWeight =
healthType == HealthType.init || healthType == HealthType.liquidationEnd
? this.initOverallAssetWeight
: this.maintOverallAssetWeight;
return assetWeight.mul(contrib);
}
return contrib;
healthContribution(healthType: HealthType, settleToken: TokenInfo): I80F48 {
const contrib = this.unweightedHealthUnsettledPnl(healthType);
return this.weighHealthContributionSettle(
this.weighHealthContributionOverall(contrib, healthType),
healthType,
settleToken,
);
}
unweightedHealthContribution(healthType: HealthType | undefined): I80F48 {
healthUnsettledPnl(healthType: HealthType | undefined): I80F48 {
const contrib = this.unweightedHealthUnsettledPnl(healthType);
return this.weighHealthContributionOverall(contrib, healthType);
}
weighHealthContributionSettle(
unweighted: I80F48,
healthType: HealthType,
settleToken: TokenInfo,
): I80F48 {
if (this.settleTokenIndex !== settleToken.tokenIndex) {
throw new Error('Settle token index should match!');
}
if (unweighted.gt(ZERO_I80F48())) {
return (
healthType == HealthType.init
? settleToken.initScaledAssetWeight
: healthType == HealthType.liquidationEnd
? settleToken.initAssetWeight
: settleToken.maintLiabWeight
)
.mul(unweighted)
.mul(settleToken.prices.asset(healthType));
}
return (
healthType == HealthType.init
? settleToken.initScaledLiabWeight
: healthType == HealthType.liquidationEnd
? settleToken.initLiabWeight
: settleToken.maintLiabWeight
)
.mul(unweighted)
.mul(settleToken.prices.liab(healthType));
}
weighHealthContributionOverall(
unweighted: I80F48,
healthType: HealthType | undefined,
): I80F48 {
if (unweighted.gt(ZERO_I80F48())) {
return (
healthType == HealthType.init || healthType == HealthType.liquidationEnd
? this.initOverallAssetWeight
: this.maintOverallAssetWeight
).mul(unweighted);
}
return unweighted;
}
unweightedHealthUnsettledPnl(healthType: HealthType | undefined): I80F48 {
function orderExecutionCase(
pi: PerpInfo,
ordersBaseLots: BN,
@ -1573,9 +1691,9 @@ export class PerpInfo {
}
if (netBaseNative.isNeg()) {
basePrice = pi.prices.liab(healthType);
basePrice = pi.basePrices.liab(healthType);
} else {
basePrice = pi.prices.asset(healthType);
basePrice = pi.basePrices.asset(healthType);
}
// Total value of the order-execution adjusted base position
@ -1594,12 +1712,12 @@ export class PerpInfo {
const bidsCase = orderExecutionCase(
this,
this.bidsBaseLots,
this.prices.liab(healthType),
this.basePrices.liab(healthType),
);
const asksCase = orderExecutionCase(
this,
this.asksBaseLots.neg(),
this.prices.asset(healthType),
this.basePrices.asset(healthType),
);
const worstCase = bidsCase.min(asksCase);
@ -1609,6 +1727,7 @@ export class PerpInfo {
static emptyFromPerpMarket(perpMarket: PerpMarket): PerpInfo {
return new PerpInfo(
perpMarket.perpMarketIndex,
perpMarket.settleTokenIndex,
perpMarket.maintBaseAssetWeight,
perpMarket.initBaseAssetWeight,
perpMarket.maintBaseLiabWeight,
@ -1632,8 +1751,8 @@ export class PerpInfo {
return ` perpMarketIndex: ${this.perpMarketIndex}, base: ${
this.baseLots
}, quote: ${this.quote}, oraclePrice: ${
this.prices.oracle
}, uncapped health contribution ${this.unweightedHealthContribution(
this.basePrices.oracle
}, uncapped health contribution ${this.unweightedHealthUnsettledPnl(
HealthType.init,
)}`;
}
@ -1653,7 +1772,7 @@ export class TokenInfoDto {
initLiabWeight: I80F48Dto;
initScaledLiabWeight: I80F48Dto;
prices: { oracle: I80F48Dto; stable: I80F48Dto };
balanceNative: I80F48Dto;
balanceSpot: I80F48Dto;
constructor(
tokenIndex: number,
@ -1674,7 +1793,7 @@ export class TokenInfoDto {
this.initLiabWeight = initLiabWeight;
this.initScaledLiabWeight = initScaledLiabWeight;
this.prices = prices;
this.balanceNative = balanceNative;
this.balanceSpot = balanceNative;
}
}
@ -1700,6 +1819,7 @@ export class Serum3InfoDto {
export class PerpInfoDto {
perpMarketIndex: number;
settleTokenIndex: number;
maintBaseAssetWeight: I80F48Dto;
initBaseAssetWeight: I80F48Dto;
maintBaseLiabWeight: I80F48Dto;

View File

@ -317,9 +317,12 @@ export class MangoAccount {
return hc.health(healthType);
}
public getPerpSettleHealth(group: Group): I80F48 {
public perpMaxSettle(
group: Group,
perpMarketSettleTokenIndex: TokenIndex,
): I80F48 {
const hc = HealthCache.fromMangoAccount(group, this);
return hc.perpSettleHealth();
return hc.perpMaxSettle(perpMarketSettleTokenIndex);
}
/**
@ -398,18 +401,18 @@ export class MangoAccount {
* Sum of all positive assets.
* @returns assets, in native quote
*/
public getAssetsValue(group: Group, healthType?: HealthType): I80F48 {
public getAssetsValue(group: Group): I80F48 {
const hc = HealthCache.fromMangoAccount(group, this);
return hc.assets(healthType);
return hc.healthAssetsAndLiabs(undefined, false).assets;
}
/**
* Sum of all negative assets.
* @returns liabs, in native quote
*/
public getLiabsValue(group: Group, healthType?: HealthType): I80F48 {
public getLiabsValue(group: Group): I80F48 {
const hc = HealthCache.fromMangoAccount(group, this);
return hc.liabs(healthType);
return hc.healthAssetsAndLiabs(undefined, false).liabs;
}
/**
@ -466,6 +469,7 @@ export class MangoAccount {
* @returns amount of given native token you can borrow, considering all existing assets as collateral, in native token
*
* TODO: take into account net_borrow_limit and min_vault_to_deposits_ratio
* TODO: see max_borrow_for_health_fn
*/
public getMaxWithdrawWithBorrowForToken(
group: Group,
@ -1560,13 +1564,16 @@ export class PerpPosition {
throw new Error("PerpPosition doesn't belong to the given market!");
}
this.updateSettleLimit(perpMarket);
const perpSettleHealth = account.getPerpSettleHealth(group);
const perpMaxSettle = account.perpMaxSettle(
group,
perpMarket.settleTokenIndex,
);
const limitedUnsettled = this.applyPnlSettleLimit(
this.getUnsettledPnl(perpMarket),
perpMarket,
);
if (limitedUnsettled.lt(ZERO_I80F48())) {
return limitedUnsettled.max(perpSettleHealth.max(ZERO_I80F48()).neg());
return limitedUnsettled.max(perpMaxSettle.max(ZERO_I80F48()).neg());
}
return limitedUnsettled;
}

View File

@ -509,12 +509,15 @@ export class PerpMarket {
? accountsWithSettleablePnl[i + 1].settleablePnl
: ZERO_I80F48();
const perpSettleHealth = acc.account.getPerpSettleHealth(group);
const perpMaxSettle = acc.account.perpMaxSettle(
group,
this.settleTokenIndex,
);
acc.settleablePnl =
// need positive settle health to settle against +ve pnl
perpSettleHealth.gt(ZERO_I80F48())
perpMaxSettle.gt(ZERO_I80F48())
? // can only settle min
acc.settleablePnl.max(perpSettleHealth.neg())
acc.settleablePnl.max(perpMaxSettle.neg())
: ZERO_I80F48();
// If the ordering was unchanged `count` times we know we have the top `count` accounts

View File

@ -4550,6 +4550,87 @@ export type MangoV4 = {
}
]
},
{
"name": "perpLiqNegativePnlOrBankruptcyV2",
"accounts": [
{
"name": "group",
"isMut": false,
"isSigner": false
},
{
"name": "liqor",
"isMut": true,
"isSigner": false
},
{
"name": "liqorOwner",
"isMut": false,
"isSigner": true
},
{
"name": "liqee",
"isMut": true,
"isSigner": false
},
{
"name": "perpMarket",
"isMut": true,
"isSigner": false
},
{
"name": "oracle",
"isMut": false,
"isSigner": false
},
{
"name": "settleBank",
"isMut": true,
"isSigner": false
},
{
"name": "settleVault",
"isMut": true,
"isSigner": false
},
{
"name": "settleOracle",
"isMut": false,
"isSigner": false
},
{
"name": "insuranceVault",
"isMut": true,
"isSigner": false
},
{
"name": "insuranceBank",
"isMut": true,
"isSigner": false
},
{
"name": "insuranceBankVault",
"isMut": true,
"isSigner": false
},
{
"name": "insuranceOracle",
"isMut": false,
"isSigner": false
},
{
"name": "tokenProgram",
"isMut": false,
"isSigner": false
}
],
"args": [
{
"name": "maxLiabTransfer",
"type": "u64"
}
]
},
{
"name": "altSet",
"accounts": [
@ -6105,7 +6186,13 @@ export type MangoV4 = {
}
},
{
"name": "balanceNative",
"name": "balanceSpot",
"docs": [
"Freely available spot balance for the token.",
"",
"Includes TokenPosition and free Serum3OpenOrders balances.",
"Does not include perp upnl or Serum3 reserved amounts."
],
"type": {
"defined": "I80F48"
}
@ -6161,6 +6248,10 @@ export type MangoV4 = {
"name": "perpMarketIndex",
"type": "u16"
},
{
"name": "settleTokenIndex",
"type": "u16"
},
{
"name": "maintBaseAssetWeight",
"type": {
@ -6220,7 +6311,7 @@ export type MangoV4 = {
}
},
{
"name": "prices",
"name": "basePrices",
"type": {
"defined": "Prices"
}
@ -6480,8 +6571,14 @@ export type MangoV4 = {
{
"name": "quotePositionNative",
"docs": [
"Active position in quote (conversation rate is that of the time the order was settled)",
"measured in native quote"
"Active position in oracle quote native. At the same time this is 1:1 a settle_token native amount.",
"",
"Example: Say there's a perp market on the BTC/USD price using SOL for settlement. The user buys",
"one long contract for $20k, then base = 1, quote = -20k. The price goes to $21k. Now their",
"unsettled pnl is (1 * 21k - 20k) __SOL__ = 1000 SOL. This is because the perp contract arbitrarily",
"decides that each unit of price difference creates 1 SOL worth of settlement.",
"(yes, causing 1 SOL of settlement for each $1 price change implies a lot of extra leverage; likely",
"there should be an extra configurable scaling factor before we use this for cases like that)"
],
"type": {
"defined": "I80F48"
@ -14366,6 +14463,87 @@ export const IDL: MangoV4 = {
}
]
},
{
"name": "perpLiqNegativePnlOrBankruptcyV2",
"accounts": [
{
"name": "group",
"isMut": false,
"isSigner": false
},
{
"name": "liqor",
"isMut": true,
"isSigner": false
},
{
"name": "liqorOwner",
"isMut": false,
"isSigner": true
},
{
"name": "liqee",
"isMut": true,
"isSigner": false
},
{
"name": "perpMarket",
"isMut": true,
"isSigner": false
},
{
"name": "oracle",
"isMut": false,
"isSigner": false
},
{
"name": "settleBank",
"isMut": true,
"isSigner": false
},
{
"name": "settleVault",
"isMut": true,
"isSigner": false
},
{
"name": "settleOracle",
"isMut": false,
"isSigner": false
},
{
"name": "insuranceVault",
"isMut": true,
"isSigner": false
},
{
"name": "insuranceBank",
"isMut": true,
"isSigner": false
},
{
"name": "insuranceBankVault",
"isMut": true,
"isSigner": false
},
{
"name": "insuranceOracle",
"isMut": false,
"isSigner": false
},
{
"name": "tokenProgram",
"isMut": false,
"isSigner": false
}
],
"args": [
{
"name": "maxLiabTransfer",
"type": "u64"
}
]
},
{
"name": "altSet",
"accounts": [
@ -15921,7 +16099,13 @@ export const IDL: MangoV4 = {
}
},
{
"name": "balanceNative",
"name": "balanceSpot",
"docs": [
"Freely available spot balance for the token.",
"",
"Includes TokenPosition and free Serum3OpenOrders balances.",
"Does not include perp upnl or Serum3 reserved amounts."
],
"type": {
"defined": "I80F48"
}
@ -15977,6 +16161,10 @@ export const IDL: MangoV4 = {
"name": "perpMarketIndex",
"type": "u16"
},
{
"name": "settleTokenIndex",
"type": "u16"
},
{
"name": "maintBaseAssetWeight",
"type": {
@ -16036,7 +16224,7 @@ export const IDL: MangoV4 = {
}
},
{
"name": "prices",
"name": "basePrices",
"type": {
"defined": "Prices"
}
@ -16296,8 +16484,14 @@ export const IDL: MangoV4 = {
{
"name": "quotePositionNative",
"docs": [
"Active position in quote (conversation rate is that of the time the order was settled)",
"measured in native quote"
"Active position in oracle quote native. At the same time this is 1:1 a settle_token native amount.",
"",
"Example: Say there's a perp market on the BTC/USD price using SOL for settlement. The user buys",
"one long contract for $20k, then base = 1, quote = -20k. The price goes to $21k. Now their",
"unsettled pnl is (1 * 21k - 20k) __SOL__ = 1000 SOL. This is because the perp contract arbitrarily",
"decides that each unit of price difference creates 1 SOL worth of settlement.",
"(yes, causing 1 SOL of settlement for each $1 price change implies a lot of extra leverage; likely",
"there should be an extra configurable scaling factor before we use this for cases like that)"
],
"type": {
"defined": "I80F48"

View File

@ -110,9 +110,6 @@ export async function getPriceImpactForLiqor(
account: a,
health: a.getHealth(group, HealthType.liquidationEnd),
healthRatio: a.getHealthRatioUi(group, HealthType.liquidationEnd),
liabs: toUiDecimalsForQuote(
a.getLiabsValue(group, HealthType.liquidationEnd),
),
};
});
@ -308,9 +305,6 @@ export async function getPerpPositionsToBeLiquidated(
account: a,
health: a.getHealth(group, HealthType.liquidationEnd),
healthRatio: a.getHealthRatioUi(group, HealthType.liquidationEnd),
liabs: toUiDecimalsForQuote(
a.getLiabsValue(group, HealthType.liquidationEnd),
),
};
});