split it into ForceCancelOrders and PartialLiquidate; PL still not complete

This commit is contained in:
dd 2021-04-04 15:37:20 -04:00
parent 0d910cb0ee
commit a52f7a1ca8
3 changed files with 276 additions and 113 deletions

View File

@ -1,20 +1,27 @@
use bytemuck::Contiguous;
use num_enum::IntoPrimitive;
use serum_dex::error::DexError;
use solana_program::program_error::ProgramError;
use thiserror::Error;
pub type MangoResult<T = ()> = Result<T, MangoError>;
#[repr(u8)]
#[derive(Error, Debug, Clone, Eq, PartialEq, Copy)]
#[derive(Debug, Clone, Eq, PartialEq, Copy)]
pub enum SourceFileId {
#[error("src/processor.rs")]
Processor = 0,
#[error("src/state.rs")]
State = 1,
}
impl std::fmt::Display for SourceFileId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SourceFileId::Processor => write!(f, "src/processor.rs"),
SourceFileId::State => write!(f, "src/state.rs")
}
}
}
#[derive(Error, Debug, PartialEq, Eq)]
pub enum MangoError {
#[error(transparent)]
@ -54,6 +61,8 @@ pub enum MangoErrorCode {
SignerNecessary,
#[error("MangoErrorCode::InvalidMangoVault")]
InvalidMangoVault,
#[error("MangoErrorCode::BeingLiquidated The margin account has restricted functionality while being liquidated")]
BeingLiquidated,
#[error("MangoErrorCode::Default Check the source code for more info")]
Default = u32::MAX_VALUE,
@ -72,8 +81,8 @@ impl From<MangoError> for ProgramError {
}
}
impl From<DexError> for MangoError {
fn from(de: DexError) -> Self {
impl From<serum_dex::error::DexError> for MangoError {
fn from(de: serum_dex::error::DexError) -> Self {
let pe: ProgramError = de.into();
pe.into()
}

View File

@ -301,6 +301,10 @@ pub enum MangoInstruction {
PlaceAndSettle {
order: serum_dex::instruction::NewOrderInstructionV3
},
/// Allow a liquidator to cancel open orders to recoup funds for liquidation incentive
///
/// Accounts expected by this instruction (16 + 2 * NUM_MARKETS):
ForceCancelOrders,
/// Take over a MarginAccount that is below init_coll_ratio by depositing funds
///
@ -465,6 +469,9 @@ impl MangoInstruction {
}
}
15 => {
MangoInstruction::ForceCancelOrders
}
16 => {
let max_deposit = array_ref![data, 0, 8];
MangoInstruction::PartialLiquidate {
max_deposit: u64::from_le_bytes(*max_deposit)

View File

@ -19,7 +19,7 @@ use solana_program::rent::Rent;
use solana_program::sysvar::Sysvar;
use spl_token::state::{Account, Mint};
use crate::error::{check_assert, MangoErrorCode, MangoResult, SourceFileId};
use crate::error::{check_assert, MangoErrorCode, MangoResult, SourceFileId, MangoError};
use crate::instruction::MangoInstruction;
use crate::state::{AccountFlag, check_open_orders, load_market_state, load_open_orders, Loadable, MangoGroup, MangoIndex, MangoSrmAccount, MarginAccount, NUM_MARKETS, NUM_TOKENS, ONE_U64F64, ZERO_U64F64, PARTIAL_LIQ_INCENTIVE};
use crate::utils::{gen_signer_key, gen_signer_seeds};
@ -49,6 +49,11 @@ macro_rules! check_eq {
}
}
macro_rules! throw_err {
($err:expr) => {
Err(MangoError::MangoErrorCode { mango_error_code: $err, line: line!(), source_file_id: SourceFileId::Processor })
}
}
mod srm_token {
use solana_program::declare_id;
@ -729,6 +734,13 @@ impl Processor {
let prices = get_prices(&mango_group, oracle_accs)?;
let coll_ratio = margin_account.get_collateral_ratio(&mango_group, &prices, open_orders_accs)?;
if margin_account.being_liquidated {
if coll_ratio >= mango_group.init_coll_ratio {
margin_account.being_liquidated = false;
} else {
throw_err!(MangoErrorCode::BeingLiquidated)?;
}
}
let reduce_only = coll_ratio < mango_group.init_coll_ratio;
check_default!(owner_acc.is_signer)?;
@ -1022,6 +1034,15 @@ impl Processor {
let prices = get_prices(&mango_group, oracle_accs)?;
let coll_ratio = margin_account.get_collateral_ratio(&mango_group, &prices, open_orders_accs)?;
if margin_account.being_liquidated {
if coll_ratio >= mango_group.init_coll_ratio {
margin_account.being_liquidated = false;
} else {
throw_err!(MangoErrorCode::BeingLiquidated)?;
}
}
let reduce_only = coll_ratio < mango_group.init_coll_ratio;
check_default!(owner_acc.is_signer)?;
@ -1169,29 +1190,27 @@ impl Processor {
Ok(())
}
/// Liquidator is allowed to cancel orders of an account that is being_liquidated
/// This will also settle funds
#[inline(never)]
fn partial_liquidate(
fn force_cancel_orders(
program_id: &Pubkey,
accounts: &[AccountInfo],
max_deposit: u64
) -> MangoResult<()> {
const NUM_FIXED: usize = 15;
// TODO make it so canceling orders feature is optional if no orders outstanding to cancel
let accounts = array_ref![accounts, 0, NUM_FIXED + 2 * NUM_MARKETS + NUM_TOKENS];
const NUM_FIXED: usize = 16;
let accounts = array_ref![accounts, 0, NUM_FIXED + 2 * NUM_MARKETS];
let (
fixed_accs,
open_orders_accs,
oracle_accs,
vault_accs,
) = array_refs![accounts, NUM_FIXED, NUM_MARKETS, NUM_MARKETS, NUM_TOKENS];
) = array_refs![accounts, NUM_FIXED, NUM_MARKETS, NUM_MARKETS];
let [
mango_group_acc,
liqor_acc,
liqor_in_token_acc,
liqor_out_token_acc,
liqee_margin_account_acc,
base_vault_acc,
quote_vault_acc,
spot_market_acc,
bids_acc,
asks_acc,
@ -1202,6 +1221,7 @@ impl Processor {
dex_signer_acc,
token_prog_acc,
dex_prog_acc,
clock_acc
] = fixed_accs;
check_eq!(token_prog_acc.key, &spl_token::id(), MangoErrorCode::InvalidProgramId)?;
@ -1212,9 +1232,9 @@ impl Processor {
check_eq!(dex_prog_acc.key, &mango_group.dex_program_id, MangoErrorCode::InvalidProgramId)?;
check_eq!(signer_acc.key, &mango_group.signer_key, MangoErrorCode::InvalidSignerKey)?;
for i in 0..NUM_TOKENS {
check_eq!(&mango_group.vaults[i], vault_accs[i].key, MangoErrorCode::InvalidMangoVault)?;
}
let market_i = mango_group.get_market_index(spot_market_acc.key).unwrap();
check_eq!(&mango_group.vaults[market_i], base_vault_acc.key, MangoErrorCode::InvalidMangoVault)?;
check_eq!(&mango_group.vaults[NUM_MARKETS], quote_vault_acc.key, MangoErrorCode::InvalidMangoVault)?;
let mut liqee_margin_account = MarginAccount::load_mut_checked(
program_id, liqee_margin_account_acc, mango_group_acc.key
@ -1226,6 +1246,8 @@ impl Processor {
check_open_orders(&open_orders_accs[i], &mango_group.signer_key)?;
}
let clock = Clock::from_account_info(clock_acc)?;
mango_group.update_indexes(&clock)?;
let prices = get_prices(&mango_group, oracle_accs)?;
let coll_ratio = liqee_margin_account.get_collateral_ratio(
&mango_group, &prices, open_orders_accs
@ -1241,25 +1263,118 @@ impl Processor {
} else {
check!(coll_ratio < mango_group.maint_coll_ratio, MangoErrorCode::NotLiquidatable)?;
}
// Force cancel some orders to recoup funds for liquidator to withdraw
let market_index = mango_group.get_market_index(spot_market_acc.key).unwrap();
let open_orders_acc = &open_orders_accs[market_index];
let open_orders_acc = &open_orders_accs[market_i];
let signers_seeds = gen_signer_seeds(&mango_group.signer_nonce, mango_group_acc.key);
cancel_all(open_orders_acc, dex_prog_acc, spot_market_acc, bids_acc, asks_acc, signer_acc,
dex_event_queue_acc, dex_base_acc, dex_quote_acc,
&vault_accs[market_index], &vault_accs[NUM_MARKETS],
dex_signer_acc, token_prog_acc, &[&signers_seeds], 5)?;
invoke_cancel_orders(open_orders_acc, dex_prog_acc, spot_market_acc, bids_acc, asks_acc, signer_acc,
dex_event_queue_acc, &[&signers_seeds], 5)?;
let (pre_base, pre_quote) = {
let open_orders = load_open_orders(open_orders_acc)?;
(open_orders.native_coin_free, open_orders.native_pc_free)
};
if pre_base == 0 && pre_quote == 0 {
return Ok(());
}
invoke_settle_funds(dex_prog_acc, spot_market_acc, open_orders_acc, signer_acc, dex_base_acc,
dex_quote_acc, base_vault_acc, quote_vault_acc, dex_signer_acc,
token_prog_acc, &[&signers_seeds])?;
let (post_base, post_quote) = {
let open_orders = load_open_orders(open_orders_acc)?;
(open_orders.native_coin_free, open_orders.native_pc_free)
};
check_default!(post_base <= pre_base)?;
check_default!(post_quote <= pre_quote)?;
let base_change = U64F64::from_num(pre_base - post_base) / mango_group.indexes[market_i].deposit;
let quote_change = U64F64::from_num(pre_quote - post_quote) / mango_group.indexes[NUM_MARKETS].deposit;
checked_add_deposit(&mut mango_group, &mut liqee_margin_account, market_i, base_change)?;
checked_add_deposit(&mut mango_group, &mut liqee_margin_account, NUM_MARKETS, quote_change)?;
Ok(())
}
#[inline(never)]
fn partial_liquidate(
program_id: &Pubkey,
accounts: &[AccountInfo],
max_deposit: u64
) -> MangoResult<()> {
const NUM_FIXED: usize = 9;
// TODO make it so canceling orders feature is optional if no orders outstanding to cancel
let accounts = array_ref![accounts, 0, NUM_FIXED + 2 * NUM_MARKETS];
let (
fixed_accs,
open_orders_accs,
oracle_accs,
) = array_refs![accounts, NUM_FIXED, NUM_MARKETS, NUM_MARKETS];
let [
mango_group_acc,
liqor_acc,
liqor_in_token_acc,
liqor_out_token_acc,
liqee_margin_account_acc,
in_vault_acc,
out_vault_acc,
signer_acc,
clock_acc,
] = fixed_accs;
check!(liqor_acc.is_signer, MangoErrorCode::SignerNecessary)?;
let mut mango_group = MangoGroup::load_mut_checked(
mango_group_acc, program_id
)?;
check_eq!(signer_acc.key, &mango_group.signer_key, MangoErrorCode::InvalidSignerKey)?;
let liqor_in_token_account = Account::unpack(&liqor_in_token_acc.try_borrow_data()?)?;
let in_token_index = mango_group.get_token_index(&liqor_in_token_account.mint).unwrap();
let liqor_out_token_account = Account::unpack(&liqor_out_token_acc.try_borrow_data()?)?;
let out_token_index = mango_group.get_token_index(&liqor_out_token_account.mint).unwrap();
check_default!(in_token_index != out_token_index)?;
check_eq!(&mango_group.vaults[in_token_index], in_vault_acc.key, MangoErrorCode::InvalidMangoVault)?;
check_eq!(&mango_group.vaults[out_token_index], out_vault_acc.key, MangoErrorCode::InvalidMangoVault)?;
let mut liqee_margin_account = MarginAccount::load_mut_checked(
program_id, liqee_margin_account_acc, mango_group_acc.key
)?;
for i in 0..NUM_MARKETS {
check_eq!(open_orders_accs[i].key, &liqee_margin_account.open_orders[i],
MangoErrorCode::InvalidOpenOrdersAccount)?;
check_open_orders(&open_orders_accs[i], &mango_group.signer_key)?;
}
let clock = Clock::from_account_info(clock_acc)?;
mango_group.update_indexes(&clock)?;
let prices = get_prices(&mango_group, oracle_accs)?;
let coll_ratio = liqee_margin_account.get_collateral_ratio(
&mango_group, &prices, open_orders_accs)?;
// Only allow liquidations on accounts already being liquidated and below init or accounts below maint
if liqee_margin_account.being_liquidated {
if coll_ratio >= mango_group.init_coll_ratio {
liqee_margin_account.being_liquidated = false;
return Ok(());
}
} else if coll_ratio >= mango_group.maint_coll_ratio {
throw_err!(MangoErrorCode::NotLiquidatable)?;
}
// Settle borrows to increase coll ratio if possible
for i in 0..NUM_TOKENS {
settle_borrow_full_unchecked(&mut mango_group, &mut liqee_margin_account, i)?;
}
// Check again to see if account still liquidatable
let coll_ratio = liqee_margin_account.get_collateral_ratio(
&mango_group, &prices, open_orders_accs
)?;
&mango_group, &prices, open_orders_accs)?;
if liqee_margin_account.being_liquidated {
if coll_ratio >= mango_group.init_coll_ratio {
// TODO make sure liquidator knows why tx was success but he didn't receive any funds
@ -1275,12 +1390,6 @@ impl Processor {
}
// Now try to liquidate
let liqor_in_token_account = Account::unpack(&liqor_in_token_acc.try_borrow_data()?)?;
let in_token_index = mango_group.get_token_index(&liqor_in_token_account.mint).unwrap();
let liqor_out_token_account = Account::unpack(&liqor_out_token_acc.try_borrow_data()?)?;
let out_token_index = mango_group.get_token_index(&liqor_out_token_account.mint).unwrap();
check!(in_token_index != out_token_index, MangoErrorCode::Default)?;
// Calculate maximum possible deposit amount
let deficit_val = liqee_margin_account.get_partial_liq_deficit(
&mango_group, &prices, open_orders_accs)?;
@ -1308,44 +1417,21 @@ impl Processor {
// Can only deposit as much there is borrows to offset in in_token
let in_quantity = std::cmp::min(std::cmp::min(max_in_token, native_borrow), max_deposit);
let deposit_instruction = spl_token::instruction::transfer(
&spl_token::id(),
liqor_in_token_acc.key,
vault_accs[in_token_index].key,
&liqor_acc.key, &[], in_quantity
)?;
let deposit_accs = [
liqor_in_token_acc.clone(),
vault_accs[in_token_index].clone(),
liqor_acc.clone(),
token_prog_acc.clone()
];
solana_program::program::invoke_signed(&deposit_instruction, &deposit_accs, &[])?;
// Pull funds from liqor
let signer_nonce = mango_group.signer_nonce;
let signers_seeds = gen_signer_seeds(&signer_nonce, mango_group_acc.key);
invoke_transfer(liqor_in_token_acc, in_vault_acc, liqor_acc,
&[&signers_seeds], in_quantity)?;
let deposit: U64F64 = U64F64::from_num(in_quantity) / mango_group.indexes[in_token_index].borrow;
checked_sub_borrow(&mut mango_group, &mut liqee_margin_account, in_token_index, deposit)?;
// Withdraw
// Withdraw incentive funds to liqor
let in_val: U64F64 = U64F64::from_num(in_quantity) * prices[in_token_index];
let out_val: U64F64 = in_val * PARTIAL_LIQ_INCENTIVE;
let out_quantity: u64 = (out_val / prices[out_token_index]).checked_floor().unwrap().to_num();
let withdraw_instruction = spl_token::instruction::transfer(
&spl_token::ID,
vault_accs[out_token_index].key,
liqor_out_token_acc.key,
signer_acc.key,
&[],
out_quantity
)?;
let withdraw_accs = [
vault_accs[out_token_index].clone(),
liqor_out_token_acc.clone(),
signer_acc.clone(),
token_prog_acc.clone()
];
let signer_seeds = gen_signer_seeds(&mango_group.signer_nonce, mango_group_acc.key);
solana_program::program::invoke_signed(&withdraw_instruction, &withdraw_accs, &[&signer_seeds])?;
invoke_transfer(out_vault_acc, liqor_out_token_acc, signer_acc,
&[&signers_seeds], out_quantity)?;
let withdraw = U64F64::from_num(out_quantity) / mango_group.indexes[out_token_index].deposit;
checked_sub_deposit(&mut mango_group, &mut liqee_margin_account, out_token_index, withdraw)?;
@ -1356,6 +1442,8 @@ impl Processor {
// set margin account to no longer being liquidated
liqee_margin_account.being_liquidated = false;
}
// TODO do I need to check total deposits and total borrows?
// TODO what to do in case account is fully empty but there are still borrows
Ok(())
}
@ -1459,6 +1547,10 @@ impl Processor {
msg!("Mango: PlaceAndSettle");
Self::place_and_settle(program_id, accounts, order)?;
}
MangoInstruction::ForceCancelOrders => {
msg!("Mango: ForceCancelOrders");
Self::force_cancel_orders(program_id, accounts)?;
}
MangoInstruction::PartialLiquidate {
max_deposit
} => {
@ -1693,7 +1785,7 @@ fn invoke_cancel_order<'a>(
solana_program::program::invoke_signed(&instruction, &account_infos, signers_seeds)
}
fn cancel_all<'a>(
fn invoke_cancel_orders<'a>(
open_orders_acc: &AccountInfo<'a>,
dex_prog_acc: &AccountInfo<'a>,
spot_market_acc: &AccountInfo<'a>,
@ -1701,38 +1793,43 @@ fn cancel_all<'a>(
asks_acc: &AccountInfo<'a>,
signer_acc: &AccountInfo<'a>,
dex_event_queue_acc: &AccountInfo<'a>,
dex_base_acc: &AccountInfo<'a>,
dex_quote_acc: &AccountInfo<'a>,
base_vault_acc: &AccountInfo<'a>,
quote_vault_acc: &AccountInfo<'a>,
dex_signer_acc: &AccountInfo<'a>,
token_prog_acc: &AccountInfo<'a>,
signers_seeds: &[&[&[u8]]],
mut limit: u32
) -> MangoResult<()> {
let open_orders = load_open_orders(open_orders_acc)?;
let mut free_slot_bits = open_orders.free_slot_bits;
limit = std::cmp::min(limit, free_slot_bits.count_zeros());
if limit == 0 {
return Ok(());
let mut cancels = vec![];
{
let open_orders = load_open_orders(open_orders_acc)?;
limit = std::cmp::min(limit, open_orders.free_slot_bits.count_zeros());
if limit == 0 {
return Ok(());
}
for j in 0..128 {
let slot_mask = 1u128 << j;
if open_orders.free_slot_bits & slot_mask != 0 { // means slot is free
continue;
}
let order_id = open_orders.orders[j];
let side = if open_orders.is_bid_bits & slot_mask != 0 {
serum_dex::matching::Side::Bid
} else {
serum_dex::matching::Side::Ask
};
let cancel_instruction = serum_dex::instruction::CancelOrderInstructionV2 { side, order_id };
cancels.push(cancel_instruction);
limit -= 1;
if limit == 0 {
break;
}
}
}
for j in 0..128 {
let slot_is_free = (free_slot_bits & 1u128) != 0;
free_slot_bits = free_slot_bits >> 1;
if slot_is_free {
continue
}
let oid = open_orders.orders[j];
let cancel_instruction = serum_dex::instruction::MarketInstruction::CancelOrderV2(
serum_dex::instruction::CancelOrderInstructionV2 {
side: open_orders.slot_side(j as u8).unwrap(),
order_id: oid
}
);
for cancel in cancels.iter() {
let cancel_instruction = serum_dex::instruction::MarketInstruction::CancelOrderV2(cancel.clone());
invoke_cancel_order(
dex_prog_acc,
spot_market_acc,
@ -1744,26 +1841,76 @@ fn cancel_all<'a>(
cancel_instruction.pack(),
signers_seeds
)?;
limit -= 1;
if limit == 0 {
break;
}
}
invoke_settle_funds(
dex_prog_acc,
spot_market_acc,
open_orders_acc,
signer_acc,
dex_base_acc,
dex_quote_acc,
base_vault_acc,
quote_vault_acc,
dex_signer_acc,
token_prog_acc,
signers_seeds
)?;
// let (free_slot_bits, is_bid_bits, oids) = {
// let open_orders = load_open_orders(open_orders_acc)?;
// (open_orders.free_slot_bits, open_orders.is_bid_bits, open_orders.orders)
// };
//
// limit = std::cmp::min(limit, free_slot_bits.count_zeros());
// if limit == 0 {
// return Ok(());
// }
//
// for j in 0..128 {
// let slot_mask = 1u128 << j;
// if free_slot_bits & slot_mask != 0 { // means slot is free
// continue
// }
//
// let order_id = oids[j];
// let side = if is_bid_bits & slot_mask != 0 {
// serum_dex::matching::Side::Bid
// } else {
// serum_dex::matching::Side::Ask
// };
// let cancel_instruction = serum_dex::instruction::MarketInstruction::CancelOrderV2(
// serum_dex::instruction::CancelOrderInstructionV2 { side, order_id }
// );
//
// invoke_cancel_order(
// dex_prog_acc,
// spot_market_acc,
// bids_acc,
// asks_acc,
// open_orders_acc,
// signer_acc,
// dex_event_queue_acc,
// cancel_instruction.pack(),
// signers_seeds
// )?;
//
// limit -= 1;
// if limit == 0 {
// break;
// }
// }
Ok(())
}
fn invoke_transfer<'a>(
source_acc: &AccountInfo<'a>,
dest_acc: &AccountInfo<'a>,
authority_acc: &AccountInfo<'a>,
signers_seeds: &[&[&[u8]]],
quantity: u64
) -> ProgramResult {
let transfer_instruction = spl_token::instruction::transfer(
&spl_token::ID,
source_acc.key,
dest_acc.key,
authority_acc.key,
&[],
quantity
)?;
let accs = [
source_acc.clone(),
dest_acc.clone(),
authority_acc.clone()
];
solana_program::program::invoke_signed(&transfer_instruction, &accs, signers_seeds)
}