2022-09-01 09:07:57 -07:00
|
|
|
use anchor_lang::prelude::*;
|
|
|
|
use checked_math as cm;
|
|
|
|
use fixed::types::I80F48;
|
|
|
|
|
|
|
|
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};
|
2022-09-01 09:07:57 -07:00
|
|
|
use crate::state::Bank;
|
2022-12-29 02:48:46 -08:00
|
|
|
use crate::state::{Group, MangoAccountFixed, MangoAccountLoader, PerpMarket};
|
2022-09-01 09:07:57 -07:00
|
|
|
|
|
|
|
#[derive(Accounts)]
|
|
|
|
pub struct PerpSettlePnl<'info> {
|
|
|
|
pub group: AccountLoader<'info, Group>,
|
|
|
|
|
2022-09-29 03:59:55 -07:00
|
|
|
#[account(
|
|
|
|
mut,
|
|
|
|
has_one = group,
|
|
|
|
// settler_owner is checked at #1
|
|
|
|
)]
|
2022-12-29 02:48:46 -08:00
|
|
|
pub settler: AccountLoader<'info, MangoAccountFixed>,
|
2022-09-29 03:59:55 -07:00
|
|
|
pub settler_owner: Signer<'info>,
|
|
|
|
|
2022-09-01 09:07:57 -07:00
|
|
|
#[account(has_one = group, has_one = oracle)]
|
|
|
|
pub perp_market: AccountLoader<'info, PerpMarket>,
|
|
|
|
|
|
|
|
// This account MUST be profitable
|
|
|
|
#[account(mut, has_one = group)]
|
2022-12-29 02:48:46 -08:00
|
|
|
pub account_a: AccountLoader<'info, MangoAccountFixed>,
|
2022-09-01 09:07:57 -07:00
|
|
|
// This account MUST have a loss
|
|
|
|
#[account(mut, has_one = group)]
|
2022-12-29 02:48:46 -08:00
|
|
|
pub account_b: AccountLoader<'info, MangoAccountFixed>,
|
2022-09-01 09:07:57 -07:00
|
|
|
|
2022-09-02 04:14:27 -07:00
|
|
|
/// CHECK: Oracle can have different account types, constrained by address in perp_market
|
2022-09-01 09:07:57 -07:00
|
|
|
pub oracle: UncheckedAccount<'info>,
|
|
|
|
|
|
|
|
#[account(mut, has_one = group)]
|
2022-09-29 05:13:28 -07:00
|
|
|
pub settle_bank: AccountLoader<'info, Bank>,
|
|
|
|
|
|
|
|
/// CHECK: Oracle can have different account types
|
|
|
|
#[account(address = settle_bank.load()?.oracle)]
|
|
|
|
pub settle_oracle: UncheckedAccount<'info>,
|
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
|
|
|
|
require!(
|
2022-09-29 03:59:55 -07:00
|
|
|
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
|
|
|
}
|
|
|
|
|
2022-09-29 03:59:55 -07:00
|
|
|
let a_init_health;
|
|
|
|
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)?;
|
|
|
|
a_init_health = a_cache.health(HealthType::Init);
|
|
|
|
a_maint_health = a_cache.health(HealthType::Maint);
|
|
|
|
};
|
|
|
|
|
2022-09-14 04:26:29 -07:00
|
|
|
// Account B is the one that must have negative pnl. Check how much of that may be actualized
|
|
|
|
// given the account's health. In that, we only care about the health of spot assets on the account.
|
|
|
|
// Example: With +100 USDC and -2 SOL (-80 USD) and -500 USD PNL the account may still settle
|
|
|
|
// 100 - 1.1*80 = 12 USD perp pnl, even though the overall health is already negative.
|
2022-09-29 03:59:55 -07:00
|
|
|
// Further settlement would convert perp-losses into token-losses and isn't allowed.
|
2022-09-29 05:35:01 -07:00
|
|
|
require!(b_settle_health >= 0, MangoError::HealthMustBePositive);
|
2022-09-14 04:26:29 -07:00
|
|
|
|
2022-09-29 05:13:28 -07:00
|
|
|
let mut bank = ctx.accounts.settle_bank.load_mut()?;
|
2022-09-01 09:07:57 -07:00
|
|
|
let perp_market = ctx.accounts.perp_market.load()?;
|
|
|
|
|
|
|
|
// Verify that the bank is the quote currency bank
|
|
|
|
require!(
|
2022-09-29 05:13:28 -07:00
|
|
|
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
|
|
|
|
|
|
|
// Fetch perp positions for accounts
|
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
|
|
|
|
|
|
|
// Settle funding before settling any PnL
|
|
|
|
a_perp_position.settle_funding(&perp_market);
|
|
|
|
b_perp_position.settle_funding(&perp_market);
|
|
|
|
|
|
|
|
// Calculate PnL for each account
|
2022-11-17 23:58:56 -08:00
|
|
|
let a_pnl = a_perp_position.pnl_for_price(&perp_market, oracle_price)?;
|
|
|
|
let b_pnl = b_perp_position.pnl_for_price(&perp_market, oracle_price)?;
|
2022-09-01 09:07:57 -07:00
|
|
|
|
|
|
|
// Account A must be profitable, and B must be unprofitable
|
|
|
|
// PnL must be opposite signs for there to be a settlement
|
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
|
|
|
|
2022-11-30 04:20:19 -08:00
|
|
|
// Cap settlement of unrealized pnl
|
|
|
|
// Settles at most x100% each hour
|
|
|
|
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);
|
|
|
|
b_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);
|
|
|
|
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
|
|
|
);
|
|
|
|
|
2022-09-29 05:35:01 -07:00
|
|
|
// Settle for the maximum possible capped to b's settle health
|
2023-01-11 05:32:15 -08:00
|
|
|
let settlement = a_settleable_pnl
|
|
|
|
.abs()
|
|
|
|
.min(b_settleable_pnl.abs())
|
|
|
|
.min(b_settle_health);
|
|
|
|
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
|
|
|
|
|
|
|
// Settle
|
|
|
|
a_perp_position.record_settle(settlement);
|
|
|
|
b_perp_position.record_settle(-settlement);
|
2022-09-01 09:07:57 -07:00
|
|
|
|
2022-10-07 12:12:55 -07:00
|
|
|
emit_perp_balances(
|
|
|
|
ctx.accounts.group.key(),
|
|
|
|
ctx.accounts.account_a.key(),
|
|
|
|
perp_market.perp_market_index,
|
|
|
|
a_perp_position,
|
|
|
|
&perp_market,
|
|
|
|
);
|
|
|
|
|
|
|
|
emit_perp_balances(
|
|
|
|
ctx.accounts.group.key(),
|
|
|
|
ctx.accounts.account_b.key(),
|
|
|
|
perp_market.perp_market_index,
|
|
|
|
b_perp_position,
|
|
|
|
&perp_market,
|
|
|
|
);
|
|
|
|
|
2022-09-29 03:59:55 -07:00
|
|
|
// A percentage fee is paid to the settler when account_a's health is low.
|
|
|
|
// That's because the settlement could avoid it getting liquidated.
|
|
|
|
let low_health_fee = if a_init_health < 0 {
|
|
|
|
let fee_fraction = I80F48::from_num(perp_market.settle_fee_fraction_low_health);
|
|
|
|
if a_maint_health < 0 {
|
|
|
|
cm!(settlement * fee_fraction)
|
|
|
|
} else {
|
|
|
|
cm!(settlement * fee_fraction * (-a_init_health / (a_maint_health - a_init_health)))
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
I80F48::ZERO
|
|
|
|
};
|
|
|
|
|
|
|
|
// The settler receives a flat fee
|
|
|
|
let flat_fee = I80F48::from_num(perp_market.settle_fee_flat);
|
|
|
|
|
|
|
|
// Fees only apply when the settlement is large enough
|
|
|
|
let fee = if settlement >= perp_market.settle_fee_amount_threshold {
|
|
|
|
cm!(low_health_fee + flat_fee).min(settlement)
|
|
|
|
} else {
|
|
|
|
I80F48::ZERO
|
|
|
|
};
|
|
|
|
|
2022-11-17 23:58:56 -08:00
|
|
|
// Safety check to prevent any accidental negative transfer
|
|
|
|
require!(fee >= 0, MangoError::SettlementAmountMustBePositive);
|
|
|
|
|
2022-09-29 03:59:55 -07:00
|
|
|
// Update the account's net_settled with the new PnL.
|
|
|
|
// Applying the fee here means that it decreases the displayed perp pnl.
|
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
|
|
|
|
2022-11-25 04:45:17 -08:00
|
|
|
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
|
|
|
|
|
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;
|
2022-11-25 04:45:17 -08:00
|
|
|
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.
|
|
|
|
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(),
|
|
|
|
mango_account: ctx.accounts.settler.key(),
|
|
|
|
token_index: settle_token_index,
|
|
|
|
indexed_position: a_token_position.indexed_position.to_bits(),
|
|
|
|
deposit_index: bank.deposit_index.to_bits(),
|
|
|
|
borrow_index: bank.borrow_index.to_bits(),
|
|
|
|
});
|
|
|
|
|
|
|
|
emit!(TokenBalanceLog {
|
|
|
|
mango_group: ctx.accounts.group.key(),
|
|
|
|
mango_account: ctx.accounts.settler.key(),
|
|
|
|
token_index: settle_token_index,
|
|
|
|
indexed_position: b_token_position.indexed_position.to_bits(),
|
|
|
|
deposit_index: bank.deposit_index.to_bits(),
|
|
|
|
borrow_index: bank.borrow_index.to_bits(),
|
|
|
|
});
|
|
|
|
|
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)?;
|
2022-11-25 04:45:17 -08:00
|
|
|
let settler_token_position_active = 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(),
|
|
|
|
deposit_index: bank.deposit_index.to_bits(),
|
|
|
|
borrow_index: bank.borrow_index.to_bits(),
|
|
|
|
});
|
|
|
|
|
|
|
|
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(),
|
|
|
|
perp_market_index: perp_market_index,
|
|
|
|
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(())
|
|
|
|
}
|