2022-09-01 09:07:57 -07:00
|
|
|
use anchor_lang::prelude::*;
|
|
|
|
use checked_math as cm;
|
|
|
|
use fixed::types::I80F48;
|
|
|
|
|
2023-02-14 23:42:07 -08:00
|
|
|
use crate::accounts_ix::*;
|
2022-09-01 09:07:57 -07:00
|
|
|
use crate::accounts_zerocopy::*;
|
|
|
|
use crate::error::*;
|
2022-12-08 04:12:43 -08:00
|
|
|
use crate::health::{new_health_cache, HealthType, ScanningAccountRetriever};
|
2022-10-07 12:12:55 -07:00
|
|
|
use crate::logs::{emit_perp_balances, PerpSettlePnlLog, TokenBalanceLog};
|
2023-02-14 23:42:07 -08:00
|
|
|
use crate::state::*;
|
2022-09-01 09:07:57 -07:00
|
|
|
|
2022-09-29 03:59:55 -07:00
|
|
|
pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>) -> Result<()> {
|
2022-09-01 09:07:57 -07:00
|
|
|
// Cannot settle with yourself
|
2023-02-01 07:15:58 -08:00
|
|
|
require_keys_neq!(
|
|
|
|
ctx.accounts.account_a.key(),
|
|
|
|
ctx.accounts.account_b.key(),
|
2022-09-01 09:07:57 -07:00
|
|
|
MangoError::CannotSettleWithSelf
|
|
|
|
);
|
|
|
|
|
2022-09-29 05:13:28 -07:00
|
|
|
let (perp_market_index, settle_token_index) = {
|
2022-09-14 04:26:29 -07:00
|
|
|
let perp_market = ctx.accounts.perp_market.load()?;
|
2022-09-29 05:13:28 -07:00
|
|
|
(
|
|
|
|
perp_market.perp_market_index,
|
|
|
|
perp_market.settle_token_index,
|
|
|
|
)
|
2022-09-14 04:26:29 -07:00
|
|
|
};
|
|
|
|
|
2022-12-29 02:48:46 -08:00
|
|
|
let mut account_a = ctx.accounts.account_a.load_full_mut()?;
|
|
|
|
let mut account_b = ctx.accounts.account_b.load_full_mut()?;
|
2022-09-14 04:26:29 -07:00
|
|
|
|
|
|
|
// check positions exist, for nicer error messages
|
|
|
|
{
|
|
|
|
account_a.perp_position(perp_market_index)?;
|
2022-09-29 05:13:28 -07:00
|
|
|
account_a.token_position(settle_token_index)?;
|
2022-09-14 04:26:29 -07:00
|
|
|
account_b.perp_position(perp_market_index)?;
|
2022-09-29 05:13:28 -07:00
|
|
|
account_b.token_position(settle_token_index)?;
|
2022-09-14 04:26:29 -07:00
|
|
|
}
|
|
|
|
|
2023-02-10 00:00:36 -08:00
|
|
|
let a_liq_end_health;
|
2022-09-29 03:59:55 -07:00
|
|
|
let a_maint_health;
|
2022-09-29 05:35:01 -07:00
|
|
|
let b_settle_health;
|
2022-09-29 03:59:55 -07:00
|
|
|
{
|
|
|
|
let retriever =
|
|
|
|
ScanningAccountRetriever::new(ctx.remaining_accounts, &ctx.accounts.group.key())
|
|
|
|
.context("create account retriever")?;
|
2022-09-29 05:35:01 -07:00
|
|
|
b_settle_health = new_health_cache(&account_b.borrow(), &retriever)?.perp_settle_health();
|
2022-09-29 03:59:55 -07:00
|
|
|
let a_cache = new_health_cache(&account_a.borrow(), &retriever)?;
|
2023-02-10 00:00:36 -08:00
|
|
|
a_liq_end_health = a_cache.health(HealthType::LiquidationEnd);
|
2022-09-29 03:59:55 -07:00
|
|
|
a_maint_health = a_cache.health(HealthType::Maint);
|
|
|
|
};
|
|
|
|
|
2023-01-12 00:07:13 -08:00
|
|
|
let mut settle_bank = ctx.accounts.settle_bank.load_mut()?;
|
2022-09-01 09:07:57 -07:00
|
|
|
let perp_market = ctx.accounts.perp_market.load()?;
|
|
|
|
|
2023-01-18 04:19:10 -08:00
|
|
|
// Verify that the bank is the quote currency bank (#2)
|
2022-09-01 09:07:57 -07:00
|
|
|
require!(
|
2023-01-12 00:07:13 -08:00
|
|
|
settle_bank.token_index == settle_token_index,
|
2022-09-01 09:07:57 -07:00
|
|
|
MangoError::InvalidBank
|
|
|
|
);
|
|
|
|
|
|
|
|
// Get oracle price for market. Price is validated inside
|
2022-11-10 06:47:11 -08:00
|
|
|
let oracle_price = perp_market.oracle_price(
|
|
|
|
&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?,
|
|
|
|
None, // staleness checked in health
|
|
|
|
)?;
|
2022-09-01 09:07:57 -07:00
|
|
|
|
2023-01-12 00:07:13 -08:00
|
|
|
// Fetch perp position and pnl
|
2022-09-14 04:26:29 -07:00
|
|
|
let a_perp_position = account_a.perp_position_mut(perp_market_index)?;
|
|
|
|
let b_perp_position = account_b.perp_position_mut(perp_market_index)?;
|
2022-09-01 09:07:57 -07:00
|
|
|
a_perp_position.settle_funding(&perp_market);
|
|
|
|
b_perp_position.settle_funding(&perp_market);
|
2023-01-17 05:07:58 -08:00
|
|
|
let a_pnl = a_perp_position.unsettled_pnl(&perp_market, oracle_price)?;
|
|
|
|
let b_pnl = b_perp_position.unsettled_pnl(&perp_market, oracle_price)?;
|
2022-09-01 09:07:57 -07:00
|
|
|
|
2023-01-12 00:07:13 -08:00
|
|
|
// PnL must have opposite signs for there to be a settlement:
|
|
|
|
// Account A must be profitable, and B must be unprofitable.
|
2023-01-11 05:32:15 -08:00
|
|
|
require_msg_typed!(
|
|
|
|
a_pnl.is_positive(),
|
|
|
|
MangoError::ProfitabilityMismatch,
|
|
|
|
"account a pnl is not positive: {}",
|
|
|
|
a_pnl
|
|
|
|
);
|
|
|
|
require_msg_typed!(
|
|
|
|
b_pnl.is_negative(),
|
|
|
|
MangoError::ProfitabilityMismatch,
|
|
|
|
"account b pnl is not negative: {}",
|
|
|
|
b_pnl
|
|
|
|
);
|
2022-09-01 09:07:57 -07:00
|
|
|
|
2023-01-12 00:07:13 -08:00
|
|
|
// Apply pnl settle limits
|
2022-11-30 04:20:19 -08:00
|
|
|
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
|
2022-12-10 08:56:56 -08:00
|
|
|
a_perp_position.update_settle_limit(&perp_market, now_ts);
|
2023-01-11 05:32:15 -08:00
|
|
|
let a_settleable_pnl = a_perp_position.apply_pnl_settle_limit(&perp_market, a_pnl);
|
2023-01-12 00:07:13 -08:00
|
|
|
b_perp_position.update_settle_limit(&perp_market, now_ts);
|
2023-01-11 05:32:15 -08:00
|
|
|
let b_settleable_pnl = b_perp_position.apply_pnl_settle_limit(&perp_market, b_pnl);
|
2022-11-30 04:20:19 -08:00
|
|
|
|
2023-01-11 05:32:15 -08:00
|
|
|
require_msg_typed!(
|
2022-11-30 04:20:19 -08:00
|
|
|
a_settleable_pnl.is_positive(),
|
2023-01-11 05:32:15 -08:00
|
|
|
MangoError::ProfitabilityMismatch,
|
|
|
|
"account a settleable pnl is not positive: {}, pnl: {}",
|
|
|
|
a_settleable_pnl,
|
|
|
|
a_pnl
|
|
|
|
);
|
|
|
|
require_msg_typed!(
|
|
|
|
b_settleable_pnl.is_negative(),
|
|
|
|
MangoError::ProfitabilityMismatch,
|
|
|
|
"account b settleable pnl is not negative: {}, pnl: {}",
|
|
|
|
b_settleable_pnl,
|
|
|
|
b_pnl
|
2022-11-30 04:20:19 -08:00
|
|
|
);
|
|
|
|
|
2023-01-12 00:07:13 -08:00
|
|
|
// Check how much of account b's negative pnl may be actualized given the health.
|
|
|
|
// In that, we only care about the health of spot assets on the account.
|
|
|
|
// Example: With +100 USDC and -2 SOL (-80 USD) and -500 USD PNL the account may still settle
|
|
|
|
// 100 - 1.1*80 = 12 USD perp pnl, even though the overall health is already negative.
|
|
|
|
// Further settlement would convert perp-losses into unbacked token-losses and isn't allowed.
|
|
|
|
require_msg_typed!(
|
|
|
|
b_settle_health >= 0,
|
|
|
|
MangoError::HealthMustBePositive,
|
|
|
|
"account b settle health is negative: {}",
|
|
|
|
b_settle_health
|
|
|
|
);
|
|
|
|
|
|
|
|
// Settle for the maximum possible capped to target's settle health
|
2023-01-11 05:32:15 -08:00
|
|
|
let settlement = a_settleable_pnl
|
2023-01-12 00:07:13 -08:00
|
|
|
.min(-b_settleable_pnl)
|
|
|
|
.min(b_settle_health)
|
|
|
|
.max(I80F48::ZERO);
|
2023-01-11 05:32:15 -08:00
|
|
|
require_msg_typed!(
|
|
|
|
settlement >= 0,
|
|
|
|
MangoError::SettlementAmountMustBePositive,
|
|
|
|
"a settleable: {}, b settleable: {}, b settle health: {}",
|
|
|
|
a_settleable_pnl,
|
|
|
|
b_settleable_pnl,
|
|
|
|
b_settle_health,
|
|
|
|
);
|
2022-11-30 04:20:19 -08:00
|
|
|
|
2023-02-10 00:00:36 -08:00
|
|
|
let fee = compute_settle_fee(&perp_market, a_liq_end_health, a_maint_health, settlement)?;
|
2023-01-12 00:07:13 -08:00
|
|
|
|
2022-11-30 04:20:19 -08:00
|
|
|
a_perp_position.record_settle(settlement);
|
|
|
|
b_perp_position.record_settle(-settlement);
|
2022-10-07 12:12:55 -07:00
|
|
|
emit_perp_balances(
|
|
|
|
ctx.accounts.group.key(),
|
|
|
|
ctx.accounts.account_a.key(),
|
|
|
|
a_perp_position,
|
|
|
|
&perp_market,
|
|
|
|
);
|
|
|
|
emit_perp_balances(
|
|
|
|
ctx.accounts.group.key(),
|
|
|
|
ctx.accounts.account_b.key(),
|
|
|
|
b_perp_position,
|
|
|
|
&perp_market,
|
|
|
|
);
|
|
|
|
|
2023-01-12 00:07:13 -08:00
|
|
|
// Update the accounts' perp_spot_transfer statistics.
|
|
|
|
//
|
2022-09-29 03:59:55 -07:00
|
|
|
// Applying the fee here means that it decreases the displayed perp pnl.
|
2023-01-12 00:07:13 -08:00
|
|
|
// Think about it like this: a's pnl reduces by `settlement` and spot increases by `settlement - fee`.
|
|
|
|
// That means that it managed to extract `settlement - fee` from perp interactions.
|
2022-11-30 04:20:19 -08:00
|
|
|
let settlement_i64 = settlement.round_to_zero().checked_to_num::<i64>().unwrap();
|
|
|
|
let fee_i64 = fee.round_to_zero().checked_to_num::<i64>().unwrap();
|
2022-10-07 12:12:55 -07:00
|
|
|
cm!(a_perp_position.perp_spot_transfers += settlement_i64 - fee_i64);
|
|
|
|
cm!(b_perp_position.perp_spot_transfers -= settlement_i64);
|
|
|
|
cm!(account_a.fixed.perp_spot_transfers += settlement_i64 - fee_i64);
|
|
|
|
cm!(account_b.fixed.perp_spot_transfers -= settlement_i64);
|
2022-09-01 09:07:57 -07:00
|
|
|
|
|
|
|
// Transfer token balances
|
2022-09-29 03:59:55 -07:00
|
|
|
// The fee is paid by the account with positive unsettled pnl
|
2022-09-29 05:13:28 -07:00
|
|
|
let a_token_position = account_a.token_position_mut(settle_token_index)?.0;
|
|
|
|
let b_token_position = account_b.token_position_mut(settle_token_index)?.0;
|
2023-01-12 00:07:13 -08:00
|
|
|
settle_bank.deposit(a_token_position, cm!(settlement - fee), now_ts)?;
|
2022-12-30 00:54:31 -08:00
|
|
|
// Don't charge loan origination fees on borrows created via settling:
|
|
|
|
// Even small loan origination fees could accumulate if a perp position is
|
|
|
|
// settled back and forth repeatedly.
|
2023-01-12 00:07:13 -08:00
|
|
|
settle_bank.withdraw_without_fee(b_token_position, settlement, now_ts, oracle_price)?;
|
2022-09-01 09:07:57 -07:00
|
|
|
|
2022-10-07 12:12:55 -07:00
|
|
|
emit!(TokenBalanceLog {
|
|
|
|
mango_group: ctx.accounts.group.key(),
|
2023-01-12 00:07:13 -08:00
|
|
|
mango_account: ctx.accounts.account_a.key(),
|
2022-10-07 12:12:55 -07:00
|
|
|
token_index: settle_token_index,
|
|
|
|
indexed_position: a_token_position.indexed_position.to_bits(),
|
2023-01-12 00:07:13 -08:00
|
|
|
deposit_index: settle_bank.deposit_index.to_bits(),
|
|
|
|
borrow_index: settle_bank.borrow_index.to_bits(),
|
2022-10-07 12:12:55 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
emit!(TokenBalanceLog {
|
|
|
|
mango_group: ctx.accounts.group.key(),
|
2023-01-12 00:07:13 -08:00
|
|
|
mango_account: ctx.accounts.account_b.key(),
|
2022-10-07 12:12:55 -07:00
|
|
|
token_index: settle_token_index,
|
|
|
|
indexed_position: b_token_position.indexed_position.to_bits(),
|
2023-01-12 00:07:13 -08:00
|
|
|
deposit_index: settle_bank.deposit_index.to_bits(),
|
|
|
|
borrow_index: settle_bank.borrow_index.to_bits(),
|
2022-10-07 12:12:55 -07:00
|
|
|
});
|
|
|
|
|
2022-09-29 03:59:55 -07:00
|
|
|
// settler might be the same as account a or b
|
|
|
|
drop(account_a);
|
|
|
|
drop(account_b);
|
|
|
|
|
2022-12-29 02:48:46 -08:00
|
|
|
let mut settler = ctx.accounts.settler.load_full_mut()?;
|
2022-09-29 03:59:55 -07:00
|
|
|
// account constraint #1
|
|
|
|
require!(
|
|
|
|
settler
|
|
|
|
.fixed
|
|
|
|
.is_owner_or_delegate(ctx.accounts.settler_owner.key()),
|
|
|
|
MangoError::SomeError
|
|
|
|
);
|
|
|
|
|
|
|
|
let (settler_token_position, settler_token_raw_index, _) =
|
2022-09-29 05:13:28 -07:00
|
|
|
settler.ensure_token_position(settle_token_index)?;
|
2023-01-12 00:07:13 -08:00
|
|
|
let settler_token_position_active = settle_bank.deposit(settler_token_position, fee, now_ts)?;
|
2022-10-07 12:12:55 -07:00
|
|
|
|
|
|
|
emit!(TokenBalanceLog {
|
|
|
|
mango_group: ctx.accounts.group.key(),
|
|
|
|
mango_account: ctx.accounts.settler.key(),
|
|
|
|
token_index: settler_token_position.token_index,
|
|
|
|
indexed_position: settler_token_position.indexed_position.to_bits(),
|
2023-01-12 00:07:13 -08:00
|
|
|
deposit_index: settle_bank.deposit_index.to_bits(),
|
|
|
|
borrow_index: settle_bank.borrow_index.to_bits(),
|
2022-10-07 12:12:55 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
if !settler_token_position_active {
|
|
|
|
settler
|
|
|
|
.deactivate_token_position_and_log(settler_token_raw_index, ctx.accounts.settler.key());
|
2022-09-29 03:59:55 -07:00
|
|
|
}
|
2022-09-01 09:07:57 -07:00
|
|
|
|
2022-10-07 12:12:55 -07:00
|
|
|
emit!(PerpSettlePnlLog {
|
|
|
|
mango_group: ctx.accounts.group.key(),
|
|
|
|
mango_account_a: ctx.accounts.account_a.key(),
|
|
|
|
mango_account_b: ctx.accounts.account_b.key(),
|
2023-01-12 05:26:55 -08:00
|
|
|
perp_market_index,
|
2022-10-07 12:12:55 -07:00
|
|
|
settlement: settlement.to_bits(),
|
|
|
|
settler: ctx.accounts.settler.key(),
|
|
|
|
fee: fee.to_bits(),
|
|
|
|
});
|
|
|
|
|
2022-09-29 03:59:55 -07:00
|
|
|
msg!("settled pnl = {}, fee = {}", settlement, fee);
|
2022-09-01 09:07:57 -07:00
|
|
|
Ok(())
|
|
|
|
}
|
2023-01-12 00:07:13 -08:00
|
|
|
|
|
|
|
pub fn compute_settle_fee(
|
|
|
|
perp_market: &PerpMarket,
|
2023-02-10 00:00:36 -08:00
|
|
|
source_liq_end_health: I80F48,
|
2023-01-12 00:07:13 -08:00
|
|
|
source_maint_health: I80F48,
|
|
|
|
settlement: I80F48,
|
|
|
|
) -> Result<I80F48> {
|
2023-02-10 00:00:36 -08:00
|
|
|
assert!(source_maint_health >= source_liq_end_health);
|
|
|
|
|
2023-01-12 00:07:13 -08:00
|
|
|
// A percentage fee is paid to the settler when the source account's health is low.
|
|
|
|
// That's because the settlement could avoid it getting liquidated: settling will
|
|
|
|
// increase its health by actualizing positive perp pnl.
|
2023-02-10 00:00:36 -08:00
|
|
|
let low_health_fee = if source_liq_end_health < 0 {
|
2023-01-12 00:07:13 -08:00
|
|
|
let fee_fraction = I80F48::from_num(perp_market.settle_fee_fraction_low_health);
|
|
|
|
if source_maint_health < 0 {
|
|
|
|
cm!(settlement * fee_fraction)
|
|
|
|
} else {
|
|
|
|
cm!(settlement
|
|
|
|
* fee_fraction
|
2023-02-10 00:00:36 -08:00
|
|
|
* (-source_liq_end_health / (source_maint_health - source_liq_end_health)))
|
2023-01-12 00:07:13 -08:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
I80F48::ZERO
|
|
|
|
};
|
|
|
|
|
|
|
|
// The settler receives a flat fee
|
|
|
|
let flat_fee = I80F48::from_num(perp_market.settle_fee_flat);
|
|
|
|
|
|
|
|
// Fees only apply when the settlement is large enough
|
|
|
|
let fee = if settlement >= perp_market.settle_fee_amount_threshold {
|
|
|
|
cm!(low_health_fee + flat_fee).min(settlement)
|
|
|
|
} else {
|
|
|
|
I80F48::ZERO
|
|
|
|
};
|
|
|
|
|
|
|
|
// Safety check to prevent any accidental negative transfer
|
|
|
|
require!(fee >= 0, MangoError::SettlementAmountMustBePositive);
|
|
|
|
|
|
|
|
Ok(fee)
|
|
|
|
}
|