mango-v4/programs/mango-v4/src/instructions/serum3_place_order.rs

535 lines
18 KiB
Rust

use crate::accounts_zerocopy::AccountInfoRef;
use crate::error::*;
use crate::health::*;
use crate::state::*;
use crate::logs::{Serum3OpenOrdersBalanceLog, TokenBalanceLog};
use crate::serum3_cpi::{load_market_state, load_open_orders_ref};
use anchor_lang::prelude::*;
use anchor_spl::token::{Token, TokenAccount};
use checked_math as cm;
use fixed::types::I80F48;
use num_enum::IntoPrimitive;
use num_enum::TryFromPrimitive;
use serum_dex::instruction::NewOrderInstructionV3;
use serum_dex::state::OpenOrders;
/// For loan origination fees bookkeeping purposes
#[derive(Debug)]
pub struct OpenOrdersSlim {
native_coin_free: u64,
native_coin_total: u64,
native_pc_free: u64,
native_pc_total: u64,
referrer_rebates_accrued: u64,
}
impl OpenOrdersSlim {
pub fn from_oo(oo: &OpenOrders) -> Self {
Self {
native_coin_free: oo.native_coin_free,
native_coin_total: oo.native_coin_total,
native_pc_free: oo.native_pc_free,
native_pc_total: oo.native_pc_total,
referrer_rebates_accrued: oo.referrer_rebates_accrued,
}
}
}
pub trait OpenOrdersAmounts {
fn native_base_reserved(&self) -> u64;
fn native_quote_reserved(&self) -> u64;
fn native_base_free(&self) -> u64;
fn native_quote_free(&self) -> u64;
fn native_quote_free_plus_rebates(&self) -> u64;
fn native_base_total(&self) -> u64;
fn native_quote_total(&self) -> u64;
fn native_quote_total_plus_rebates(&self) -> u64;
fn native_rebates(&self) -> u64;
}
impl OpenOrdersAmounts for OpenOrdersSlim {
fn native_base_reserved(&self) -> u64 {
cm!(self.native_coin_total - self.native_coin_free)
}
fn native_quote_reserved(&self) -> u64 {
cm!(self.native_pc_total - self.native_pc_free)
}
fn native_base_free(&self) -> u64 {
self.native_coin_free
}
fn native_quote_free(&self) -> u64 {
self.native_pc_free
}
fn native_quote_free_plus_rebates(&self) -> u64 {
cm!(self.native_pc_free + self.referrer_rebates_accrued)
}
fn native_base_total(&self) -> u64 {
self.native_coin_total
}
fn native_quote_total(&self) -> u64 {
self.native_pc_total
}
fn native_quote_total_plus_rebates(&self) -> u64 {
cm!(self.native_pc_total + self.referrer_rebates_accrued)
}
fn native_rebates(&self) -> u64 {
self.referrer_rebates_accrued
}
}
impl OpenOrdersAmounts for OpenOrders {
fn native_base_reserved(&self) -> u64 {
cm!(self.native_coin_total - self.native_coin_free)
}
fn native_quote_reserved(&self) -> u64 {
cm!(self.native_pc_total - self.native_pc_free)
}
fn native_base_free(&self) -> u64 {
self.native_coin_free
}
fn native_quote_free(&self) -> u64 {
self.native_pc_free
}
fn native_quote_free_plus_rebates(&self) -> u64 {
cm!(self.native_pc_free + self.referrer_rebates_accrued)
}
fn native_base_total(&self) -> u64 {
self.native_coin_total
}
fn native_quote_total(&self) -> u64 {
self.native_pc_total
}
fn native_quote_total_plus_rebates(&self) -> u64 {
cm!(self.native_pc_total + self.referrer_rebates_accrued)
}
fn native_rebates(&self) -> u64 {
self.referrer_rebates_accrued
}
}
/// Copy paste a bunch of enums so that we could AnchorSerialize & AnchorDeserialize them
#[derive(Clone, Copy, TryFromPrimitive, IntoPrimitive, AnchorSerialize, AnchorDeserialize)]
#[repr(u8)]
pub enum Serum3SelfTradeBehavior {
DecrementTake = 0,
CancelProvide = 1,
AbortTransaction = 2,
}
#[derive(Clone, Copy, TryFromPrimitive, IntoPrimitive, AnchorSerialize, AnchorDeserialize)]
#[repr(u8)]
pub enum Serum3OrderType {
Limit = 0,
ImmediateOrCancel = 1,
PostOnly = 2,
}
#[derive(Clone, Copy, TryFromPrimitive, IntoPrimitive, AnchorSerialize, AnchorDeserialize)]
#[repr(u8)]
pub enum Serum3Side {
Bid = 0,
Ask = 1,
}
#[derive(Accounts)]
pub struct Serum3PlaceOrder<'info> {
pub group: AccountLoader<'info, Group>,
#[account(
mut,
has_one = group
// owner is checked at #1
)]
pub account: AccountLoader<'info, MangoAccountFixed>,
pub owner: Signer<'info>,
#[account(mut)]
/// CHECK: Validated inline by checking against the pubkey stored in the account at #2
pub open_orders: UncheckedAccount<'info>,
#[account(
has_one = group,
has_one = serum_program,
has_one = serum_market_external,
)]
pub serum_market: AccountLoader<'info, Serum3Market>,
/// CHECK: The pubkey is checked and then it's passed to the serum cpi
pub serum_program: UncheckedAccount<'info>,
#[account(mut)]
/// CHECK: The pubkey is checked and then it's passed to the serum cpi
pub serum_market_external: UncheckedAccount<'info>,
// These accounts are forwarded directly to the serum cpi call
// and are validated there.
#[account(mut)]
/// CHECK: Validated by the serum cpi call
pub market_bids: UncheckedAccount<'info>,
#[account(mut)]
/// CHECK: Validated by the serum cpi call
pub market_asks: UncheckedAccount<'info>,
#[account(mut)]
/// CHECK: Validated by the serum cpi call
pub market_event_queue: UncheckedAccount<'info>,
#[account(mut)]
/// CHECK: Validated by the serum cpi call
pub market_request_queue: UncheckedAccount<'info>,
#[account(mut)]
/// CHECK: Validated by the serum cpi call
pub market_base_vault: UncheckedAccount<'info>,
#[account(mut)]
/// CHECK: Validated by the serum cpi call
pub market_quote_vault: UncheckedAccount<'info>,
/// needed for the automatic settle_funds call
/// CHECK: Validated by the serum cpi call
pub market_vault_signer: UncheckedAccount<'info>,
/// The bank that pays for the order, if necessary
// token_index and payer_bank.vault == payer_vault is validated inline at #3
#[account(mut, has_one = group)]
pub payer_bank: AccountLoader<'info, Bank>,
/// The bank vault that pays for the order, if necessary
#[account(mut)]
pub payer_vault: Box<Account<'info, TokenAccount>>,
/// CHECK: The oracle can be one of several different account types
#[account(address = payer_bank.load()?.oracle)]
pub payer_oracle: UncheckedAccount<'info>,
pub token_program: Program<'info, Token>,
}
#[allow(clippy::too_many_arguments)]
pub fn serum3_place_order(
ctx: Context<Serum3PlaceOrder>,
side: Serum3Side,
limit_price: u64,
max_base_qty: u64,
max_native_quote_qty_including_fees: u64,
self_trade_behavior: Serum3SelfTradeBehavior,
order_type: Serum3OrderType,
client_order_id: u64,
limit: u16,
) -> Result<()> {
let serum_market = ctx.accounts.serum_market.load()?;
require!(
!serum_market.is_reduce_only(),
MangoError::MarketInReduceOnlyMode
);
//
// Validation
//
{
let account = ctx.accounts.account.load_full()?;
// account constraint #1
require!(
account.fixed.is_owner_or_delegate(ctx.accounts.owner.key()),
MangoError::SomeError
);
// Validate open_orders #2
require!(
account
.serum3_orders(serum_market.market_index)?
.open_orders
== ctx.accounts.open_orders.key(),
MangoError::SomeError
);
// Validate bank and vault #3
let payer_bank = ctx.accounts.payer_bank.load()?;
require_keys_eq!(payer_bank.vault, ctx.accounts.payer_vault.key());
let payer_token_index = match side {
Serum3Side::Bid => serum_market.quote_token_index,
Serum3Side::Ask => serum_market.base_token_index,
};
require_eq!(payer_bank.token_index, payer_token_index);
}
//
// Pre-health computation
//
let mut account = ctx.accounts.account.load_full_mut()?;
let pre_health_opt = if !account.fixed.is_in_health_region() {
let retriever =
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
let health_cache =
new_health_cache(&account.borrow(), &retriever).context("pre-withdraw init health")?;
let pre_health = account.check_health_pre(&health_cache)?;
Some((health_cache, pre_health))
} else {
None
};
//
// Before-order tracking
//
let before_vault = ctx.accounts.payer_vault.amount;
let before_oo = {
let oo_ai = &ctx.accounts.open_orders.as_ref();
let open_orders = load_open_orders_ref(oo_ai)?;
OpenOrdersSlim::from_oo(&open_orders)
};
// Provide a readable error message in case the vault doesn't have enough tokens
{
let base_lot_size = load_market_state(
&ctx.accounts.serum_market_external,
&ctx.accounts.serum_program.key(),
)?
.coin_lot_size;
let needed_amount = match side {
Serum3Side::Ask => {
cm!(max_base_qty * base_lot_size).saturating_sub(before_oo.native_base_free())
}
Serum3Side::Bid => {
max_native_quote_qty_including_fees.saturating_sub(before_oo.native_quote_free())
}
};
if before_vault < needed_amount {
return err!(MangoError::InsufficentBankVaultFunds).with_context(|| {
format!(
"bank vault does not have enough tokens, need {} but have {}",
needed_amount, before_vault
)
});
}
}
//
// Apply the order to serum
//
let order = serum_dex::instruction::NewOrderInstructionV3 {
side: u8::try_from(side).unwrap().try_into().unwrap(),
limit_price: limit_price.try_into().unwrap(),
max_coin_qty: max_base_qty.try_into().unwrap(),
max_native_pc_qty_including_fees: max_native_quote_qty_including_fees.try_into().unwrap(),
self_trade_behavior: u8::try_from(self_trade_behavior)
.unwrap()
.try_into()
.unwrap(),
order_type: u8::try_from(order_type).unwrap().try_into().unwrap(),
client_order_id,
limit,
max_ts: i64::MAX,
};
cpi_place_order(ctx.accounts, order)?;
let oo_difference = {
let oo_ai = &ctx.accounts.open_orders.as_ref();
let open_orders = load_open_orders_ref(oo_ai)?;
let after_oo = OpenOrdersSlim::from_oo(&open_orders);
emit!(Serum3OpenOrdersBalanceLog {
mango_group: ctx.accounts.group.key(),
mango_account: ctx.accounts.account.key(),
base_token_index: serum_market.base_token_index,
quote_token_index: serum_market.quote_token_index,
base_total: after_oo.native_coin_total,
base_free: after_oo.native_coin_free,
quote_total: after_oo.native_pc_total,
quote_free: after_oo.native_pc_free,
referrer_rebates_accrued: after_oo.referrer_rebates_accrued,
});
OODifference::new(&before_oo, &after_oo)
};
//
// After-order tracking
//
ctx.accounts.payer_vault.reload()?;
let after_vault = ctx.accounts.payer_vault.amount;
// Placing an order cannot increase vault balance
require_gte!(before_vault, after_vault);
let mut payer_bank = ctx.accounts.payer_bank.load_mut()?;
// Enforce min vault to deposits ratio
let withdrawn_from_vault = I80F48::from(cm!(before_vault - after_vault));
let position_native = account
.token_position_mut(payer_bank.token_index)?
.0
.native(&payer_bank);
if withdrawn_from_vault > position_native {
payer_bank.enforce_min_vault_to_deposits_ratio((*ctx.accounts.payer_vault).as_ref())?;
}
// Charge the difference in vault balance to the user's account
let vault_difference = {
let oracle_price =
payer_bank.oracle_price(&AccountInfoRef::borrow(&ctx.accounts.payer_oracle)?, None)?;
apply_vault_difference(
ctx.accounts.account.key(),
&mut account.borrow_mut(),
serum_market.market_index,
&mut payer_bank,
after_vault,
before_vault,
Some(oracle_price),
)?
};
//
// Health check
//
if let Some((mut health_cache, pre_health)) = pre_health_opt {
vault_difference.adjust_health_cache(&mut health_cache, &payer_bank)?;
oo_difference.adjust_health_cache(&mut health_cache, &serum_market)?;
account.check_health_post(&health_cache, pre_health)?;
}
// TODO: enforce min_vault_to_deposits_ratio
Ok(())
}
pub struct OODifference {
reserved_base_change: I80F48,
reserved_quote_change: I80F48,
free_base_change: I80F48,
free_quote_change: I80F48,
}
impl OODifference {
pub fn new(before_oo: &OpenOrdersSlim, after_oo: &OpenOrdersSlim) -> Self {
Self {
reserved_base_change: cm!(I80F48::from(after_oo.native_base_reserved())
- I80F48::from(before_oo.native_base_reserved())),
reserved_quote_change: cm!(I80F48::from(after_oo.native_quote_reserved())
- I80F48::from(before_oo.native_quote_reserved())),
free_base_change: cm!(I80F48::from(after_oo.native_base_free())
- I80F48::from(before_oo.native_base_free())),
free_quote_change: cm!(I80F48::from(after_oo.native_quote_free_plus_rebates())
- I80F48::from(before_oo.native_quote_free_plus_rebates())),
}
}
pub fn adjust_health_cache(
&self,
health_cache: &mut HealthCache,
market: &Serum3Market,
) -> Result<()> {
health_cache.adjust_serum3_reserved(
market.market_index,
market.base_token_index,
self.reserved_base_change,
self.free_base_change,
market.quote_token_index,
self.reserved_quote_change,
self.free_quote_change,
)
}
}
pub struct VaultDifference {
token_index: TokenIndex,
native_change: I80F48,
}
impl VaultDifference {
pub fn adjust_health_cache(&self, health_cache: &mut HealthCache, bank: &Bank) -> Result<()> {
assert_eq!(bank.token_index, self.token_index);
health_cache.adjust_token_balance(bank, self.native_change)?;
Ok(())
}
}
/// Called in settle_funds, place_order, liq_force_cancel to adjust token positions after
/// changing the vault balances
/// Also logs changes to token balances
pub fn apply_vault_difference(
account_pk: Pubkey,
account: &mut MangoAccountRefMut,
serum_market_index: Serum3MarketIndex,
bank: &mut Bank,
vault_after: u64,
vault_before: u64,
oracle_price: Option<I80F48>,
) -> Result<VaultDifference> {
let needed_change = cm!(I80F48::from(vault_after) - I80F48::from(vault_before));
let (position, _) = account.token_position_mut(bank.token_index)?;
let native_before = position.native(bank);
let now_ts = Clock::get()?.unix_timestamp.try_into().unwrap();
if needed_change >= 0 {
bank.deposit(position, needed_change, now_ts)?;
} else {
bank.withdraw_without_fee(
position,
-needed_change,
now_ts,
oracle_price.unwrap(), // required for withdraws
)?;
}
let native_after = position.native(bank);
let native_change = cm!(native_after - native_before);
let new_borrows = native_change
.max(native_after)
.min(I80F48::ZERO)
.abs()
.to_num::<u64>();
let indexed_position = position.indexed_position;
let market = account.serum3_orders_mut(serum_market_index).unwrap();
let borrows_without_fee = if bank.token_index == market.base_token_index {
&mut market.base_borrows_without_fee
} else if bank.token_index == market.quote_token_index {
&mut market.quote_borrows_without_fee
} else {
return Err(error_msg!(
"assert failed: apply_vault_difference called with bad token index"
));
};
// Only for place: Add to potential borrow amount
let old_value = *borrows_without_fee;
*borrows_without_fee = cm!(old_value + new_borrows);
// Only for settle/liq_force_cancel: Reduce the potential borrow amounts
if needed_change > 0 {
*borrows_without_fee = (*borrows_without_fee).saturating_sub(needed_change.to_num::<u64>());
}
emit!(TokenBalanceLog {
mango_group: bank.group,
mango_account: account_pk,
token_index: bank.token_index,
indexed_position: indexed_position.to_bits(),
deposit_index: bank.deposit_index.to_bits(),
borrow_index: bank.borrow_index.to_bits(),
});
Ok(VaultDifference {
token_index: bank.token_index,
native_change,
})
}
fn cpi_place_order(ctx: &Serum3PlaceOrder, order: NewOrderInstructionV3) -> Result<()> {
use crate::serum3_cpi;
let group = ctx.group.load()?;
serum3_cpi::PlaceOrder {
program: ctx.serum_program.to_account_info(),
market: ctx.serum_market_external.to_account_info(),
request_queue: ctx.market_request_queue.to_account_info(),
event_queue: ctx.market_event_queue.to_account_info(),
bids: ctx.market_bids.to_account_info(),
asks: ctx.market_asks.to_account_info(),
base_vault: ctx.market_base_vault.to_account_info(),
quote_vault: ctx.market_quote_vault.to_account_info(),
token_program: ctx.token_program.to_account_info(),
open_orders: ctx.open_orders.to_account_info(),
order_payer_token_account: ctx.payer_vault.to_account_info(),
user_authority: ctx.group.to_account_info(),
}
.call(&group, order)
}