Openbook token tracking and price bands (#805)

- track min bid, max ask
- track maximal token outflow from oo
- add serum3_place_order_v2 with mutable receiver bank
- placing openbook orders is restricted to a certain distance from the
  oracle

(cherry picked from commit 81501837a9)
This commit is contained in:
Christian Kamm 2023-12-05 15:39:24 +01:00
parent 99790a01e4
commit f533d65a58
21 changed files with 1269 additions and 213 deletions

View File

@ -2740,6 +2740,161 @@
}
]
},
{
"name": "serum3PlaceOrderV2",
"accounts": [
{
"name": "group",
"isMut": false,
"isSigner": false
},
{
"name": "account",
"isMut": true,
"isSigner": false,
"relations": [
"group"
]
},
{
"name": "owner",
"isMut": false,
"isSigner": true
},
{
"name": "openOrders",
"isMut": true,
"isSigner": false
},
{
"name": "serumMarket",
"isMut": false,
"isSigner": false,
"relations": [
"group",
"serum_program",
"serum_market_external"
]
},
{
"name": "serumProgram",
"isMut": false,
"isSigner": false
},
{
"name": "serumMarketExternal",
"isMut": true,
"isSigner": false
},
{
"name": "marketBids",
"isMut": true,
"isSigner": false
},
{
"name": "marketAsks",
"isMut": true,
"isSigner": false
},
{
"name": "marketEventQueue",
"isMut": true,
"isSigner": false
},
{
"name": "marketRequestQueue",
"isMut": true,
"isSigner": false
},
{
"name": "marketBaseVault",
"isMut": true,
"isSigner": false
},
{
"name": "marketQuoteVault",
"isMut": true,
"isSigner": false
},
{
"name": "marketVaultSigner",
"isMut": false,
"isSigner": false,
"docs": [
"needed for the automatic settle_funds call"
]
},
{
"name": "payerBank",
"isMut": true,
"isSigner": false,
"docs": [
"The bank that pays for the order, if necessary"
],
"relations": [
"group"
]
},
{
"name": "payerVault",
"isMut": true,
"isSigner": false,
"docs": [
"The bank vault that pays for the order, if necessary"
]
},
{
"name": "payerOracle",
"isMut": false,
"isSigner": false
},
{
"name": "tokenProgram",
"isMut": false,
"isSigner": false
}
],
"args": [
{
"name": "side",
"type": {
"defined": "Serum3Side"
}
},
{
"name": "limitPrice",
"type": "u64"
},
{
"name": "maxBaseQty",
"type": "u64"
},
{
"name": "maxNativeQuoteQtyIncludingFees",
"type": "u64"
},
{
"name": "selfTradeBehavior",
"type": {
"defined": "Serum3SelfTradeBehavior"
}
},
{
"name": "orderType",
"type": {
"defined": "Serum3OrderType"
}
},
{
"name": "clientOrderId",
"type": "u64"
},
{
"name": "limit",
"type": "u16"
}
]
},
{
"name": "serum3CancelOrder",
"accounts": [
@ -7108,7 +7263,13 @@
"type": "f64"
},
{
"name": "depositsInSerum",
"name": "potentialSerumTokens",
"docs": [
"Largest amount of tokens that might be added the the bank based on",
"serum open order execution.",
"",
"Can be negative with multiple banks, then it'd need to be balanced in the keeper."
],
"type": "i64"
},
{
@ -8680,26 +8841,41 @@
"type": "f64"
},
{
"name": "baseDepositsReserved",
"name": "potentialBaseTokens",
"docs": [
"Tracks the amount of deposits that flowed into the serum open orders account.",
"An overestimate of the amount of tokens that might flow out of the open orders account.",
"",
"The bank still considers these amounts user deposits (see deposits_in_serum)",
"and they need to be deducted from there when they flow back into the bank",
"as real tokens."
"The bank still considers these amounts user deposits (see Bank::potential_serum_tokens)",
"and that value needs to be updated in conjunction with these numbers.",
"",
"This estimation is based on the amount of tokens in the open orders account",
"(see update_bank_potential_tokens() in serum3_place_order and settle)"
],
"type": "u64"
},
{
"name": "quoteDepositsReserved",
"name": "potentialQuoteTokens",
"type": "u64"
},
{
"name": "lowestPlacedBidInv",
"docs": [
"Track lowest bid/highest ask, same way as for highest bid/lowest ask.",
"",
"0 is a special \"unset\" state."
],
"type": "f64"
},
{
"name": "highestPlacedAsk",
"type": "f64"
},
{
"name": "reserved",
"type": {
"array": [
"u8",
32
16
]
}
}
@ -10362,6 +10538,9 @@
},
{
"name": "TokenConditionalSwapCreateLinearAuction"
},
{
"name": "Serum3PlaceOrderV2"
}
]
}
@ -13283,6 +13462,11 @@
"code": 6059,
"name": "TokenConditionalSwapTypeNotStartable",
"msg": "token conditional swap type cannot be started"
},
{
"code": 6060,
"name": "HealthAccountBankNotWritable",
"msg": "a bank in the health account list should be writable but is not"
}
]
}

View File

@ -31,11 +31,10 @@ pub enum Serum3Side {
Ask = 1,
}
// Used for Serum3PlaceOrder v1 and v2
#[derive(Accounts)]
pub struct Serum3PlaceOrder<'info> {
#[account(
constraint = group.load()?.is_ix_enabled(IxGate::Serum3PlaceOrder) @ MangoError::IxIsDisabled,
)]
// ix gate check is done at #4
pub group: AccountLoader<'info, Group>,
#[account(

View File

@ -125,6 +125,10 @@ pub enum MangoError {
TokenConditionalSwapTooSmallForStartIncentive,
#[msg("token conditional swap type cannot be started")]
TokenConditionalSwapTypeNotStartable,
#[msg("a bank in the health account list should be writable but is not")]
HealthAccountBankNotWritable,
#[msg("the market does not allow limit orders too far from the current oracle value")]
Serum3PriceBandExceeded,
}
impl MangoError {

View File

@ -1462,7 +1462,7 @@ mod tests {
borrows: u64,
deposit_weight_scale_start_quote: u64,
borrow_weight_scale_start_quote: u64,
deposits_in_serum: i64,
potential_serum_tokens: u64,
}
#[derive(Default)]
@ -1518,7 +1518,7 @@ mod tests {
let bank = bank.data();
bank.indexed_deposits = I80F48::from(settings.deposits) / bank.deposit_index;
bank.indexed_borrows = I80F48::from(settings.borrows) / bank.borrow_index;
bank.deposits_in_serum = settings.deposits_in_serum;
bank.potential_serum_tokens = settings.potential_serum_tokens;
if settings.deposit_weight_scale_start_quote > 0 {
bank.deposit_weight_scale_start_quote =
settings.deposit_weight_scale_start_quote as f64;
@ -1834,7 +1834,7 @@ mod tests {
..Default::default()
},
TestHealth1Case {
// 17, deposits_in_serum counts for deposit weight scaling
// 17, potential_serum_tokens counts for deposit weight scaling
token1: 100,
token2: 100,
token3: 100,
@ -1847,13 +1847,13 @@ mod tests {
BankSettings {
deposits: 100,
deposit_weight_scale_start_quote: 100 * 5,
deposits_in_serum: 100,
potential_serum_tokens: 100,
..BankSettings::default()
},
BankSettings {
deposits: 600,
deposit_weight_scale_start_quote: 500 * 10,
deposits_in_serum: 100,
potential_serum_tokens: 100,
..BankSettings::default()
},
],

View File

@ -23,6 +23,7 @@ pub fn health_region_begin<'key, 'accounts, 'remaining, 'info>(
crate::instruction::Serum3CancelAllOrders::discriminator(),
crate::instruction::Serum3CancelOrder::discriminator(),
crate::instruction::Serum3PlaceOrder::discriminator(),
crate::instruction::Serum3PlaceOrderV2::discriminator(),
crate::instruction::Serum3SettleFunds::discriminator(),
crate::instruction::Serum3SettleFundsV2::discriminator(),
];

View File

@ -94,6 +94,7 @@ pub fn ix_gate_set(ctx: Context<IxGateSet>, ix_gate: u128) -> Result<()> {
ix_gate,
IxGate::TokenConditionalSwapCreateLinearAuction,
);
log_if_changed(&group, ix_gate, IxGate::Serum3PlaceOrderV2);
group.ix_gate = ix_gate;

View File

@ -7,6 +7,7 @@ pub fn serum3_edit_market(
reduce_only_opt: Option<bool>,
force_close_opt: Option<bool>,
name_opt: Option<String>,
oracle_price_band_opt: Option<f32>,
) -> Result<()> {
let mut serum3_market = ctx.accounts.market.load_mut()?;
@ -46,6 +47,16 @@ pub fn serum3_edit_market(
require_group_admin = true;
};
if let Some(oracle_price_band) = oracle_price_band_opt {
msg!(
"Oracle price band: old - {:?}, new - {:?}",
serum3_market.oracle_price_band,
oracle_price_band
);
serum3_market.oracle_price_band = oracle_price_band;
require_group_admin = true;
};
if require_group_admin {
require!(
group.admin == ctx.accounts.admin.key(),

View File

@ -1,4 +1,4 @@
use crate::accounts_zerocopy::AccountInfoRef;
use crate::accounts_zerocopy::*;
use crate::error::*;
use crate::health::*;
use crate::i80f48::ClampToInt;
@ -25,6 +25,7 @@ pub fn serum3_place_order(
order_type: Serum3OrderType,
client_order_id: u64,
limit: u16,
require_v2: bool,
) -> Result<()> {
// Also required by serum3's place order
require_gt!(limit_price_lots, 0);
@ -87,18 +88,52 @@ pub fn serum3_place_order(
};
// Check if the bank for the token whose balance is increased is in reduce-only mode
let receiver_bank_reduce_only = {
let receiver_bank_ai;
let receiver_bank_oracle;
let receiver_bank_reduce_only;
{
// The token position already exists, but we need the active_index.
let (_, _, active_index) = account.ensure_token_position(receiver_token_index)?;
let group_key = ctx.accounts.group.key();
let receiver_bank = retriever
.bank_and_oracle(&group_key, active_index, receiver_token_index)?
.0;
receiver_bank.are_deposits_reduce_only()
};
let (receiver_bank, oracle) =
retriever.bank_and_oracle(&group_key, active_index, receiver_token_index)?;
receiver_bank_oracle = oracle;
receiver_bank_reduce_only = receiver_bank.are_deposits_reduce_only();
// The fixed_order account retriever can't give us mut references, so use the above
// call to .bank_and_oracle() as validation and then copy out the matching AccountInfo.
receiver_bank_ai = ctx.remaining_accounts[active_index].clone();
// Double-check that we got the right account
let receiver_bank2 = receiver_bank_ai.load::<Bank>()?;
assert_eq!(receiver_bank2.group, group_key);
assert_eq!(receiver_bank2.token_index, receiver_token_index);
}
drop(retriever);
//
// Instruction version checking #4
//
let is_v2_instruction;
{
let group = ctx.accounts.group.load()?;
let v1_available = group.is_ix_enabled(IxGate::Serum3PlaceOrder);
let v2_available = group.is_ix_enabled(IxGate::Serum3PlaceOrderV2);
is_v2_instruction =
require_v2 || !v1_available || (receiver_bank_ai.is_writable && v2_available);
if is_v2_instruction {
require!(v2_available, MangoError::IxIsDisabled);
require_msg_typed!(
receiver_bank_ai.is_writable,
MangoError::HealthAccountBankNotWritable,
"the receiver bank (token index {}) in the health account list must be writable",
receiver_token_index
);
} else {
require!(v1_available, MangoError::IxIsDisabled);
}
}
//
// Before-order tracking
//
@ -185,33 +220,45 @@ pub fn serum3_place_order(
if !before_had_bids {
// The 0 state means uninitialized/no value
serum.highest_placed_bid_inv = 0.0;
serum.lowest_placed_bid_inv = 0.0;
}
if !before_had_asks {
serum.lowest_placed_ask = 0.0;
serum.highest_placed_ask = 0.0;
}
// in the normal quote per base units
let limit_price = limit_price_lots as f64 * quote_lot_size as f64 / base_lot_size as f64;
let new_order_on_book = after_oo_free_slots != before_oo_free_slots;
if new_order_on_book {
match side {
Serum3Side::Ask => {
// in the normal quote per base units
let limit_price =
limit_price_lots as f64 * quote_lot_size as f64 / base_lot_size as f64;
serum.lowest_placed_ask = if serum.lowest_placed_ask == 0.0 {
limit_price
} else {
serum.lowest_placed_ask.min(limit_price)
};
serum.highest_placed_ask = if serum.highest_placed_ask == 0.0 {
limit_price
} else {
serum.highest_placed_ask.max(limit_price)
}
}
Serum3Side::Bid => {
// in base per quote units, to avoid a division in health
let limit_price_inv =
base_lot_size as f64 / (limit_price_lots as f64 * quote_lot_size as f64);
let limit_price_inv = 1.0 / limit_price;
serum.highest_placed_bid_inv = if serum.highest_placed_bid_inv == 0.0 {
limit_price_inv
} else {
// the highest bid has the lowest _inv value
serum.highest_placed_bid_inv.min(limit_price_inv)
};
serum.lowest_placed_bid_inv = if serum.lowest_placed_bid_inv == 0.0 {
limit_price_inv
} else {
// lowest bid has max _inv value
serum.lowest_placed_bid_inv.max(limit_price_inv)
}
}
}
}
@ -237,6 +284,18 @@ pub fn serum3_place_order(
let mut payer_bank = ctx.accounts.payer_bank.load_mut()?;
// Update the tracking in banks
if is_v2_instruction {
let mut receiver_bank = receiver_bank_ai.load_mut::<Bank>()?;
let (base_bank, quote_bank) = match side {
Serum3Side::Bid => (&mut receiver_bank, &mut payer_bank),
Serum3Side::Ask => (&mut payer_bank, &mut receiver_bank),
};
update_bank_potential_tokens(serum, base_bank, quote_bank, &after_oo);
} else {
update_bank_potential_tokens_payer_only(serum, &mut payer_bank, &after_oo);
}
// Enforce min vault to deposits ratio
let withdrawn_from_vault = I80F48::from(before_vault - after_vault);
let position_native = account
@ -256,20 +315,65 @@ pub fn serum3_place_order(
)?
};
// Payer bank safety checks like reduce-only, net borrows, vault-to-deposits ratio
let payer_bank_oracle =
payer_bank.oracle_price(&AccountInfoRef::borrow(&ctx.accounts.payer_oracle)?, None)?;
if withdrawn_from_vault > position_native {
require_msg_typed!(
!payer_bank.are_borrows_reduce_only(),
MangoError::TokenInReduceOnlyMode,
"the payer tokens cannot be borrowed"
);
let oracle_price =
payer_bank.oracle_price(&AccountInfoRef::borrow(&ctx.accounts.payer_oracle)?, None)?;
payer_bank.enforce_min_vault_to_deposits_ratio((*ctx.accounts.payer_vault).as_ref())?;
payer_bank.check_net_borrows(oracle_price)?;
payer_bank.check_net_borrows(payer_bank_oracle)?;
}
vault_difference.adjust_health_cache_token_balance(&mut health_cache, &payer_bank)?;
// Limit order price bands: If the order ends up on the book, ensure
// - a bid isn't too far below oracle
// - an ask isn't too far above oracle
// because placing orders that are guaranteed to never be hit can be bothersome:
// For example placing a very large bid near zero would make the potential_base_tokens
// value go through the roof, reducing available init margin for other users.
let band_threshold = serum_market.oracle_price_band();
if new_order_on_book && band_threshold != f32::MAX {
let (base_oracle, quote_oracle) = match side {
Serum3Side::Bid => (&receiver_bank_oracle, &payer_bank_oracle),
Serum3Side::Ask => (&payer_bank_oracle, &receiver_bank_oracle),
};
let base_oracle_f64 = base_oracle.to_num::<f64>();
let quote_oracle_f64 = quote_oracle.to_num::<f64>();
// this has the same units as base_oracle: USD per BASE; limit_price is in QUOTE per BASE
let limit_price_in_dollar = limit_price * quote_oracle_f64;
let band_factor = 1.0 + band_threshold as f64;
match side {
Serum3Side::Bid => {
require_msg_typed!(
limit_price_in_dollar * band_factor >= base_oracle_f64,
MangoError::Serum3PriceBandExceeded,
"bid price {} must be larger than {} ({}% of oracle)",
limit_price,
base_oracle_f64 / (quote_oracle_f64 * band_factor),
(100.0 / band_factor) as u64,
);
}
Serum3Side::Ask => {
require_msg_typed!(
limit_price_in_dollar <= base_oracle_f64 * band_factor,
MangoError::Serum3PriceBandExceeded,
"ask price {} must be smaller than {} ({}% of oracle)",
limit_price,
base_oracle_f64 * band_factor / quote_oracle_f64,
(100.0 * band_factor) as u64,
);
}
}
}
// Health cache updates for the changed account state
let receiver_bank = receiver_bank_ai.load::<Bank>()?;
// update scaled weights for receiver bank
health_cache.adjust_token_balance(&receiver_bank, I80F48::ZERO)?;
vault_difference.adjust_health_cache_token_balance(&mut health_cache, &payer_bank)?;
let serum_account = account.serum3_orders(serum_market.market_index)?;
oo_difference.recompute_health_cache_serum3_state(
&mut health_cache,
@ -379,33 +483,22 @@ fn apply_vault_difference(
.min(I80F48::ZERO)
.abs()
.to_num::<u64>();
// amount of tokens transferred to serum3 reserved that were taken from deposits
let used_deposits = native_before
.min(-needed_change)
.max(I80F48::ZERO)
.to_num::<u64>();
let indexed_position = position.indexed_position;
let market = account.serum3_orders_mut(serum_market_index).unwrap();
let borrows_without_fee;
let deposits_reserved;
if bank.token_index == market.base_token_index {
borrows_without_fee = &mut market.base_borrows_without_fee;
deposits_reserved = &mut market.base_deposits_reserved;
} else if bank.token_index == market.quote_token_index {
borrows_without_fee = &mut market.quote_borrows_without_fee;
deposits_reserved = &mut market.quote_deposits_reserved;
} else {
return Err(error_msg!(
"assert failed: apply_vault_difference called with bad token index"
));
};
// Only for place: Add to potential borrow amount and reserved deposits
// Only for place: Add to potential borrow amount
*borrows_without_fee += new_borrows;
*deposits_reserved += used_deposits;
let used_deposits_signed: i64 = used_deposits.try_into().unwrap();
bank.deposits_in_serum += used_deposits_signed;
// Only for settle/liq_force_cancel: Reduce the potential borrow amounts
if needed_change > 0 {
@ -499,25 +592,10 @@ pub fn apply_settle_changes(
)?;
// Tokens were moved from open orders into banks again: also update the tracking
// for deposits_in_serum on the banks.
// for potential_serum_tokens on the banks.
{
let serum_orders = account.serum3_orders_mut(serum_market.market_index)?;
let after_base_reserved = after_oo.native_base_reserved();
if after_base_reserved < serum_orders.base_deposits_reserved {
let diff = serum_orders.base_deposits_reserved - after_base_reserved;
serum_orders.base_deposits_reserved = after_base_reserved;
let diff_signed: i64 = diff.try_into().unwrap();
base_bank.deposits_in_serum -= diff_signed;
}
let after_quote_reserved = after_oo.native_quote_reserved();
if after_quote_reserved < serum_orders.quote_deposits_reserved {
let diff = serum_orders.quote_deposits_reserved - after_quote_reserved;
serum_orders.quote_deposits_reserved = after_quote_reserved;
let diff_signed: i64 = diff.try_into().unwrap();
quote_bank.deposits_in_serum -= diff_signed;
}
update_bank_potential_tokens(serum_orders, base_bank, quote_bank, after_oo);
}
if let Some(health_cache) = health_cache {
@ -535,6 +613,58 @@ pub fn apply_settle_changes(
Ok(())
}
fn update_bank_potential_tokens_payer_only(
serum_orders: &mut Serum3Orders,
payer_bank: &mut Bank,
oo: &OpenOrdersSlim,
) {
// Do the tracking for the avaliable bank
if serum_orders.base_token_index == payer_bank.token_index {
let new_base = oo.native_base_total()
+ (oo.native_quote_reserved() as f64 * serum_orders.lowest_placed_bid_inv) as u64;
let old_base = serum_orders.potential_base_tokens;
payer_bank.update_potential_serum_tokens(old_base, new_base);
serum_orders.potential_base_tokens = new_base;
} else {
assert_eq!(serum_orders.quote_token_index, payer_bank.token_index);
let new_quote = oo.native_quote_total()
+ (oo.native_base_reserved() as f64 * serum_orders.highest_placed_ask) as u64;
let old_quote = serum_orders.potential_quote_tokens;
payer_bank.update_potential_serum_tokens(old_quote, new_quote);
serum_orders.potential_quote_tokens = new_quote;
}
}
fn update_bank_potential_tokens(
serum_orders: &mut Serum3Orders,
base_bank: &mut Bank,
quote_bank: &mut Bank,
oo: &OpenOrdersSlim,
) {
assert_eq!(serum_orders.base_token_index, base_bank.token_index);
assert_eq!(serum_orders.quote_token_index, quote_bank.token_index);
// Potential tokens are all tokens on the side, plus reserved on the other side
// converted at favorable price. This creates an overestimation of the potential
// base and quote tokens flowing out of this open orders account.
let new_base = oo.native_base_total()
+ (oo.native_quote_reserved() as f64 * serum_orders.lowest_placed_bid_inv) as u64;
let new_quote = oo.native_quote_total()
+ (oo.native_base_reserved() as f64 * serum_orders.highest_placed_ask) as u64;
let old_base = serum_orders.potential_base_tokens;
let old_quote = serum_orders.potential_quote_tokens;
base_bank.update_potential_serum_tokens(old_base, new_base);
quote_bank.update_potential_serum_tokens(old_quote, new_quote);
serum_orders.potential_base_tokens = new_base;
serum_orders.potential_quote_tokens = new_quote;
}
fn cpi_place_order(ctx: &Serum3PlaceOrder, order: NewOrderInstructionV3) -> Result<()> {
use crate::serum3_cpi;

View File

@ -12,6 +12,7 @@ pub fn serum3_register_market(
ctx: Context<Serum3RegisterMarket>,
market_index: Serum3MarketIndex,
name: String,
oracle_price_band: f32,
) -> Result<()> {
// TODO: must guard against accidentally using the same market_index twice!
@ -44,6 +45,7 @@ pub fn serum3_register_market(
market_index,
bump: *ctx.bumps.get("serum_market").ok_or(MangoError::SomeError)?,
padding2: Default::default(),
oracle_price_band,
registration_time: Clock::get()?.unix_timestamp.try_into().unwrap(),
reserved: [0; 128],
};

View File

@ -113,7 +113,7 @@ pub fn token_register(
flash_loan_swap_fee_rate: flash_loan_swap_fee_rate,
interest_target_utilization,
interest_curve_scaling: interest_curve_scaling.into(),
deposits_in_serum: 0,
potential_serum_tokens: 0,
maint_weight_shift_start: 0,
maint_weight_shift_end: 0,
maint_weight_shift_duration_inv: I80F48::ZERO,

View File

@ -96,7 +96,7 @@ pub fn token_register_trustless(
flash_loan_swap_fee_rate: 0.0,
interest_target_utilization: 0.5,
interest_curve_scaling: 4.0,
deposits_in_serum: 0,
potential_serum_tokens: 0,
maint_weight_shift_start: 0,
maint_weight_shift_end: 0,
maint_weight_shift_duration_inv: I80F48::ZERO,

View File

@ -560,9 +560,10 @@ pub mod mango_v4 {
ctx: Context<Serum3RegisterMarket>,
market_index: Serum3MarketIndex,
name: String,
oracle_price_band: f32,
) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::serum3_register_market(ctx, market_index, name)?;
instructions::serum3_register_market(ctx, market_index, name, oracle_price_band)?;
Ok(())
}
@ -571,9 +572,16 @@ pub mod mango_v4 {
reduce_only_opt: Option<bool>,
force_close_opt: Option<bool>,
name_opt: Option<String>,
oracle_price_band_opt: Option<f32>,
) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::serum3_edit_market(ctx, reduce_only_opt, force_close_opt, name_opt)?;
instructions::serum3_edit_market(
ctx,
reduce_only_opt,
force_close_opt,
name_opt,
oracle_price_band_opt,
)?;
Ok(())
}
@ -623,6 +631,35 @@ pub mod mango_v4 {
order_type,
client_order_id,
limit,
false,
)?;
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub fn serum3_place_order_v2(
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<()> {
#[cfg(feature = "enable-gpl")]
instructions::serum3_place_order(
ctx,
side,
limit_price,
max_base_qty,
max_native_quote_qty_including_fees,
self_trade_behavior,
order_type,
client_order_id,
limit,
true,
)?;
Ok(())
}

View File

@ -157,9 +157,9 @@ pub struct Bank {
/// Except when first migrating to having this field, then 0.0
pub interest_curve_scaling: f64,
// user deposits that were moved into serum open orders
// can be negative due to multibank, then it'd need to be balanced in the keeper
pub deposits_in_serum: i64,
/// Largest amount of tokens that might be added the the bank based on
/// serum open order execution.
pub potential_serum_tokens: u64,
pub maint_weight_shift_start: u64,
pub maint_weight_shift_end: u64,
@ -238,7 +238,7 @@ impl Bank {
flash_loan_approved_amount: 0,
flash_loan_token_account_initial: u64::MAX,
net_borrows_in_window: 0,
deposits_in_serum: 0,
potential_serum_tokens: 0,
bump,
bank_num,
@ -969,7 +969,8 @@ impl Bank {
if self.deposit_weight_scale_start_quote == f64::MAX {
return self.init_asset_weight;
}
let all_deposits = self.native_deposits().to_num::<f64>() + self.deposits_in_serum as f64;
let all_deposits =
self.native_deposits().to_num::<f64>() + self.potential_serum_tokens as f64;
let deposits_quote = all_deposits * price.to_num::<f64>();
if deposits_quote <= self.deposit_weight_scale_start_quote {
self.init_asset_weight
@ -998,6 +999,16 @@ impl Bank {
self.init_liab_weight * I80F48::from_num(scale)
}
}
/// Grows potential_serum_tokens if new > old, shrinks it otherwise
#[inline(always)]
pub fn update_potential_serum_tokens(&mut self, old: u64, new: u64) {
if new >= old {
self.potential_serum_tokens += new - old;
} else {
self.potential_serum_tokens = self.potential_serum_tokens.saturating_sub(old - new);
}
}
}
#[macro_export]

View File

@ -220,6 +220,7 @@ pub enum IxGate {
TokenConditionalSwapStart = 68,
TokenConditionalSwapCreatePremiumAuction = 69,
TokenConditionalSwapCreateLinearAuction = 70,
Serum3PlaceOrderV2 = 71,
// NOTE: Adding new variants requires matching changes in ts and the ix_gate_set instruction.
}

View File

@ -145,20 +145,28 @@ pub struct Serum3Orders {
pub highest_placed_bid_inv: f64,
pub lowest_placed_ask: f64,
/// Tracks the amount of deposits that flowed into the serum open orders account.
/// An overestimate of the amount of tokens that might flow out of the open orders account.
///
/// The bank still considers these amounts user deposits (see deposits_in_serum)
/// and they need to be deducted from there when they flow back into the bank
/// as real tokens.
pub base_deposits_reserved: u64,
pub quote_deposits_reserved: u64,
/// The bank still considers these amounts user deposits (see Bank::potential_serum_tokens)
/// and that value needs to be updated in conjunction with these numbers.
///
/// This estimation is based on the amount of tokens in the open orders account
/// (see update_bank_potential_tokens() in serum3_place_order and settle)
pub potential_base_tokens: u64,
pub potential_quote_tokens: u64,
/// Track lowest bid/highest ask, same way as for highest bid/lowest ask.
///
/// 0 is a special "unset" state.
pub lowest_placed_bid_inv: f64,
pub highest_placed_ask: f64,
#[derivative(Debug = "ignore")]
pub reserved: [u8; 32],
pub reserved: [u8; 16],
}
const_assert_eq!(
size_of::<Serum3Orders>(),
32 + 8 * 2 + 2 * 3 + 2 + 4 * 8 + 32
32 + 8 * 2 + 2 * 3 + 2 + 6 * 8 + 16
);
const_assert_eq!(size_of::<Serum3Orders>(), 120);
const_assert_eq!(size_of::<Serum3Orders>() % 8, 0);
@ -185,9 +193,11 @@ impl Default for Serum3Orders {
quote_borrows_without_fee: 0,
highest_placed_bid_inv: 0.0,
lowest_placed_ask: 0.0,
base_deposits_reserved: 0,
quote_deposits_reserved: 0,
reserved: [0; 32],
potential_base_tokens: 0,
potential_quote_tokens: 0,
lowest_placed_bid_inv: 0.0,
highest_placed_ask: 0.0,
reserved: [0; 16],
}
}
}

View File

@ -26,7 +26,13 @@ pub struct Serum3Market {
pub bump: u8,
pub padding2: [u8; 5],
pub padding2: [u8; 1],
/// Limit orders must be <= oracle * (1+band) and >= oracle / (1+band)
///
/// Zero value is the default due to migration and disables the limit,
/// same as f32::MAX.
pub oracle_price_band: f32,
pub registration_time: u64,
@ -34,7 +40,7 @@ pub struct Serum3Market {
}
const_assert_eq!(
size_of::<Serum3Market>(),
32 + 2 + 2 + 1 + 3 + 16 + 2 * 32 + 2 + 1 + 5 + 8 + 128
32 + 2 + 2 + 1 + 3 + 16 + 2 * 32 + 2 + 1 + 1 + 4 + 8 + 128
);
const_assert_eq!(size_of::<Serum3Market>(), 264);
const_assert_eq!(size_of::<Serum3Market>() % 8, 0);
@ -53,6 +59,14 @@ impl Serum3Market {
pub fn is_force_close(&self) -> bool {
self.force_close == 1
}
pub fn oracle_price_band(&self) -> f32 {
if self.oracle_price_band == 0.0 {
f32::MAX // default disabled
} else {
self.oracle_price_band
}
}
}
#[account(zero_copy)]

View File

@ -309,13 +309,13 @@ async fn test_serum_basics() -> Result<(), TransportError> {
//
// TEST: Place an order
//
let (order_id, _) = order_placer.bid_maker(1.0, 100).await.unwrap();
let (order_id, _) = order_placer.bid_maker(0.9, 100).await.unwrap();
check_prev_instruction_post_health(&solana, account).await;
let native0 = account_position(solana, account, base_token.bank).await;
let native1 = account_position(solana, account, quote_token.bank).await;
assert_eq!(native0, 1000);
assert_eq!(native1, 900);
assert_eq!(native1, 910);
let account_data = get_mango_account(solana, account).await;
assert_eq!(
@ -342,13 +342,13 @@ async fn test_serum_basics() -> Result<(), TransportError> {
let serum_orders = account_data.serum3_orders_by_raw_index(0).unwrap();
assert_eq!(serum_orders.base_borrows_without_fee, 0);
assert_eq!(serum_orders.quote_borrows_without_fee, 0);
assert_eq!(serum_orders.base_deposits_reserved, 0);
assert_eq!(serum_orders.quote_deposits_reserved, 100);
assert_eq!(serum_orders.potential_base_tokens, 100);
assert_eq!(serum_orders.potential_quote_tokens, 90);
let base_bank = solana.get_account::<Bank>(base_token.bank).await;
assert_eq!(base_bank.deposits_in_serum, 0);
assert_eq!(base_bank.potential_serum_tokens, 100);
let quote_bank = solana.get_account::<Bank>(quote_token.bank).await;
assert_eq!(quote_bank.deposits_in_serum, 100);
assert_eq!(quote_bank.potential_serum_tokens, 90);
assert!(order_id != 0);
@ -371,13 +371,13 @@ async fn test_serum_basics() -> Result<(), TransportError> {
let serum_orders = account_data.serum3_orders_by_raw_index(0).unwrap();
assert_eq!(serum_orders.base_borrows_without_fee, 0);
assert_eq!(serum_orders.quote_borrows_without_fee, 0);
assert_eq!(serum_orders.base_deposits_reserved, 0);
assert_eq!(serum_orders.quote_deposits_reserved, 0);
assert_eq!(serum_orders.potential_base_tokens, 0);
assert_eq!(serum_orders.potential_quote_tokens, 0);
let base_bank = solana.get_account::<Bank>(base_token.bank).await;
assert_eq!(base_bank.deposits_in_serum, 0);
assert_eq!(base_bank.potential_serum_tokens, 0);
let quote_bank = solana.get_account::<Bank>(quote_token.bank).await;
assert_eq!(quote_bank.deposits_in_serum, 0);
assert_eq!(quote_bank.potential_serum_tokens, 0);
// Process events such that the OutEvent deactivates the closed order on open_orders
context
@ -1150,89 +1150,72 @@ async fn test_serum_track_bid_ask() -> Result<(), TransportError> {
// TEST: highest bid/lowest ask updating
//
assert_eq!(
order_placer
.mango_serum_orders()
.await
.highest_placed_bid_inv,
0.0
);
assert_eq!(
order_placer.mango_serum_orders().await.lowest_placed_ask,
0.0
);
let srm = order_placer.mango_serum_orders().await;
assert_eq!(srm.highest_placed_bid_inv, 0.0);
assert_eq!(srm.lowest_placed_bid_inv, 0.0);
assert_eq!(srm.highest_placed_ask, 0.0);
assert_eq!(srm.lowest_placed_ask, 0.0);
order_placer.bid_maker(10.0, 100).await.unwrap();
assert_eq!(
order_placer
.mango_serum_orders()
.await
.highest_placed_bid_inv,
1.0 / 10.0
);
let srm = order_placer.mango_serum_orders().await;
assert_eq!(srm.highest_placed_bid_inv, 1.0 / 10.0);
assert_eq!(srm.lowest_placed_bid_inv, 1.0 / 10.0);
assert_eq!(srm.highest_placed_ask, 0.0);
assert_eq!(srm.lowest_placed_ask, 0.0);
order_placer.bid_maker(9.0, 100).await.unwrap();
assert_eq!(
order_placer
.mango_serum_orders()
.await
.highest_placed_bid_inv,
1.0 / 10.0
);
let srm = order_placer.mango_serum_orders().await;
assert_eq!(srm.highest_placed_bid_inv, 1.0 / 10.0);
assert_eq!(srm.lowest_placed_bid_inv, 1.0 / 9.0);
assert_eq!(srm.highest_placed_ask, 0.0);
assert_eq!(srm.lowest_placed_ask, 0.0);
order_placer.bid_maker(11.0, 100).await.unwrap();
assert_eq!(
order_placer
.mango_serum_orders()
.await
.highest_placed_bid_inv,
1.0 / 11.0
);
assert_eq!(
order_placer.mango_serum_orders().await.lowest_placed_ask,
0.0
);
let srm = order_placer.mango_serum_orders().await;
assert_eq!(srm.highest_placed_bid_inv, 1.0 / 11.0);
assert_eq!(srm.lowest_placed_bid_inv, 1.0 / 9.0);
assert_eq!(srm.highest_placed_ask, 0.0);
assert_eq!(srm.lowest_placed_ask, 0.0);
order_placer.ask(20.0, 100).await.unwrap();
assert_eq!(
order_placer.mango_serum_orders().await.lowest_placed_ask,
20.0
);
order_placer.ask(19.0, 100).await.unwrap();
assert_eq!(
order_placer.mango_serum_orders().await.lowest_placed_ask,
19.0
);
order_placer.ask(21.0, 100).await.unwrap();
assert_eq!(
order_placer.mango_serum_orders().await.lowest_placed_ask,
19.0
);
assert_eq!(
order_placer
.mango_serum_orders()
.await
.highest_placed_bid_inv,
1.0 / 11.0
);
let srm = order_placer.mango_serum_orders().await;
assert_eq!(srm.highest_placed_bid_inv, 1.0 / 11.0);
assert_eq!(srm.lowest_placed_bid_inv, 1.0 / 9.0);
assert_eq!(srm.highest_placed_ask, 20.0);
assert_eq!(srm.lowest_placed_ask, 20.0);
order_placer.ask(19.0, 100).await.unwrap();
let srm = order_placer.mango_serum_orders().await;
assert_eq!(srm.highest_placed_bid_inv, 1.0 / 11.0);
assert_eq!(srm.lowest_placed_bid_inv, 1.0 / 9.0);
assert_eq!(srm.highest_placed_ask, 20.0);
assert_eq!(srm.lowest_placed_ask, 19.0);
order_placer.ask(21.0, 100).await.unwrap();
let srm = order_placer.mango_serum_orders().await;
assert_eq!(srm.highest_placed_bid_inv, 1.0 / 11.0);
assert_eq!(srm.lowest_placed_bid_inv, 1.0 / 9.0);
assert_eq!(srm.highest_placed_ask, 21.0);
assert_eq!(srm.lowest_placed_ask, 19.0);
//
// TEST: cancellation allows for resets
//
order_placer.cancel_all().await;
assert_eq!(
order_placer.mango_serum_orders().await.lowest_placed_ask,
19.0
);
assert_eq!(
order_placer
.mango_serum_orders()
.await
.highest_placed_bid_inv,
1.0 / 11.0
);
// no immediate change
let srm = order_placer.mango_serum_orders().await;
assert_eq!(srm.highest_placed_bid_inv, 1.0 / 11.0);
assert_eq!(srm.lowest_placed_bid_inv, 1.0 / 9.0);
assert_eq!(srm.highest_placed_ask, 21.0);
assert_eq!(srm.lowest_placed_ask, 19.0);
// Process events such that the OutEvent deactivates the closed order on open_orders
context
@ -1242,36 +1225,36 @@ async fn test_serum_track_bid_ask() -> Result<(), TransportError> {
// takes new value for bid, resets ask
order_placer.bid_maker(1.0, 100).await.unwrap();
assert_eq!(
order_placer.mango_serum_orders().await.lowest_placed_ask,
0.0
);
assert_eq!(
order_placer
.mango_serum_orders()
.await
.highest_placed_bid_inv,
1.0
);
let srm = order_placer.mango_serum_orders().await;
assert_eq!(srm.highest_placed_bid_inv, 1.0);
assert_eq!(srm.lowest_placed_bid_inv, 1.0);
assert_eq!(srm.highest_placed_ask, 0.0);
assert_eq!(srm.lowest_placed_ask, 0.0);
//
// TEST: can reset even when there's still an order on the other side
//
let (oid, _) = order_placer.ask(10.0, 100).await.unwrap();
assert_eq!(
order_placer.mango_serum_orders().await.lowest_placed_ask,
10.0
);
let srm = order_placer.mango_serum_orders().await;
assert_eq!(srm.highest_placed_bid_inv, 1.0);
assert_eq!(srm.lowest_placed_bid_inv, 1.0);
assert_eq!(srm.highest_placed_ask, 10.0);
assert_eq!(srm.lowest_placed_ask, 10.0);
order_placer.cancel(oid).await;
context
.serum
.consume_spot_events(&serum_market_cookie, &[order_placer.open_orders])
.await;
order_placer.ask(9.0, 100).await.unwrap();
assert_eq!(
order_placer.mango_serum_orders().await.lowest_placed_ask,
9.0
);
let srm = order_placer.mango_serum_orders().await;
assert_eq!(srm.highest_placed_bid_inv, 1.0);
assert_eq!(srm.lowest_placed_bid_inv, 1.0);
assert_eq!(srm.highest_placed_ask, 9.0);
assert_eq!(srm.lowest_placed_ask, 9.0);
Ok(())
}
@ -1305,10 +1288,10 @@ async fn test_serum_track_reserved_deposits() -> Result<(), TransportError> {
let base_bank = solana.get_account::<Bank>(base_bank).await;
let quote_bank = solana.get_account::<Bank>(quote_bank).await;
(
orders.base_deposits_reserved,
base_bank.deposits_in_serum,
orders.quote_deposits_reserved,
quote_bank.deposits_in_serum,
orders.potential_base_tokens,
base_bank.potential_serum_tokens,
orders.potential_quote_tokens,
quote_bank.potential_serum_tokens,
)
};
@ -1317,9 +1300,14 @@ async fn test_serum_track_reserved_deposits() -> Result<(), TransportError> {
//
order_placer.bid_maker(0.8, 2000).await.unwrap();
order_placer.ask(1.2, 2000).await.unwrap();
assert_eq!(get_vals(solana).await, (2000, 2000, 1600, 1600));
order_placer.ask(1.2, 2000).await.unwrap();
assert_eq!(
get_vals(solana).await,
(2 * 2000, 2 * 2000, 1600 + 2400, 1600 + 2400)
);
//
// TEST: match partially on both sides, increasing the on-bank reserved amounts
// because order_placer2 puts funds into the serum oo
@ -1333,9 +1321,12 @@ async fn test_serum_track_reserved_deposits() -> Result<(), TransportError> {
&[order_placer.open_orders, order_placer2.open_orders],
)
.await;
assert_eq!(get_vals(solana).await, (2000, 2000, 1600, 2801));
// taker order directly converted to base, no change to quote
assert_eq!(get_vals(solana).await, (4000, 4000 + 1000, 4000, 4000));
// takes out 1000 base
order_placer2.settle_v2(false).await;
assert_eq!(get_vals(solana).await, (2000, 2000, 1600, 1600));
assert_eq!(get_vals(solana).await, (4000, 4000, 4000, 4000));
order_placer2.ask(0.8, 1000).await.unwrap();
context
@ -1345,16 +1336,19 @@ async fn test_serum_track_reserved_deposits() -> Result<(), TransportError> {
&[order_placer.open_orders, order_placer2.open_orders],
)
.await;
assert_eq!(get_vals(solana).await, (2000, 3000, 1600, 1600));
// taker order directly converted to quote
assert_eq!(get_vals(solana).await, (4000, 4000, 4000, 4000 + 799));
order_placer2.settle_v2(false).await;
assert_eq!(get_vals(solana).await, (2000, 2000, 1600, 1600));
assert_eq!(get_vals(solana).await, (4000, 4000, 4000, 4000));
//
// TEST: Settlement updates the values
//
order_placer.settle_v2(false).await;
assert_eq!(get_vals(solana).await, (1000, 1000, 800, 800));
// remaining is bid 1000 @ 0.8; ask 1000 @ 1.2
assert_eq!(get_vals(solana).await, (2000, 2000, 2000, 2000));
Ok(())
}
@ -1481,6 +1475,82 @@ async fn test_serum_compute() -> Result<(), TransportError> {
Ok(())
}
#[tokio::test]
async fn test_serum_bands() -> Result<(), TransportError> {
let mut test_builder = TestContextBuilder::new();
test_builder.test().set_compute_max_units(150_000); // Serum3PlaceOrder needs lots
let context = test_builder.start_default().await;
let solana = &context.solana.clone();
//
// SETUP: Create a group, accounts, market etc
//
let deposit_amount = 10000;
let CommonSetup {
group_with_tokens,
mut order_placer,
quote_token,
base_token,
..
} = common_setup(&context, deposit_amount).await;
//
// SETUP: Set oracle price for market to 100
//
set_bank_stub_oracle_price(
solana,
group_with_tokens.group,
&base_token,
group_with_tokens.admin,
200.0,
)
.await;
set_bank_stub_oracle_price(
solana,
group_with_tokens.group,
&quote_token,
group_with_tokens.admin,
2.0,
)
.await;
//
// TEST: can place way over/under oracle
//
order_placer.bid_maker(1.0, 100).await.unwrap();
order_placer.ask(200.0, 100).await.unwrap();
order_placer.cancel_all().await;
//
// TEST: Can't when bands are enabled
//
send_tx(
solana,
Serum3EditMarketInstruction {
group: group_with_tokens.group,
admin: group_with_tokens.admin,
market: order_placer.serum_market,
options: mango_v4::instruction::Serum3EditMarket {
oracle_price_band_opt: Some(0.5),
..serum3_edit_market_instruction_default()
},
},
)
.await
.unwrap();
let r = order_placer.try_bid(65.0, 100, false).await;
assert!(r.is_err());
let r = order_placer.try_ask(151.0, 100).await;
assert!(r.is_err());
order_placer.try_bid(67.0, 100, false).await.unwrap();
order_placer.try_ask(149.0, 100).await.unwrap();
Ok(())
}
struct CommonSetup {
group_with_tokens: GroupWithTokens,
serum_market_cookie: SpotMarketCookie,

View File

@ -2275,6 +2275,7 @@ impl ClientInstruction for Serum3RegisterMarketInstruction {
let instruction = Self::Instruction {
market_index: self.market_index,
name: "UUU/usdc".to_string(),
oracle_price_band: f32::MAX,
};
let serum_market = Pubkey::find_program_address(
@ -2319,6 +2320,46 @@ impl ClientInstruction for Serum3RegisterMarketInstruction {
}
}
pub fn serum3_edit_market_instruction_default() -> mango_v4::instruction::Serum3EditMarket {
mango_v4::instruction::Serum3EditMarket {
reduce_only_opt: None,
force_close_opt: None,
name_opt: None,
oracle_price_band_opt: None,
}
}
pub struct Serum3EditMarketInstruction {
pub group: Pubkey,
pub admin: TestKeypair,
pub market: Pubkey,
pub options: mango_v4::instruction::Serum3EditMarket,
}
#[async_trait::async_trait(?Send)]
impl ClientInstruction for Serum3EditMarketInstruction {
type Accounts = mango_v4::accounts::Serum3EditMarket;
type Instruction = mango_v4::instruction::Serum3EditMarket;
async fn to_instruction(
&self,
_account_loader: impl ClientAccountLoader + 'async_trait,
) -> (Self::Accounts, instruction::Instruction) {
let program_id = mango_v4::id();
let accounts = Self::Accounts {
group: self.group,
admin: self.admin.pubkey(),
market: self.market,
};
let instruction = make_instruction(program_id, &accounts, &self.options);
(accounts, instruction)
}
fn signers(&self) -> Vec<TestKeypair> {
vec![self.admin]
}
}
pub struct Serum3DeregisterMarketInstruction {
pub group: Pubkey,
pub admin: TestKeypair,
@ -2493,7 +2534,7 @@ pub struct Serum3PlaceOrderInstruction {
#[async_trait::async_trait(?Send)]
impl ClientInstruction for Serum3PlaceOrderInstruction {
type Accounts = mango_v4::accounts::Serum3PlaceOrder;
type Instruction = mango_v4::instruction::Serum3PlaceOrder;
type Instruction = mango_v4::instruction::Serum3PlaceOrderV2;
async fn to_instruction(
&self,
account_loader: impl ClientAccountLoader + 'async_trait,
@ -2547,7 +2588,7 @@ impl ClientInstruction for Serum3PlaceOrderInstruction {
)
.unwrap();
let health_check_metas = derive_health_check_remaining_account_metas(
let mut health_check_metas = derive_health_check_remaining_account_metas(
&account_loader,
&account,
None,
@ -2556,11 +2597,17 @@ impl ClientInstruction for Serum3PlaceOrderInstruction {
)
.await;
let payer_info = &match self.side {
Serum3Side::Bid => &quote_info,
Serum3Side::Ask => &base_info,
let (payer_info, receiver_info) = &match self.side {
Serum3Side::Bid => (&quote_info, &base_info),
Serum3Side::Ask => (&base_info, &quote_info),
};
let receiver_active_index = account
.active_token_positions()
.position(|tp| tp.token_index == receiver_info.token_index)
.unwrap();
health_check_metas[receiver_active_index].is_writable = true;
let accounts = Self::Accounts {
group: account.fixed.group,
account: self.account,

View File

@ -1849,6 +1849,32 @@ export class MangoClient {
orderType: Serum3OrderType,
clientOrderId: number,
limit: number,
): Promise<TransactionInstruction[]> {
return await this.serum3PlaceOrderV2Ix(
group,
mangoAccount,
externalMarketPk,
side,
price,
size,
selfTradeBehavior,
orderType,
clientOrderId,
limit,
);
}
public async serum3PlaceOrderV1Ix(
group: Group,
mangoAccount: MangoAccount,
externalMarketPk: PublicKey,
side: Serum3Side,
price: number,
size: number,
selfTradeBehavior: Serum3SelfTradeBehavior,
orderType: Serum3OrderType,
clientOrderId: number,
limit: number,
): Promise<TransactionInstruction[]> {
const ixs: TransactionInstruction[] = [];
const serum3Market = group.serum3MarketsMapByExternal.get(
@ -1962,6 +1988,143 @@ export class MangoClient {
return ixs;
}
public async serum3PlaceOrderV2Ix(
group: Group,
mangoAccount: MangoAccount,
externalMarketPk: PublicKey,
side: Serum3Side,
price: number,
size: number,
selfTradeBehavior: Serum3SelfTradeBehavior,
orderType: Serum3OrderType,
clientOrderId: number,
limit: number,
): Promise<TransactionInstruction[]> {
const ixs: TransactionInstruction[] = [];
const serum3Market = group.serum3MarketsMapByExternal.get(
externalMarketPk.toBase58(),
)!;
let openOrderPk: PublicKey | undefined = undefined;
const banks: Bank[] = [];
const openOrdersForMarket: [Serum3Market, PublicKey][] = [];
if (!mangoAccount.getSerum3Account(serum3Market.marketIndex)) {
const ix = await this.serum3CreateOpenOrdersIx(
group,
mangoAccount,
serum3Market.serumMarketExternal,
);
ixs.push(ix);
openOrderPk = await serum3Market.findOoPda(
this.program.programId,
mangoAccount.publicKey,
);
openOrdersForMarket.push([serum3Market, openOrderPk]);
const baseTokenIndex = serum3Market.baseTokenIndex;
const quoteTokenIndex = serum3Market.quoteTokenIndex;
// only include banks if no deposit has been previously made for same token
banks.push(group.getFirstBankByTokenIndex(quoteTokenIndex));
banks.push(group.getFirstBankByTokenIndex(baseTokenIndex));
}
const healthRemainingAccounts: PublicKey[] =
this.buildHealthRemainingAccounts(
group,
[mangoAccount],
banks,
[],
openOrdersForMarket,
);
const serum3MarketExternal = group.serum3ExternalMarketsMap.get(
externalMarketPk.toBase58(),
)!;
const serum3MarketExternalVaultSigner =
await generateSerum3MarketExternalVaultSignerAddress(
this.cluster,
serum3Market,
serum3MarketExternal,
);
const limitPrice = serum3MarketExternal.priceNumberToLots(price);
const maxBaseQuantity = serum3MarketExternal.baseSizeNumberToLots(size);
const isTaker = orderType !== Serum3OrderType.postOnly;
const maxQuoteQuantity = new BN(
Math.ceil(
serum3MarketExternal.decoded.quoteLotSize.toNumber() *
(1 + Math.max(serum3Market.getFeeRates(isTaker), 0)) *
serum3MarketExternal.baseSizeNumberToLots(size).toNumber() *
serum3MarketExternal.priceNumberToLots(price).toNumber(),
),
);
const payerTokenIndex = ((): TokenIndex => {
if (side == Serum3Side.bid) {
return serum3Market.quoteTokenIndex;
} else {
return serum3Market.baseTokenIndex;
}
})();
const receiverTokenIndex = ((): TokenIndex => {
if (side == Serum3Side.bid) {
return serum3Market.baseTokenIndex;
} else {
return serum3Market.quoteTokenIndex;
}
})();
const payerBank = group.getFirstBankByTokenIndex(payerTokenIndex);
const receiverBank = group.getFirstBankByTokenIndex(receiverTokenIndex);
const ix = await this.program.methods
.serum3PlaceOrderV2(
side,
limitPrice,
maxBaseQuantity,
maxQuoteQuantity,
selfTradeBehavior,
orderType,
new BN(clientOrderId),
limit,
)
.accounts({
group: group.publicKey,
account: mangoAccount.publicKey,
owner: (this.program.provider as AnchorProvider).wallet.publicKey,
openOrders:
openOrderPk ||
mangoAccount.getSerum3Account(serum3Market.marketIndex)?.openOrders,
serumMarket: serum3Market.publicKey,
serumProgram: OPENBOOK_PROGRAM_ID[this.cluster],
serumMarketExternal: serum3Market.serumMarketExternal,
marketBids: serum3MarketExternal.bidsAddress,
marketAsks: serum3MarketExternal.asksAddress,
marketEventQueue: serum3MarketExternal.decoded.eventQueue,
marketRequestQueue: serum3MarketExternal.decoded.requestQueue,
marketBaseVault: serum3MarketExternal.decoded.baseVault,
marketQuoteVault: serum3MarketExternal.decoded.quoteVault,
marketVaultSigner: serum3MarketExternalVaultSigner,
payerBank: payerBank.publicKey,
payerVault: payerBank.vault,
payerOracle: payerBank.oracle,
})
.remainingAccounts(
healthRemainingAccounts.map(
(pk) =>
({
pubkey: pk,
isWritable: receiverBank.publicKey.equals(pk) ? true : false,
isSigner: false,
} as AccountMeta),
),
)
.instruction();
ixs.push(ix);
return ixs;
}
public async serum3PlaceOrder(
group: Group,
mangoAccount: MangoAccount,
@ -1974,7 +2137,7 @@ export class MangoClient {
clientOrderId: number,
limit: number,
): Promise<MangoSignatureStatus> {
const placeOrderIxs = await this.serum3PlaceOrderIx(
const placeOrderIxs = await this.serum3PlaceOrderV2Ix(
group,
mangoAccount,
externalMarketPk,
@ -4520,7 +4683,7 @@ export class MangoClient {
orderId,
),
this.serum3SettleFundsV2Ix(group, mangoAccount, externalMarketPk),
this.serum3PlaceOrderIx(
this.serum3PlaceOrderV2Ix(
group,
mangoAccount,
externalMarketPk,

View File

@ -282,6 +282,7 @@ export interface IxGateParams {
TokenConditionalSwapStart: boolean;
TokenConditionalSwapCreatePremiumAuction: boolean;
TokenConditionalSwapCreateLinearAuction: boolean;
Serum3PlaceOrderV2: boolean;
}
// Default with all ixs enabled, use with buildIxGate
@ -360,6 +361,7 @@ export const TrueIxGateParams: IxGateParams = {
TokenConditionalSwapStart: true,
TokenConditionalSwapCreatePremiumAuction: true,
TokenConditionalSwapCreateLinearAuction: true,
Serum3PlaceOrderV2: true,
};
// build ix gate e.g. buildIxGate(Builder(TrueIxGateParams).TokenDeposit(false).build()).toNumber(),
@ -448,6 +450,7 @@ export function buildIxGate(p: IxGateParams): BN {
toggleIx(ixGate, p, 'TokenConditionalSwapStart', 68);
toggleIx(ixGate, p, 'TokenConditionalSwapCreatePremiumAuction', 69);
toggleIx(ixGate, p, 'TokenConditionalSwapCreateLinearAuction', 70);
toggleIx(ixGate, p, 'Serum3PlaceOrderV2', 71);
return ixGate;
}

View File

@ -2740,6 +2740,161 @@ export type MangoV4 = {
}
]
},
{
"name": "serum3PlaceOrderV2",
"accounts": [
{
"name": "group",
"isMut": false,
"isSigner": false
},
{
"name": "account",
"isMut": true,
"isSigner": false,
"relations": [
"group"
]
},
{
"name": "owner",
"isMut": false,
"isSigner": true
},
{
"name": "openOrders",
"isMut": true,
"isSigner": false
},
{
"name": "serumMarket",
"isMut": false,
"isSigner": false,
"relations": [
"group",
"serum_program",
"serum_market_external"
]
},
{
"name": "serumProgram",
"isMut": false,
"isSigner": false
},
{
"name": "serumMarketExternal",
"isMut": true,
"isSigner": false
},
{
"name": "marketBids",
"isMut": true,
"isSigner": false
},
{
"name": "marketAsks",
"isMut": true,
"isSigner": false
},
{
"name": "marketEventQueue",
"isMut": true,
"isSigner": false
},
{
"name": "marketRequestQueue",
"isMut": true,
"isSigner": false
},
{
"name": "marketBaseVault",
"isMut": true,
"isSigner": false
},
{
"name": "marketQuoteVault",
"isMut": true,
"isSigner": false
},
{
"name": "marketVaultSigner",
"isMut": false,
"isSigner": false,
"docs": [
"needed for the automatic settle_funds call"
]
},
{
"name": "payerBank",
"isMut": true,
"isSigner": false,
"docs": [
"The bank that pays for the order, if necessary"
],
"relations": [
"group"
]
},
{
"name": "payerVault",
"isMut": true,
"isSigner": false,
"docs": [
"The bank vault that pays for the order, if necessary"
]
},
{
"name": "payerOracle",
"isMut": false,
"isSigner": false
},
{
"name": "tokenProgram",
"isMut": false,
"isSigner": false
}
],
"args": [
{
"name": "side",
"type": {
"defined": "Serum3Side"
}
},
{
"name": "limitPrice",
"type": "u64"
},
{
"name": "maxBaseQty",
"type": "u64"
},
{
"name": "maxNativeQuoteQtyIncludingFees",
"type": "u64"
},
{
"name": "selfTradeBehavior",
"type": {
"defined": "Serum3SelfTradeBehavior"
}
},
{
"name": "orderType",
"type": {
"defined": "Serum3OrderType"
}
},
{
"name": "clientOrderId",
"type": "u64"
},
{
"name": "limit",
"type": "u16"
}
]
},
{
"name": "serum3CancelOrder",
"accounts": [
@ -7108,7 +7263,13 @@ export type MangoV4 = {
"type": "f64"
},
{
"name": "depositsInSerum",
"name": "potentialSerumTokens",
"docs": [
"Largest amount of tokens that might be added the the bank based on",
"serum open order execution.",
"",
"Can be negative with multiple banks, then it'd need to be balanced in the keeper."
],
"type": "i64"
},
{
@ -8680,26 +8841,41 @@ export type MangoV4 = {
"type": "f64"
},
{
"name": "baseDepositsReserved",
"name": "potentialBaseTokens",
"docs": [
"Tracks the amount of deposits that flowed into the serum open orders account.",
"An overestimate of the amount of tokens that might flow out of the open orders account.",
"",
"The bank still considers these amounts user deposits (see deposits_in_serum)",
"and they need to be deducted from there when they flow back into the bank",
"as real tokens."
"The bank still considers these amounts user deposits (see Bank::potential_serum_tokens)",
"and that value needs to be updated in conjunction with these numbers.",
"",
"This estimation is based on the amount of tokens in the open orders account",
"(see update_bank_potential_tokens() in serum3_place_order and settle)"
],
"type": "u64"
},
{
"name": "quoteDepositsReserved",
"name": "potentialQuoteTokens",
"type": "u64"
},
{
"name": "lowestPlacedBidInv",
"docs": [
"Track lowest bid/highest ask, same way as for highest bid/lowest ask.",
"",
"0 is a special \"unset\" state."
],
"type": "f64"
},
{
"name": "highestPlacedAsk",
"type": "f64"
},
{
"name": "reserved",
"type": {
"array": [
"u8",
32
16
]
}
}
@ -10362,6 +10538,9 @@ export type MangoV4 = {
},
{
"name": "TokenConditionalSwapCreateLinearAuction"
},
{
"name": "Serum3PlaceOrderV2"
}
]
}
@ -13283,6 +13462,11 @@ export type MangoV4 = {
"code": 6059,
"name": "TokenConditionalSwapTypeNotStartable",
"msg": "token conditional swap type cannot be started"
},
{
"code": 6060,
"name": "HealthAccountBankNotWritable",
"msg": "a bank in the health account list should be writable but is not"
}
]
};
@ -16029,6 +16213,161 @@ export const IDL: MangoV4 = {
}
]
},
{
"name": "serum3PlaceOrderV2",
"accounts": [
{
"name": "group",
"isMut": false,
"isSigner": false
},
{
"name": "account",
"isMut": true,
"isSigner": false,
"relations": [
"group"
]
},
{
"name": "owner",
"isMut": false,
"isSigner": true
},
{
"name": "openOrders",
"isMut": true,
"isSigner": false
},
{
"name": "serumMarket",
"isMut": false,
"isSigner": false,
"relations": [
"group",
"serum_program",
"serum_market_external"
]
},
{
"name": "serumProgram",
"isMut": false,
"isSigner": false
},
{
"name": "serumMarketExternal",
"isMut": true,
"isSigner": false
},
{
"name": "marketBids",
"isMut": true,
"isSigner": false
},
{
"name": "marketAsks",
"isMut": true,
"isSigner": false
},
{
"name": "marketEventQueue",
"isMut": true,
"isSigner": false
},
{
"name": "marketRequestQueue",
"isMut": true,
"isSigner": false
},
{
"name": "marketBaseVault",
"isMut": true,
"isSigner": false
},
{
"name": "marketQuoteVault",
"isMut": true,
"isSigner": false
},
{
"name": "marketVaultSigner",
"isMut": false,
"isSigner": false,
"docs": [
"needed for the automatic settle_funds call"
]
},
{
"name": "payerBank",
"isMut": true,
"isSigner": false,
"docs": [
"The bank that pays for the order, if necessary"
],
"relations": [
"group"
]
},
{
"name": "payerVault",
"isMut": true,
"isSigner": false,
"docs": [
"The bank vault that pays for the order, if necessary"
]
},
{
"name": "payerOracle",
"isMut": false,
"isSigner": false
},
{
"name": "tokenProgram",
"isMut": false,
"isSigner": false
}
],
"args": [
{
"name": "side",
"type": {
"defined": "Serum3Side"
}
},
{
"name": "limitPrice",
"type": "u64"
},
{
"name": "maxBaseQty",
"type": "u64"
},
{
"name": "maxNativeQuoteQtyIncludingFees",
"type": "u64"
},
{
"name": "selfTradeBehavior",
"type": {
"defined": "Serum3SelfTradeBehavior"
}
},
{
"name": "orderType",
"type": {
"defined": "Serum3OrderType"
}
},
{
"name": "clientOrderId",
"type": "u64"
},
{
"name": "limit",
"type": "u16"
}
]
},
{
"name": "serum3CancelOrder",
"accounts": [
@ -20397,7 +20736,13 @@ export const IDL: MangoV4 = {
"type": "f64"
},
{
"name": "depositsInSerum",
"name": "potentialSerumTokens",
"docs": [
"Largest amount of tokens that might be added the the bank based on",
"serum open order execution.",
"",
"Can be negative with multiple banks, then it'd need to be balanced in the keeper."
],
"type": "i64"
},
{
@ -21969,26 +22314,41 @@ export const IDL: MangoV4 = {
"type": "f64"
},
{
"name": "baseDepositsReserved",
"name": "potentialBaseTokens",
"docs": [
"Tracks the amount of deposits that flowed into the serum open orders account.",
"An overestimate of the amount of tokens that might flow out of the open orders account.",
"",
"The bank still considers these amounts user deposits (see deposits_in_serum)",
"and they need to be deducted from there when they flow back into the bank",
"as real tokens."
"The bank still considers these amounts user deposits (see Bank::potential_serum_tokens)",
"and that value needs to be updated in conjunction with these numbers.",
"",
"This estimation is based on the amount of tokens in the open orders account",
"(see update_bank_potential_tokens() in serum3_place_order and settle)"
],
"type": "u64"
},
{
"name": "quoteDepositsReserved",
"name": "potentialQuoteTokens",
"type": "u64"
},
{
"name": "lowestPlacedBidInv",
"docs": [
"Track lowest bid/highest ask, same way as for highest bid/lowest ask.",
"",
"0 is a special \"unset\" state."
],
"type": "f64"
},
{
"name": "highestPlacedAsk",
"type": "f64"
},
{
"name": "reserved",
"type": {
"array": [
"u8",
32
16
]
}
}
@ -23651,6 +24011,9 @@ export const IDL: MangoV4 = {
},
{
"name": "TokenConditionalSwapCreateLinearAuction"
},
{
"name": "Serum3PlaceOrderV2"
}
]
}
@ -26572,6 +26935,11 @@ export const IDL: MangoV4 = {
"code": 6059,
"name": "TokenConditionalSwapTypeNotStartable",
"msg": "token conditional swap type cannot be started"
},
{
"code": 6060,
"name": "HealthAccountBankNotWritable",
"msg": "a bank in the health account list should be writable but is not"
}
]
};