Perp force close positions in a market (#525)

* force close tokens

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* Fixes from review

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* Fixes from review

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* Fixes from review

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* Fixes from review

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* Fixes from review

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* add test

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* Fixes from review

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* Fixes from review

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* Fixes from review

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* Fixes from review

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* Fixes from review

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* Fixes from review

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* reset

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* force close perp market

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* Fixes from review

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* Fixes from review

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* Fixes from review

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* rename

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* test

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* update

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* add back staleness slot check

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* Fixes from review

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

---------

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>
This commit is contained in:
microwavedcola1 2023-04-19 17:42:01 +02:00 committed by GitHub
parent 55bfcc3a76
commit 6ac9f19287
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 533 additions and 6 deletions

View File

@ -3092,6 +3092,12 @@
"type": {
"option": "string"
}
},
{
"name": "forceCloseOpt",
"type": {
"option": "bool"
}
}
]
},
@ -3635,6 +3641,37 @@
],
"args": []
},
{
"name": "perpForceClosePosition",
"accounts": [
{
"name": "group",
"isMut": false,
"isSigner": false
},
{
"name": "perpMarket",
"isMut": true,
"isSigner": false
},
{
"name": "accountA",
"isMut": true,
"isSigner": false
},
{
"name": "accountB",
"isMut": true,
"isSigner": false
},
{
"name": "oracle",
"isMut": false,
"isSigner": false
}
],
"args": []
},
{
"name": "perpSettleFees",
"accounts": [
@ -5048,12 +5085,16 @@
],
"type": "u8"
},
{
"name": "forceClose",
"type": "u8"
},
{
"name": "padding4",
"type": {
"array": [
"u8",
7
6
]
}
},
@ -7065,6 +7106,9 @@
},
{
"name": "TokenForceCloseBorrowsWithToken"
},
{
"name": "PerpForceClosePosition"
}
]
}

View File

@ -23,6 +23,7 @@ pub use perp_consume_events::*;
pub use perp_create_market::*;
pub use perp_deactivate_position::*;
pub use perp_edit_market::*;
pub use perp_force_close_position::*;
pub use perp_liq_base_or_positive_pnl::*;
pub use perp_liq_force_cancel_orders::*;
pub use perp_liq_negative_pnl_or_bankruptcy::*;
@ -80,6 +81,7 @@ mod perp_consume_events;
mod perp_create_market;
mod perp_deactivate_position;
mod perp_edit_market;
mod perp_force_close_position;
mod perp_liq_base_or_positive_pnl;
mod perp_liq_force_cancel_orders;
mod perp_liq_negative_pnl_or_bankruptcy;

View File

@ -0,0 +1,37 @@
use crate::error::*;
use crate::state::*;
use anchor_lang::prelude::*;
#[derive(Accounts)]
pub struct PerpForceClosePosition<'info> {
#[account(
constraint = group.load()?.is_ix_enabled(IxGate::PerpForceClosePosition) @ MangoError::IxIsDisabled,
)]
pub group: AccountLoader<'info, Group>,
#[account(
mut,
has_one = group,
has_one = oracle,
constraint = perp_market.load()?.is_force_close()
)]
pub perp_market: AccountLoader<'info, PerpMarket>,
#[account(
mut,
has_one = group,
constraint = account_a.load()?.is_operational() @ MangoError::AccountIsFrozen,
constraint = account_a.key() != account_b.key()
)]
pub account_a: AccountLoader<'info, MangoAccountFixed>,
#[account(
mut,
has_one = group,
constraint = account_b.load()?.is_operational() @ MangoError::AccountIsFrozen
)]
pub account_b: AccountLoader<'info, MangoAccountFixed>,
/// CHECK: Oracle can have different account types, constrained by address in perp_market
pub oracle: UncheckedAccount<'info>,
}

View File

@ -66,6 +66,7 @@ pub fn ix_gate_set(ctx: Context<IxGateSet>, ix_gate: u128) -> Result<()> {
log_if_changed(&group, ix_gate, IxGate::TokenWithdraw);
log_if_changed(&group, ix_gate, IxGate::AccountBuybackFeesWithMngo);
log_if_changed(&group, ix_gate, IxGate::TokenForceCloseBorrowsWithToken);
log_if_changed(&group, ix_gate, IxGate::PerpForceClosePosition);
group.ix_gate = ix_gate;

View File

@ -23,6 +23,7 @@ pub use perp_consume_events::*;
pub use perp_create_market::*;
pub use perp_deactivate_position::*;
pub use perp_edit_market::*;
pub use perp_force_close_position::*;
pub use perp_liq_base_or_positive_pnl::*;
pub use perp_liq_force_cancel_orders::*;
pub use perp_liq_negative_pnl_or_bankruptcy::*;
@ -80,6 +81,7 @@ mod perp_consume_events;
mod perp_create_market;
mod perp_deactivate_position;
mod perp_edit_market;
mod perp_force_close_position;
mod perp_liq_base_or_positive_pnl;
mod perp_liq_force_cancel_orders;
mod perp_liq_negative_pnl_or_bankruptcy;

View File

@ -97,6 +97,7 @@ pub fn perp_create_market(
padding3: Default::default(),
settle_pnl_limit_window_size_ts,
reduce_only: 0,
force_close: 0,
padding4: Default::default(),
maint_overall_asset_weight: I80F48::from_num(maint_overall_asset_weight),
init_overall_asset_weight: I80F48::from_num(init_overall_asset_weight),

View File

@ -38,6 +38,7 @@ pub fn perp_edit_market(
reset_stable_price: bool,
positive_pnl_liquidation_fee_opt: Option<f32>,
name_opt: Option<String>,
force_close_opt: Option<bool>,
) -> Result<()> {
let group = ctx.accounts.group.load()?;
@ -330,6 +331,19 @@ pub fn perp_edit_market(
require_group_admin = true;
};
if let Some(force_close) = force_close_opt {
if force_close {
require!(perp_market.reduce_only > 0, MangoError::SomeError);
}
msg!(
"Force close: old - {:?}, new - {:?}",
perp_market.force_close,
u8::from(force_close)
);
perp_market.force_close = u8::from(force_close);
require_group_admin = true;
};
// account constraint #1
if require_group_admin {
require!(

View File

@ -0,0 +1,62 @@
use anchor_lang::prelude::*;
use crate::accounts_ix::*;
use crate::accounts_zerocopy::AccountInfoRef;
use crate::error::MangoError;
use crate::logs::emit_perp_balances;
use crate::state::*;
use fixed::types::I80F48;
pub fn perp_force_close_position(ctx: Context<PerpForceClosePosition>) -> Result<()> {
let mut perp_market = ctx.accounts.perp_market.load_mut()?;
let perp_market_index = perp_market.perp_market_index;
let mut account_a = ctx.accounts.account_a.load_full_mut()?;
let mut account_b = ctx.accounts.account_b.load_full_mut()?;
let account_a_perp_position = account_a.perp_position_mut(perp_market_index)?;
let account_b_perp_position = account_b.perp_position_mut(perp_market_index)?;
require_gt!(
account_a_perp_position.base_position_lots(),
0,
MangoError::SomeError
);
require_gt!(
0,
account_b_perp_position.base_position_lots(),
MangoError::SomeError
);
let base_transfer = account_a_perp_position
.base_position_lots()
.min(account_b_perp_position.base_position_lots().abs())
.max(0);
let now_slot = Clock::get()?.slot;
let oracle_price = perp_market.oracle_price(
&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?,
Some(now_slot),
);
let quote_transfer = I80F48::from(base_transfer * perp_market.base_lot_size) * oracle_price;
account_a_perp_position.record_trade(&mut perp_market, -base_transfer, quote_transfer);
account_b_perp_position.record_trade(&mut perp_market, base_transfer, -quote_transfer);
emit_perp_balances(
ctx.accounts.group.key(),
ctx.accounts.account_a.key(),
account_a_perp_position,
&perp_market,
);
emit_perp_balances(
ctx.accounts.group.key(),
ctx.accounts.account_b.key(),
&account_b_perp_position,
&perp_market,
);
// TODO force-close trade log
Ok(())
}

View File

@ -677,6 +677,7 @@ pub mod mango_v4 {
reset_stable_price: bool,
positive_pnl_liquidation_fee_opt: Option<f32>,
name_opt: Option<String>,
force_close_opt: Option<bool>,
) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::perp_edit_market(
@ -710,6 +711,7 @@ pub mod mango_v4 {
reset_stable_price,
positive_pnl_liquidation_fee_opt,
name_opt,
force_close_opt,
)?;
Ok(())
}
@ -909,6 +911,12 @@ pub mod mango_v4 {
Ok(())
}
pub fn perp_force_close_position(ctx: Context<PerpForceClosePosition>) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::perp_force_close_position(ctx)?;
Ok(())
}
pub fn perp_settle_fees(ctx: Context<PerpSettleFees>, max_settle_amount: u64) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::perp_settle_fees(ctx, max_settle_amount)?;

View File

@ -188,6 +188,7 @@ pub enum IxGate {
TokenWithdraw = 47,
AccountBuybackFeesWithMngo = 48,
TokenForceCloseBorrowsWithToken = 49,
PerpForceClosePosition = 50,
// NOTE: Adding new variants requires matching changes in ts and the ix_gate_set instruction.
}

View File

@ -157,8 +157,9 @@ pub struct PerpMarket {
/// If true, users may no longer increase their market exposure. Only actions
/// that reduce their position are still allowed.
pub reduce_only: u8,
pub force_close: u8,
pub padding4: [u8; 7],
pub padding4: [u8; 6],
/// Weights for full perp market health, if positive
pub maint_overall_asset_weight: I80F48,
@ -218,6 +219,10 @@ impl PerpMarket {
self.reduce_only == 1
}
pub fn is_force_close(&self) -> bool {
self.force_close == 1
}
pub fn elligible_for_group_insurance_fund(&self) -> bool {
self.group_insurance_fund == 1
}
@ -478,6 +483,7 @@ impl PerpMarket {
padding3: Default::default(),
settle_pnl_limit_window_size_ts: 24 * 60 * 60,
reduce_only: 0,
force_close: 0,
padding4: Default::default(),
maint_overall_asset_weight: I80F48::ONE,
init_overall_asset_weight: I80F48::ONE,

View File

@ -1,7 +1,7 @@
use super::*;
#[tokio::test]
async fn test_force_close() -> Result<(), TransportError> {
async fn test_force_close_token() -> Result<(), TransportError> {
let test_builder = TestContextBuilder::new();
let context = test_builder.start_default().await;
let solana = &context.solana.clone();
@ -228,3 +228,215 @@ async fn test_force_close() -> Result<(), TransportError> {
Ok(())
}
#[tokio::test]
async fn test_force_close_perp() -> Result<(), TransportError> {
let context = TestContext::new().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];
//
// SETUP: Create a group and an account
//
let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig {
admin,
payer,
mints: mints.to_vec(),
..GroupWithTokensConfig::default()
}
.create(solana)
.await;
let deposit_amount = 1000;
let account_0 = create_funded_account(
&solana,
group,
owner,
0,
&context.users[1],
mints,
deposit_amount,
0,
)
.await;
let account_1 = create_funded_account(
&solana,
group,
owner,
1,
&context.users[1],
mints,
deposit_amount,
0,
)
.await;
//
// TEST: Create a perp market
//
let mango_v4::accounts::PerpCreateMarket { perp_market, .. } = send_tx(
solana,
PerpCreateMarketInstruction {
group,
admin,
payer,
perp_market_index: 0,
quote_lot_size: 10,
base_lot_size: 100,
maint_base_asset_weight: 0.975,
init_base_asset_weight: 0.95,
maint_base_liab_weight: 1.025,
init_base_liab_weight: 1.05,
base_liquidation_fee: 0.012,
maker_fee: -0.0001,
taker_fee: 0.0002,
settle_pnl_limit_factor: -1.0,
settle_pnl_limit_window_size_ts: 24 * 60 * 60,
..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, &tokens[0]).await
},
)
.await
.unwrap();
let price_lots = {
let perp_market = solana.get_account::<PerpMarket>(perp_market).await;
perp_market.native_price_to_lot(I80F48::ONE)
};
//
// Place a bid, corresponding ask, and consume event
//
send_tx(
solana,
PerpPlaceOrderInstruction {
account: account_0,
perp_market,
owner,
side: Side::Bid,
price_lots,
max_base_lots: 1,
max_quote_lots: i64::MAX,
reduce_only: false,
client_order_id: 5,
},
)
.await
.unwrap();
check_prev_instruction_post_health(&solana, account_0).await;
send_tx(
solana,
PerpPlaceOrderInstruction {
account: account_1,
perp_market,
owner,
side: Side::Ask,
price_lots,
max_base_lots: 1,
max_quote_lots: i64::MAX,
reduce_only: false,
client_order_id: 6,
},
)
.await
.unwrap();
check_prev_instruction_post_health(&solana, account_1).await;
send_tx(
solana,
PerpConsumeEventsInstruction {
perp_market,
mango_accounts: vec![account_0, account_1],
},
)
.await
.unwrap();
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
assert_eq!(mango_account_0.perps[0].base_position_lots(), 1);
assert!(assert_equal(
mango_account_0.perps[0].quote_position_native(),
-99.99,
0.001
));
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
assert_eq!(mango_account_1.perps[0].base_position_lots(), -1);
assert!(assert_equal(
mango_account_1.perps[0].quote_position_native(),
99.98,
0.001
));
// Market needs to be in force close
assert!(send_tx(
solana,
PerpForceClosePositionInstruction {
account_a: account_0,
account_b: account_1,
perp_market: perp_market,
},
)
.await
.is_err());
//
// Set force close and force close position and verify that base position is 0
//
send_tx(
solana,
PerpMakeReduceOnly {
admin,
group,
perp_market: perp_market,
reduce_only: true,
force_close: true,
},
)
.await
.unwrap();
// account_a needs to be long, and account_b needs to be short
assert!(send_tx(
solana,
PerpForceClosePositionInstruction {
account_a: account_1,
account_b: account_0,
perp_market: perp_market,
},
)
.await
.is_err());
send_tx(
solana,
PerpForceClosePositionInstruction {
account_a: account_0,
account_b: account_1,
perp_market: perp_market,
},
)
.await
.unwrap();
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
assert_eq!(mango_account_0.perps[0].base_position_lots(), 0);
assert!(assert_equal(
mango_account_0.perps[0].quote_position_native(),
0.009,
0.001
));
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
assert_eq!(mango_account_1.perps[0].base_position_lots(), 0);
assert!(assert_equal(
mango_account_1.perps[0].quote_position_native(),
-0.0199,
0.001
));
Ok(())
}

View File

@ -364,6 +364,8 @@ async fn test_perp_reduce_only() -> Result<(), TransportError> {
group,
admin,
perp_market,
reduce_only: true,
force_close: false,
},
)
.await

View File

@ -3006,6 +3006,7 @@ fn perp_edit_instruction_default() -> mango_v4::instruction::PerpEditMarket {
reset_stable_price: false,
positive_pnl_liquidation_fee_opt: None,
name_opt: None,
force_close_opt: None,
}
}
@ -3092,6 +3093,8 @@ pub struct PerpMakeReduceOnly {
pub group: Pubkey,
pub admin: TestKeypair,
pub perp_market: Pubkey,
pub reduce_only: bool,
pub force_close: bool,
}
#[async_trait::async_trait(?Send)]
@ -3107,7 +3110,8 @@ impl ClientInstruction for PerpMakeReduceOnly {
let perp_market: PerpMarket = account_loader.load(&self.perp_market).await.unwrap();
let instruction = Self::Instruction {
reduce_only_opt: Some(true),
reduce_only_opt: Some(self.reduce_only),
force_close_opt: Some(self.force_close),
..perp_edit_instruction_default()
};
@ -3617,6 +3621,42 @@ impl ClientInstruction for PerpSettlePnlInstruction {
}
}
pub struct PerpForceClosePositionInstruction {
pub account_a: Pubkey,
pub account_b: Pubkey,
pub perp_market: Pubkey,
}
#[async_trait::async_trait(?Send)]
impl ClientInstruction for PerpForceClosePositionInstruction {
type Accounts = mango_v4::accounts::PerpForceClosePosition;
type Instruction = mango_v4::instruction::PerpForceClosePosition;
async fn to_instruction(
&self,
account_loader: impl ClientAccountLoader + 'async_trait,
) -> (Self::Accounts, instruction::Instruction) {
let program_id = mango_v4::id();
let instruction = Self::Instruction {};
let perp_market: PerpMarket = account_loader.load(&self.perp_market).await.unwrap();
let accounts = Self::Accounts {
group: perp_market.group,
perp_market: self.perp_market,
account_a: self.account_a,
account_b: self.account_b,
oracle: perp_market.oracle,
};
let instruction = make_instruction(program_id, &accounts, instruction);
(accounts, instruction)
}
fn signers(&self) -> Vec<TestKeypair> {
vec![]
}
}
pub struct PerpSettleFeesInstruction {
pub account: Pubkey,
pub perp_market: Pubkey,

View File

@ -1999,6 +1999,7 @@ export class MangoClient {
params.resetStablePrice ?? false,
params.positivePnlLiquidationFee,
params.name,
params.forceClose,
)
.accounts({
group: group.publicKey,

View File

@ -86,6 +86,7 @@ export interface PerpEditParams {
resetStablePrice: boolean | null;
positivePnlLiquidationFee: number | null;
name: string | null;
forceClose: boolean | null;
}
export const NullPerpEditParams: PerpEditParams = {
@ -118,6 +119,7 @@ export const NullPerpEditParams: PerpEditParams = {
resetStablePrice: null,
positivePnlLiquidationFee: null,
name: null,
forceClose: null,
};
// Use with TrueIxGateParams and buildIxGate
@ -175,6 +177,7 @@ export interface IxGateParams {
TokenWithdraw: boolean;
AccountBuybackFeesWithMngo: boolean;
TokenForceCloseBorrowsWithToken: boolean;
PerpForceClosePosition: boolean;
}
// Default with all ixs enabled, use with buildIxGate
@ -232,6 +235,7 @@ export const TrueIxGateParams: IxGateParams = {
TokenWithdraw: true,
AccountBuybackFeesWithMngo: true,
TokenForceCloseBorrowsWithToken: true,
PerpForceClosePosition: true,
};
// build ix gate e.g. buildIxGate(Builder(TrueIxGateParams).TokenDeposit(false).build()).toNumber(),
@ -299,6 +303,8 @@ export function buildIxGate(p: IxGateParams): BN {
toggleIx(ixGate, p, 'TokenWithdraw', 47);
toggleIx(ixGate, p, 'AccountBuybackFeesWithMngo', 48);
toggleIx(ixGate, p, 'TokenForceCloseBorrowsWithToken', 49);
toggleIx(ixGate, p, 'TokenForceCloseBorrowsWithToken', 49);
toggleIx(ixGate, p, 'PerpForceClosePosition', 49);
return ixGate;
}

View File

@ -3092,6 +3092,12 @@ export type MangoV4 = {
"type": {
"option": "string"
}
},
{
"name": "forceCloseOpt",
"type": {
"option": "bool"
}
}
]
},
@ -3635,6 +3641,37 @@ export type MangoV4 = {
],
"args": []
},
{
"name": "perpForceClosePosition",
"accounts": [
{
"name": "group",
"isMut": false,
"isSigner": false
},
{
"name": "perpMarket",
"isMut": true,
"isSigner": false
},
{
"name": "accountA",
"isMut": true,
"isSigner": false
},
{
"name": "accountB",
"isMut": true,
"isSigner": false
},
{
"name": "oracle",
"isMut": false,
"isSigner": false
}
],
"args": []
},
{
"name": "perpSettleFees",
"accounts": [
@ -5048,12 +5085,16 @@ export type MangoV4 = {
],
"type": "u8"
},
{
"name": "forceClose",
"type": "u8"
},
{
"name": "padding4",
"type": {
"array": [
"u8",
7
6
]
}
},
@ -7065,6 +7106,9 @@ export type MangoV4 = {
},
{
"name": "TokenForceCloseBorrowsWithToken"
},
{
"name": "PerpForceClosePosition"
}
]
}
@ -11896,6 +11940,12 @@ export const IDL: MangoV4 = {
"type": {
"option": "string"
}
},
{
"name": "forceCloseOpt",
"type": {
"option": "bool"
}
}
]
},
@ -12439,6 +12489,37 @@ export const IDL: MangoV4 = {
],
"args": []
},
{
"name": "perpForceClosePosition",
"accounts": [
{
"name": "group",
"isMut": false,
"isSigner": false
},
{
"name": "perpMarket",
"isMut": true,
"isSigner": false
},
{
"name": "accountA",
"isMut": true,
"isSigner": false
},
{
"name": "accountB",
"isMut": true,
"isSigner": false
},
{
"name": "oracle",
"isMut": false,
"isSigner": false
}
],
"args": []
},
{
"name": "perpSettleFees",
"accounts": [
@ -13852,12 +13933,16 @@ export const IDL: MangoV4 = {
],
"type": "u8"
},
{
"name": "forceClose",
"type": "u8"
},
{
"name": "padding4",
"type": {
"array": [
"u8",
7
6
]
}
},
@ -15869,6 +15954,9 @@ export const IDL: MangoV4 = {
},
{
"name": "TokenForceCloseBorrowsWithToken"
},
{
"name": "PerpForceClosePosition"
}
]
}