Skip to content

Commit

Permalink
program: add force_cancel_orders (#298)
Browse files Browse the repository at this point in the history
* program: more leniency in allowing risk decreasing trades for perps

* remove unused param

* skip maker maintenance check if maker wasn't filled

* program: add force_cancel_orders for users with too much leverage and risk increasing trades

* update CHANGELOG.md
  • Loading branch information
crispheaney committed Dec 22, 2022
1 parent fa340c8 commit 2d7c147
Show file tree
Hide file tree
Showing 7 changed files with 472 additions and 0 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Features

- program: add force_cancel_orders to cancel risk-increasing orders for users with excessive leverage ([#298](https://github.com/drift-labs/protocol-v2/pull/298))

### Fixes

- program: fix calculate_availability_borrow_liquidity ([#301](https://github.com/drift-labs/protocol-v2/pull/301))
Expand Down
109 changes: 109 additions & 0 deletions programs/drift/src/controller/orders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2323,6 +2323,101 @@ pub fn trigger_order(
Ok(())
}

pub fn force_cancel_orders(
state: &State,
user: &AccountLoader<User>,
spot_market_map: &SpotMarketMap,
perp_market_map: &PerpMarketMap,
oracle_map: &mut OracleMap,
filler: &AccountLoader<User>,
clock: &Clock,
) -> DriftResult {
let now = clock.unix_timestamp;
let slot = clock.slot;

let filler_key = filler.key();
let user_key = user.key();
let user = &mut load_mut!(user)?;
let filler = &mut load_mut!(filler)?;

validate!(
!user.is_being_liquidated(),
ErrorCode::UserIsBeingLiquidated
)?;

validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?;

let meets_initial_margin_requirement =
meets_initial_margin_requirement(user, perp_market_map, spot_market_map, oracle_map)?;

validate!(
!meets_initial_margin_requirement,
ErrorCode::SufficientCollateral
)?;

let mut total_fee = 0_u64;

for order_index in 0..user.orders.len() {
if user.orders[order_index].status != OrderStatus::Open {
continue;
}

let market_index = user.orders[order_index].market_index;
let market_type = user.orders[order_index].market_type;

let fee = match market_type {
MarketType::Spot => {
let spot_market = spot_market_map.get_ref(&market_index)?;
let is_risk_decreasing = determine_if_user_spot_order_is_risk_decreasing(
user,
&spot_market,
order_index,
)?;
if is_risk_decreasing {
continue;
}

state.spot_fee_structure.flat_filler_fee
}
MarketType::Perp => {
let is_risk_decreasing =
determine_if_user_order_is_risk_decreasing(user, market_index, order_index)?;
if is_risk_decreasing {
continue;
}

state.perp_fee_structure.flat_filler_fee
}
};

total_fee = total_fee.safe_add(fee)?;

cancel_order(
order_index,
user,
&user_key,
perp_market_map,
spot_market_map,
oracle_map,
now,
slot,
OrderActionExplanation::InsufficientFreeCollateral,
Some(&filler_key),
fee,
false,
)?;
}

pay_keeper_flat_reward_for_spot(
user,
Some(filler),
spot_market_map.get_quote_spot_market_mut()?.deref_mut(),
total_fee,
)?;

Ok(())
}

pub fn can_reward_user_with_perp_pnl(user: &mut Option<&mut User>, market_index: u16) -> bool {
user.is_some()
&& user
Expand Down Expand Up @@ -3204,6 +3299,20 @@ fn fulfill_spot_order(
Ok((base_asset_amount, base_asset_amount != 0))
}

fn determine_if_user_spot_order_is_risk_decreasing(
user: &User,
spot_market: &SpotMarket,
order_index: usize,
) -> DriftResult<bool> {
let position_index = user.get_spot_position_index(spot_market.market_index)?;
let token_amount = user.spot_positions[position_index].get_token_amount(spot_market)?;
is_spot_order_risk_decreasing(
&user.orders[order_index],
&user.spot_positions[position_index].balance_type,
token_amount,
)
}

pub fn fulfill_spot_order_with_match(
base_market: &mut SpotMarket,
quote_market: &mut SpotMarket,
Expand Down
255 changes: 255 additions & 0 deletions programs/drift/src/controller/orders/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7675,3 +7675,258 @@ pub mod fill_spot_order {
assert_eq!(taker_after.orders[0], Order::default()); // order expired
}
}

pub mod force_cancel_orders {
use std::str::FromStr;

use anchor_lang::prelude::{AccountLoader, Clock};

use crate::controller::orders::force_cancel_orders;
use crate::controller::position::PositionDirection;
use crate::create_account_info;
use crate::create_anchor_account_info;
use crate::math::constants::{
AMM_RESERVE_PRECISION, BASE_PRECISION_I64, BASE_PRECISION_U64, LAMPORTS_PER_SOL_I64,
LAMPORTS_PER_SOL_U64, PEG_PRECISION, PRICE_PRECISION_U64, SPOT_BALANCE_PRECISION,
SPOT_BALANCE_PRECISION_U64, SPOT_CUMULATIVE_INTEREST_PRECISION, SPOT_WEIGHT_PRECISION,
};
use crate::state::oracle::HistoricalOracleData;
use crate::state::oracle::OracleSource;
use crate::state::perp_market::{PerpMarket, AMM};
use crate::state::perp_market_map::PerpMarketMap;
use crate::state::spot_market::{SpotBalanceType, SpotMarket};
use crate::state::spot_market_map::SpotMarketMap;
use crate::state::state::State;
use crate::state::user::{MarketType, OrderStatus, OrderType, SpotPosition, User, UserStats};
use crate::test_utils::*;
use crate::test_utils::{
create_account_info, get_positions, get_pyth_price, get_spot_positions,
};

use super::*;

#[test]
fn cancel_order_after_fulfill() {
let clock = Clock {
slot: 6,
epoch_start_timestamp: 0,
epoch: 0,
leader_schedule_epoch: 0,
unix_timestamp: 0,
};

let mut oracle_price = get_pyth_price(100, 6);
let oracle_price_key =
Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap();
let pyth_program = crate::ids::pyth_program::id();
create_account_info!(
oracle_price,
&oracle_price_key,
&pyth_program,
oracle_account_info
);
let mut oracle_map = OracleMap::load_one(&oracle_account_info, clock.slot, None).unwrap();

let mut market = PerpMarket {
amm: AMM {
base_asset_reserve: 100 * AMM_RESERVE_PRECISION,
quote_asset_reserve: 100 * AMM_RESERVE_PRECISION,
terminal_quote_asset_reserve: 100 * AMM_RESERVE_PRECISION,
// bid_base_asset_reserve: 101 * AMM_RESERVE_PRECISION,
// bid_quote_asset_reserve: 99 * AMM_RESERVE_PRECISION,
// ask_base_asset_reserve: 99 * AMM_RESERVE_PRECISION,
// ask_quote_asset_reserve: 101 * AMM_RESERVE_PRECISION,
sqrt_k: 100 * AMM_RESERVE_PRECISION,
peg_multiplier: 100 * PEG_PRECISION,
max_slippage_ratio: 100,
max_fill_reserve_fraction: 100,
order_step_size: 1000,
order_tick_size: 1,
oracle: oracle_price_key,
max_spread: 1000,
base_spread: 0,
long_spread: 0,
short_spread: 0,
historical_oracle_data: HistoricalOracleData {
last_oracle_price_twap: oracle_price.twap as i64,
last_oracle_price_twap_5min: oracle_price.twap as i64,
last_oracle_price: oracle_price.agg.price as i64,
..HistoricalOracleData::default()
},
..AMM::default()
},
margin_ratio_initial: 1000,
margin_ratio_maintenance: 500,
status: MarketStatus::Initialized,
..PerpMarket::default()
};
market.status = MarketStatus::Active;
market.amm.max_base_asset_reserve = u128::MAX;
market.amm.min_base_asset_reserve = 0;
let (new_ask_base_asset_reserve, new_ask_quote_asset_reserve) =
crate::math::amm_spread::calculate_spread_reserves(
&market.amm,
PositionDirection::Long,
)
.unwrap();
let (new_bid_base_asset_reserve, new_bid_quote_asset_reserve) =
crate::math::amm_spread::calculate_spread_reserves(
&market.amm,
PositionDirection::Short,
)
.unwrap();
market.amm.ask_base_asset_reserve = new_ask_base_asset_reserve;
market.amm.bid_base_asset_reserve = new_bid_base_asset_reserve;
market.amm.ask_quote_asset_reserve = new_ask_quote_asset_reserve;
market.amm.bid_quote_asset_reserve = new_bid_quote_asset_reserve;
create_anchor_account_info!(market, PerpMarket, market_account_info);
let market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap();

let mut usdc_spot_market = SpotMarket {
market_index: 0,
oracle_source: OracleSource::QuoteAsset,
deposit_balance: SPOT_BALANCE_PRECISION,
cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION,
cumulative_borrow_interest: SPOT_CUMULATIVE_INTEREST_PRECISION,
decimals: 6,
initial_asset_weight: SPOT_WEIGHT_PRECISION,
maintenance_asset_weight: SPOT_WEIGHT_PRECISION,
..SpotMarket::default()
};
create_anchor_account_info!(usdc_spot_market, SpotMarket, usdc_spot_market_account_info);

let mut sol_spot_market = SpotMarket {
market_index: 1,
deposit_balance: SPOT_BALANCE_PRECISION,
cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION,
cumulative_borrow_interest: SPOT_CUMULATIVE_INTEREST_PRECISION,
oracle: oracle_price_key,
..SpotMarket::default_base_market()
};
create_anchor_account_info!(sol_spot_market, SpotMarket, sol_spot_market_account_info);

let spot_market_map = SpotMarketMap::load_multiple(
vec![
&usdc_spot_market_account_info,
&sol_spot_market_account_info,
],
true,
)
.unwrap();

let mut orders = [Order::default(); 32];
orders[0] = Order {
market_index: 0,
order_id: 1,
status: OrderStatus::Open,
order_type: OrderType::Limit,
market_type: MarketType::Perp,
direction: PositionDirection::Long,
base_asset_amount: 100 * BASE_PRECISION_U64,
slot: 0,
price: 102 * PRICE_PRECISION_U64,
..Order::default()
};
orders[1] = Order {
market_index: 0,
order_id: 2,
status: OrderStatus::Open,
order_type: OrderType::Limit,
market_type: MarketType::Perp,
direction: PositionDirection::Short,
base_asset_amount: BASE_PRECISION_U64,
slot: 0,
price: 102 * PRICE_PRECISION_U64,
..Order::default()
};
orders[2] = Order {
market_index: 1,
order_id: 1,
status: OrderStatus::Open,
order_type: OrderType::Limit,
market_type: MarketType::Spot,
direction: PositionDirection::Long,
base_asset_amount: 100 * LAMPORTS_PER_SOL_U64,
slot: 0,
price: 102 * PRICE_PRECISION_U64,
..Order::default()
};
orders[3] = Order {
market_index: 1,
order_id: 1,
status: OrderStatus::Open,
order_type: OrderType::Limit,
market_type: MarketType::Spot,
direction: PositionDirection::Short,
base_asset_amount: LAMPORTS_PER_SOL_U64,
slot: 0,
price: 102 * PRICE_PRECISION_U64,
..Order::default()
};

let mut user = User {
authority: Pubkey::from_str("My11111111111111111111111111111111111111111").unwrap(), // different authority than filler
orders,
perp_positions: get_positions(PerpPosition {
market_index: 0,
base_asset_amount: BASE_PRECISION_I64,
open_orders: 2,
open_bids: 100 * BASE_PRECISION_I64,
open_asks: -BASE_PRECISION_I64,
..PerpPosition::default()
}),
spot_positions: get_spot_positions(SpotPosition {
market_index: 1,
balance_type: SpotBalanceType::Deposit,
scaled_balance: SPOT_BALANCE_PRECISION_U64,
open_orders: 2,
open_bids: 100 * LAMPORTS_PER_SOL_I64,
open_asks: -LAMPORTS_PER_SOL_I64,
..SpotPosition::default()
}),
..User::default()
};
create_anchor_account_info!(user, User, user_account_info);
let user_account_loader: AccountLoader<User> =
AccountLoader::try_from(&user_account_info).unwrap();

create_anchor_account_info!(UserStats::default(), UserStats, user_stats_account_info);
let _user_stats_account_loader: AccountLoader<UserStats> =
AccountLoader::try_from(&user_stats_account_info).unwrap();

let filler_key = Pubkey::from_str("My11111111111111111111111111111111111111111").unwrap();
create_anchor_account_info!(User::default(), &filler_key, User, user_account_info);
let filler_account_loader: AccountLoader<User> =
AccountLoader::try_from(&user_account_info).unwrap();

create_anchor_account_info!(UserStats::default(), UserStats, filler_stats_account_info);
let _filler_stats_account_loader: AccountLoader<UserStats> =
AccountLoader::try_from(&filler_stats_account_info).unwrap();

let state = State {
min_perp_auction_duration: 1,
default_market_order_time_in_force: 10,
..State::default()
};

force_cancel_orders(
&state,
&user_account_loader,
&spot_market_map,
&market_map,
&mut oracle_map,
&filler_account_loader,
&clock,
)
.unwrap();

let user = user_account_loader.load().unwrap();
assert_eq!(user.orders[0], Order::default());
assert_ne!(user.orders[1], Order::default());
assert_eq!(user.orders[2], Order::default());
assert_ne!(user.orders[3], Order::default());

assert_eq!(user.spot_positions[0].scaled_balance, 20000001);
assert_eq!(user.spot_positions[0].balance_type, SpotBalanceType::Borrow,);
}
}
Loading

0 comments on commit 2d7c147

Please sign in to comment.