Merge branch 'main' into deploy
This commit is contained in:
commit
59fe0ffcd9
36
CHANGELOG.md
36
CHANGELOG.md
|
@ -4,10 +4,42 @@ Update this for each program release and mainnet deployment.
|
|||
|
||||
## not on mainnet
|
||||
|
||||
### v0.14.0, 2023-4-
|
||||
### v0.15.0, 2023-5-
|
||||
|
||||
Deployment:
|
||||
|
||||
- Change TokenRegisterTrustless instruction to disable borrows by default (#567)
|
||||
|
||||
The instruction is intended to use very conservative defaults for listing
|
||||
tokens. It now lists new tokens with zero asset weights and without allowing
|
||||
borrowing, which should leave oracle staleness and potential bugs as the main
|
||||
risks of listing new tokens.
|
||||
|
||||
- OpenBook place order instruction: Respect reduce-only flags on the base and
|
||||
quote bank (#569)
|
||||
|
||||
This way the DAO can potentially leave related OpenBook markets open when it
|
||||
marks a token as reduce-only.
|
||||
|
||||
- FlashLoan: Whitelist the ComputeBudget program when called by delegates (#572)
|
||||
|
||||
For convenience. When constructing a flash loan instruction for a delegated
|
||||
account, users no longer need to take care to remove compute budget
|
||||
instructions from the flash loan scope.
|
||||
|
||||
- Perp Order Matching: Exit when no lots can be filled due to the quote limit (#576)
|
||||
|
||||
Previously it would keep looping unnecessarily.
|
||||
|
||||
- Improve error message for incorrect number of accounts in FixedAccountRetriever (#566)
|
||||
- Add oracle confidence and type information to perp update funding logs (#568)
|
||||
|
||||
## mainnet
|
||||
|
||||
### v0.14.0, 2023-4-29
|
||||
|
||||
Deployment: Apr 29, 2023 at 11:58:43 Central European Summer Time, https://explorer.solana.com/tx/2iaLQTT6PqFjFQr94j5g2iUhDT9v6CJk5rNC9mY7cY7BfRjn6pWixnUF5Wv2qAAUq4hmEvM7WyajDxQjq6QbufSk
|
||||
|
||||
- Force-closing of perp positions (#525)
|
||||
|
||||
When a perp markets is set to "force-close" by the DAO, anyone can close open
|
||||
|
@ -32,8 +64,6 @@ Deployment:
|
|||
- Fix perp order seqnum logging (#556)
|
||||
- Fix build when using mango-v4 code with the "no-entrypoint" feature (#558)
|
||||
|
||||
## mainnet
|
||||
|
||||
### v0.13.0, 2023-4-18
|
||||
|
||||
Deployment: Apr 18, 2023 at 17:33:15 Central European Summer Time, https://explorer.solana.com/tx/4WWVHCAheTRBhzyXUjsV1Kqfn8LdnkupiVbK4qaPNqby8P5vv7hY6HS3rHHL9bMu1RGdCZvqsd2MHjdawLYQ6Pxi
|
||||
|
|
|
@ -3005,7 +3005,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "mango-v4"
|
||||
version = "0.14.0"
|
||||
version = "0.15.0"
|
||||
dependencies = [
|
||||
"anchor-lang",
|
||||
"anchor-spl",
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "0.14.0",
|
||||
"version": "0.15.0",
|
||||
"name": "mango_v4",
|
||||
"instructions": [
|
||||
{
|
||||
|
@ -7881,6 +7881,78 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "PerpUpdateFundingLogV2",
|
||||
"fields": [
|
||||
{
|
||||
"name": "mangoGroup",
|
||||
"type": "publicKey",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "marketIndex",
|
||||
"type": "u16",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "longFunding",
|
||||
"type": "i128",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "shortFunding",
|
||||
"type": "i128",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "price",
|
||||
"type": "i128",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "oracleSlot",
|
||||
"type": "u64",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "oracleConfidence",
|
||||
"type": "i128",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "oracleType",
|
||||
"type": {
|
||||
"defined": "OracleType"
|
||||
},
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "stablePrice",
|
||||
"type": "i128",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "feesAccrued",
|
||||
"type": "i128",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "feesSettled",
|
||||
"type": "i128",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "openInterest",
|
||||
"type": "i64",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "instantaneousFundingRate",
|
||||
"type": "i128",
|
||||
"index": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "UpdateIndexLog",
|
||||
"fields": [
|
||||
|
@ -9009,6 +9081,11 @@
|
|||
"code": 6046,
|
||||
"name": "TokenInForceClose",
|
||||
"msg": "token is in force close"
|
||||
},
|
||||
{
|
||||
"code": 6047,
|
||||
"name": "InvalidHealthAccountCount",
|
||||
"msg": "incorrect number of health accounts"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "mango-v4"
|
||||
version = "0.14.0"
|
||||
version = "0.15.0"
|
||||
description = "Created with Anchor"
|
||||
edition = "2021"
|
||||
|
||||
|
|
|
@ -99,6 +99,8 @@ pub enum MangoError {
|
|||
HealthRegionBadInnerInstruction,
|
||||
#[msg("token is in force close")]
|
||||
TokenInForceClose,
|
||||
#[msg("incorrect number of health accounts")]
|
||||
InvalidHealthAccountCount,
|
||||
}
|
||||
|
||||
impl MangoError {
|
||||
|
|
|
@ -66,7 +66,11 @@ pub fn new_fixed_order_account_retriever<'a, 'info>(
|
|||
let expected_ais = active_token_len * 2 // banks + oracles
|
||||
+ active_perp_len * 2 // PerpMarkets + Oracles
|
||||
+ active_serum3_len; // open_orders
|
||||
require_eq!(ais.len(), expected_ais);
|
||||
require_msg_typed!(ais.len() == expected_ais, MangoError::InvalidHealthAccountCount,
|
||||
"received {} accounts but expected {} ({} banks, {} bank oracles, {} perp markets, {} perp oracles, {} serum3 oos)",
|
||||
ais.len(), expected_ais,
|
||||
active_token_len, active_token_len, active_perp_len, active_perp_len, active_serum3_len
|
||||
);
|
||||
|
||||
Ok(FixedOrderAccountRetriever {
|
||||
ais: AccountInfoRef::borrow_slice(ais)?,
|
||||
|
|
|
@ -149,6 +149,32 @@ pub struct Serum3Info {
|
|||
}
|
||||
|
||||
impl Serum3Info {
|
||||
#[inline(always)]
|
||||
fn all_reserved_as_base(
|
||||
&self,
|
||||
health_type: HealthType,
|
||||
quote_info: &TokenInfo,
|
||||
base_info: &TokenInfo,
|
||||
) -> I80F48 {
|
||||
let quote_asset = quote_info.prices.asset(health_type);
|
||||
let base_liab = base_info.prices.liab(health_type);
|
||||
// OPTIMIZATION: These divisions can be extremely expensive (up to 5k CU each)
|
||||
self.reserved_base + self.reserved_quote * quote_asset / base_liab
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn all_reserved_as_quote(
|
||||
&self,
|
||||
health_type: HealthType,
|
||||
quote_info: &TokenInfo,
|
||||
base_info: &TokenInfo,
|
||||
) -> I80F48 {
|
||||
let base_asset = base_info.prices.asset(health_type);
|
||||
let quote_liab = quote_info.prices.liab(health_type);
|
||||
// OPTIMIZATION: These divisions can be extremely expensive (up to 5k CU each)
|
||||
self.reserved_quote + self.reserved_base * base_asset / quote_liab
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn health_contribution(
|
||||
&self,
|
||||
|
@ -600,23 +626,15 @@ impl HealthCache {
|
|||
let mut serum3_reserved = Vec::with_capacity(self.serum3_infos.len());
|
||||
|
||||
for info in self.serum3_infos.iter() {
|
||||
let quote = &self.token_infos[info.quote_index];
|
||||
let base = &self.token_infos[info.base_index];
|
||||
let quote_info = &self.token_infos[info.quote_index];
|
||||
let base_info = &self.token_infos[info.base_index];
|
||||
|
||||
let reserved_base = info.reserved_base;
|
||||
let reserved_quote = info.reserved_quote;
|
||||
|
||||
let quote_asset = quote.prices.asset(health_type);
|
||||
let base_liab = base.prices.liab(health_type);
|
||||
// OPTIMIZATION: These divisions can be extremely expensive (up to 5k CU each)
|
||||
let all_reserved_as_base = reserved_base + reserved_quote * quote_asset / base_liab;
|
||||
|
||||
let base_asset = base.prices.asset(health_type);
|
||||
let quote_liab = quote.prices.liab(health_type);
|
||||
let all_reserved_as_quote = reserved_quote + reserved_base * base_asset / quote_liab;
|
||||
let all_reserved_as_base =
|
||||
info.all_reserved_as_base(health_type, quote_info, base_info);
|
||||
let all_reserved_as_quote =
|
||||
info.all_reserved_as_quote(health_type, quote_info, base_info);
|
||||
|
||||
let base_max_reserved = &mut token_max_reserved[info.base_index];
|
||||
// note: () does not work with mutable references
|
||||
*base_max_reserved += all_reserved_as_base;
|
||||
let quote_max_reserved = &mut token_max_reserved[info.quote_index];
|
||||
*quote_max_reserved += all_reserved_as_quote;
|
||||
|
@ -688,6 +706,36 @@ impl HealthCache {
|
|||
}
|
||||
health
|
||||
}
|
||||
|
||||
pub fn total_serum3_potential(
|
||||
&self,
|
||||
health_type: HealthType,
|
||||
token_index: TokenIndex,
|
||||
) -> Result<I80F48> {
|
||||
let target_token_info_index = self.token_info_index(token_index)?;
|
||||
let total_reserved = self
|
||||
.serum3_infos
|
||||
.iter()
|
||||
.filter_map(|info| {
|
||||
if info.quote_index == target_token_info_index {
|
||||
Some(info.all_reserved_as_quote(
|
||||
health_type,
|
||||
&self.token_infos[info.quote_index],
|
||||
&self.token_infos[info.base_index],
|
||||
))
|
||||
} else if info.base_index == target_token_info_index {
|
||||
Some(info.all_reserved_as_base(
|
||||
health_type,
|
||||
&self.token_infos[info.quote_index],
|
||||
&self.token_infos[info.base_index],
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.sum();
|
||||
Ok(total_reserved)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn find_token_info_index(infos: &[TokenInfo], token_index: TokenIndex) -> Result<usize> {
|
||||
|
|
|
@ -138,8 +138,9 @@ pub fn flash_loan_begin<'key, 'accounts, 'remaining, 'info>(
|
|||
ix.program_id == AssociatedToken::id()
|
||||
|| ix.program_id == jupiter_mainnet_3::ID
|
||||
|| ix.program_id == jupiter_mainnet_4::ID
|
||||
|| ix.program_id == compute_budget::ID
|
||||
|| ix.program_id == crate::id(),
|
||||
"delegate is only allowed to pass in ixs to ATA or Jupiter v3 or v4 programs"
|
||||
"delegate is only allowed to pass in ixs to ATA or Jupiter v3 or v4 programs, passed ({})", ix.program_id
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -30,13 +30,13 @@ pub fn perp_place_order(
|
|||
asks: ctx.accounts.asks.load_mut()?,
|
||||
};
|
||||
|
||||
let oracle_slot;
|
||||
(oracle_price, oracle_slot) = perp_market.oracle_price_and_slot(
|
||||
let oracle_state;
|
||||
(oracle_price, oracle_state) = perp_market.oracle_price_and_state(
|
||||
&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?,
|
||||
None, // staleness checked in health
|
||||
)?;
|
||||
|
||||
perp_market.update_funding_and_stable_price(&book, oracle_price, oracle_slot, now_ts)?;
|
||||
perp_market.update_funding_and_stable_price(&book, oracle_price, oracle_state, now_ts)?;
|
||||
}
|
||||
|
||||
let mut account = ctx.accounts.account.load_full_mut()?;
|
||||
|
|
|
@ -14,12 +14,12 @@ pub fn perp_update_funding(ctx: Context<PerpUpdateFunding>) -> Result<()> {
|
|||
};
|
||||
|
||||
let now_slot = Clock::get()?.slot;
|
||||
let (oracle_price, oracle_slot) = perp_market.oracle_price_and_slot(
|
||||
let (oracle_price, oracle_state) = perp_market.oracle_price_and_state(
|
||||
&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?,
|
||||
Some(now_slot),
|
||||
)?;
|
||||
|
||||
perp_market.update_funding_and_stable_price(&book, oracle_price, oracle_slot, now_ts)?;
|
||||
perp_market.update_funding_and_stable_price(&book, oracle_price, oracle_state, now_ts)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -113,6 +113,7 @@ pub fn serum3_place_order(
|
|||
//
|
||||
// Validation
|
||||
//
|
||||
let receiver_token_index;
|
||||
{
|
||||
let account = ctx.accounts.account.load_full()?;
|
||||
// account constraint #1
|
||||
|
@ -138,23 +139,40 @@ pub fn serum3_place_order(
|
|||
Serum3Side::Ask => serum_market.base_token_index,
|
||||
};
|
||||
require_eq!(payer_bank.token_index, payer_token_index);
|
||||
|
||||
receiver_token_index = match side {
|
||||
Serum3Side::Bid => serum_market.base_token_index,
|
||||
Serum3Side::Ask => serum_market.quote_token_index,
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Pre-health computation
|
||||
//
|
||||
let mut account = ctx.accounts.account.load_full_mut()?;
|
||||
let retriever = new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
|
||||
let mut health_cache =
|
||||
new_health_cache(&account.borrow(), &retriever).context("pre-withdraw init health")?;
|
||||
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_init_health = account.check_health_pre(&health_cache)?;
|
||||
Some((health_cache, pre_init_health))
|
||||
Some(pre_init_health)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Check if the bank for the token whose balance is increased is in reduce-only mode
|
||||
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()
|
||||
};
|
||||
|
||||
drop(retriever);
|
||||
|
||||
//
|
||||
// Before-order tracking
|
||||
//
|
||||
|
@ -212,30 +230,29 @@ pub fn serum3_place_order(
|
|||
};
|
||||
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!(Serum3OpenOrdersBalanceLogV2 {
|
||||
mango_group: ctx.accounts.group.key(),
|
||||
mango_account: ctx.accounts.account.key(),
|
||||
market_index: serum_market.market_index,
|
||||
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
|
||||
//
|
||||
let after_oo = {
|
||||
let oo_ai = &ctx.accounts.open_orders.as_ref();
|
||||
let open_orders = load_open_orders_ref(oo_ai)?;
|
||||
OpenOrdersSlim::from_oo(&open_orders)
|
||||
};
|
||||
let oo_difference = OODifference::new(&before_oo, &after_oo);
|
||||
|
||||
emit!(Serum3OpenOrdersBalanceLogV2 {
|
||||
mango_group: ctx.accounts.group.key(),
|
||||
mango_account: ctx.accounts.account.key(),
|
||||
market_index: serum_market.market_index,
|
||||
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,
|
||||
});
|
||||
|
||||
ctx.accounts.payer_vault.reload()?;
|
||||
let after_vault = ctx.accounts.payer_vault.amount;
|
||||
|
||||
|
@ -264,23 +281,45 @@ pub fn serum3_place_order(
|
|||
};
|
||||
|
||||
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)?;
|
||||
}
|
||||
|
||||
vault_difference.adjust_health_cache_token_balance(&mut health_cache, &payer_bank)?;
|
||||
oo_difference.adjust_health_cache_serum3_state(&mut health_cache, &serum_market)?;
|
||||
|
||||
// Check the receiver's reduce only flag.
|
||||
//
|
||||
// Note that all orders on the book executing can still cause a net deposit. That's because
|
||||
// the total serum3 potential amount assumes all reserved amounts convert at the current
|
||||
// oracle price.
|
||||
if receiver_bank_reduce_only {
|
||||
let balance = health_cache
|
||||
.token_info(receiver_token_index)?
|
||||
.balance_native;
|
||||
let potential =
|
||||
health_cache.total_serum3_potential(HealthType::Maint, receiver_token_index)?;
|
||||
require_msg_typed!(
|
||||
balance + potential < 1,
|
||||
MangoError::TokenInReduceOnlyMode,
|
||||
"receiver bank does not accept deposits"
|
||||
);
|
||||
}
|
||||
|
||||
//
|
||||
// Health check
|
||||
//
|
||||
if let Some((mut health_cache, pre_init_health)) = pre_health_opt {
|
||||
vault_difference.adjust_health_cache_token_balance(&mut health_cache, &payer_bank)?;
|
||||
oo_difference.adjust_health_cache_serum3_state(&mut health_cache, &serum_market)?;
|
||||
if let Some(pre_init_health) = pre_health_opt {
|
||||
account.check_health_post(&health_cache, pre_init_health)?;
|
||||
}
|
||||
|
||||
// TODO: enforce min_vault_to_deposits_ratio
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
@ -71,9 +71,9 @@ pub fn token_register_trustless(
|
|||
* net_borrow_limit_window_size_ts,
|
||||
net_borrow_limit_per_window_quote: 250_000_000_000, // $250k
|
||||
net_borrows_in_window: 0,
|
||||
borrow_weight_scale_start_quote: 100_000_000_000.0, // $100k
|
||||
deposit_weight_scale_start_quote: 100_000_000_000.0, // $100k
|
||||
reduce_only: 0,
|
||||
borrow_weight_scale_start_quote: 5_000_000_000.0, // $5k
|
||||
deposit_weight_scale_start_quote: 5_000_000_000.0, // $5k
|
||||
reduce_only: 2, // deposit-only
|
||||
force_close: 0,
|
||||
reserved: [0; 2118],
|
||||
};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::{
|
||||
accounts_ix::FlashLoanType,
|
||||
state::{PerpMarket, PerpPosition},
|
||||
state::{OracleType, PerpMarket, PerpPosition},
|
||||
};
|
||||
use anchor_lang::prelude::*;
|
||||
use borsh::BorshSerialize;
|
||||
|
@ -152,6 +152,23 @@ pub struct PerpUpdateFundingLog {
|
|||
pub instantaneous_funding_rate: i128,
|
||||
}
|
||||
|
||||
#[event]
|
||||
pub struct PerpUpdateFundingLogV2 {
|
||||
pub mango_group: Pubkey,
|
||||
pub market_index: u16,
|
||||
pub long_funding: i128,
|
||||
pub short_funding: i128,
|
||||
pub price: i128,
|
||||
pub oracle_slot: u64,
|
||||
pub oracle_confidence: i128,
|
||||
pub oracle_type: OracleType,
|
||||
pub stable_price: i128,
|
||||
pub fees_accrued: i128,
|
||||
pub fees_settled: i128,
|
||||
pub open_interest: i64,
|
||||
pub instantaneous_funding_rate: i128,
|
||||
}
|
||||
|
||||
#[event]
|
||||
pub struct UpdateIndexLog {
|
||||
pub mango_group: Pubkey,
|
||||
|
|
|
@ -789,7 +789,7 @@ impl Bank {
|
|||
staleness_slot: Option<u64>,
|
||||
) -> Result<I80F48> {
|
||||
require_keys_eq!(self.oracle, *oracle_acc.key());
|
||||
let (price, _) = oracle::oracle_price_and_slot(
|
||||
let (price, _) = oracle::oracle_price_and_state(
|
||||
oracle_acc,
|
||||
&self.oracle_config,
|
||||
self.mint_decimals,
|
||||
|
|
|
@ -83,7 +83,7 @@ impl OracleConfigParams {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
#[derive(PartialEq, AnchorSerialize, AnchorDeserialize)]
|
||||
pub enum OracleType {
|
||||
Pyth,
|
||||
Stub,
|
||||
|
@ -91,6 +91,12 @@ pub enum OracleType {
|
|||
SwitchboardV2,
|
||||
}
|
||||
|
||||
pub struct OracleState {
|
||||
pub last_update_slot: u64,
|
||||
pub confidence: I80F48,
|
||||
pub oracle_type: OracleType,
|
||||
}
|
||||
|
||||
#[account(zero_copy(safe_bytemuck_derives))]
|
||||
pub struct StubOracle {
|
||||
// ABI: Clients rely on this being at offset 8
|
||||
|
@ -135,18 +141,25 @@ pub fn determine_oracle_type(acc_info: &impl KeyedAccountReader) -> Result<Oracl
|
|||
/// This currently assumes that quote decimals is 6, like for USDC.
|
||||
///
|
||||
/// Pass `staleness_slot` = None to skip the staleness check
|
||||
pub fn oracle_price_and_slot(
|
||||
pub fn oracle_price_and_state(
|
||||
acc_info: &impl KeyedAccountReader,
|
||||
config: &OracleConfig,
|
||||
base_decimals: u8,
|
||||
staleness_slot: Option<u64>,
|
||||
) -> Result<(I80F48, u64)> {
|
||||
) -> Result<(I80F48, OracleState)> {
|
||||
let data = &acc_info.data();
|
||||
let oracle_type = determine_oracle_type(acc_info)?;
|
||||
let staleness_slot = staleness_slot.unwrap_or(0);
|
||||
|
||||
Ok(match oracle_type {
|
||||
OracleType::Stub => (acc_info.load::<StubOracle>()?.price, 0),
|
||||
OracleType::Stub => (
|
||||
acc_info.load::<StubOracle>()?.price,
|
||||
OracleState {
|
||||
last_update_slot: 0,
|
||||
confidence: I80F48::ZERO,
|
||||
oracle_type: OracleType::Stub,
|
||||
},
|
||||
),
|
||||
OracleType::Pyth => {
|
||||
let price_account = pyth_sdk_solana::state::load_price_account(data).unwrap();
|
||||
let price_data = price_account.to_price();
|
||||
|
@ -187,7 +200,14 @@ pub fn oracle_price_and_slot(
|
|||
|
||||
let decimals = (price_account.expo as i8) + QUOTE_DECIMALS - (base_decimals as i8);
|
||||
let decimal_adj = power_of_ten(decimals);
|
||||
(price * decimal_adj, last_slot)
|
||||
(
|
||||
price * decimal_adj,
|
||||
OracleState {
|
||||
last_update_slot: last_slot,
|
||||
confidence: I80F48::from_num(price_data.conf),
|
||||
oracle_type: OracleType::Pyth,
|
||||
},
|
||||
)
|
||||
}
|
||||
OracleType::SwitchboardV2 => {
|
||||
fn from_foreign_error(e: impl std::fmt::Display) -> Error {
|
||||
|
@ -233,7 +253,14 @@ pub fn oracle_price_and_slot(
|
|||
|
||||
let decimals = QUOTE_DECIMALS - (base_decimals as i8);
|
||||
let decimal_adj = power_of_ten(decimals);
|
||||
(price * decimal_adj, round_open_slot)
|
||||
(
|
||||
price * decimal_adj,
|
||||
OracleState {
|
||||
last_update_slot: round_open_slot,
|
||||
confidence: I80F48::from_num(std_deviation_decimal),
|
||||
oracle_type: OracleType::SwitchboardV2,
|
||||
},
|
||||
)
|
||||
}
|
||||
OracleType::SwitchboardV1 => {
|
||||
let result = FastRoundResultAccountData::deserialize(data).unwrap();
|
||||
|
@ -269,7 +296,14 @@ pub fn oracle_price_and_slot(
|
|||
|
||||
let decimals = QUOTE_DECIMALS - (base_decimals as i8);
|
||||
let decimal_adj = power_of_ten(decimals);
|
||||
(price * decimal_adj, round_open_slot)
|
||||
(
|
||||
price * decimal_adj,
|
||||
OracleState {
|
||||
last_update_slot: round_open_slot,
|
||||
confidence: max_response - min_response,
|
||||
oracle_type: OracleType::SwitchboardV1,
|
||||
},
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -120,6 +120,10 @@ impl<'a> Orderbook<'a> {
|
|||
}
|
||||
|
||||
let max_match_by_quote = remaining_quote_lots / best_opposing_price;
|
||||
if max_match_by_quote == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
let match_base_lots = remaining_base_lots
|
||||
.min(best_opposing.node.quantity)
|
||||
.min(max_match_by_quote);
|
||||
|
|
|
@ -544,4 +544,66 @@ mod tests {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Check that there are no zero-quantity fills when max_quote_lots is not
|
||||
// enough for a single lot
|
||||
#[test]
|
||||
fn book_max_quote_lots() {
|
||||
let (mut perp_market, oracle_price, mut event_queue, book_accs) = test_setup(5000.0);
|
||||
let mut book = book_accs.orderbook();
|
||||
let settle_token_index = 0;
|
||||
|
||||
let mut new_order = |book: &mut Orderbook,
|
||||
event_queue: &mut EventQueue,
|
||||
side,
|
||||
price_lots,
|
||||
max_base_lots: i64,
|
||||
max_quote_lots: i64|
|
||||
-> u128 {
|
||||
let buffer = MangoAccount::default_for_tests().try_to_vec().unwrap();
|
||||
let mut account = MangoAccountValue::from_bytes(&buffer).unwrap();
|
||||
account
|
||||
.ensure_perp_position(perp_market.perp_market_index, settle_token_index)
|
||||
.unwrap();
|
||||
|
||||
book.new_order(
|
||||
Order {
|
||||
side,
|
||||
max_base_lots,
|
||||
max_quote_lots,
|
||||
client_order_id: 0,
|
||||
time_in_force: 0,
|
||||
reduce_only: false,
|
||||
params: OrderParams::Fixed {
|
||||
price_lots,
|
||||
order_type: PostOrderType::Limit,
|
||||
},
|
||||
},
|
||||
&mut perp_market,
|
||||
event_queue,
|
||||
oracle_price,
|
||||
&mut account.borrow_mut(),
|
||||
&Pubkey::default(),
|
||||
0, // now_ts
|
||||
u8::MAX,
|
||||
)
|
||||
.unwrap();
|
||||
account.perp_order_by_raw_index(0).id
|
||||
};
|
||||
|
||||
// Setup
|
||||
new_order(&mut book, &mut event_queue, Side::Ask, 5000, 5, i64::MAX);
|
||||
new_order(&mut book, &mut event_queue, Side::Ask, 5001, 5, i64::MAX);
|
||||
new_order(&mut book, &mut event_queue, Side::Ask, 5002, 5, i64::MAX);
|
||||
|
||||
// Try taking: the quote limit allows only one base lot to be taken.
|
||||
new_order(&mut book, &mut event_queue, Side::Bid, 5005, 30, 6000);
|
||||
// Only one fill event is generated, the matching aborts even though neither the base nor quote limit
|
||||
// is exhausted.
|
||||
assert_eq!(event_queue.len(), 1);
|
||||
|
||||
// Try taking: the quote limit allows no fills
|
||||
new_order(&mut book, &mut event_queue, Side::Bid, 5005, 30, 1);
|
||||
assert_eq!(event_queue.len(), 1);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,11 +7,11 @@ use static_assertions::const_assert_eq;
|
|||
|
||||
use crate::accounts_zerocopy::KeyedAccountReader;
|
||||
use crate::error::MangoError;
|
||||
use crate::logs::PerpUpdateFundingLog;
|
||||
use crate::logs::PerpUpdateFundingLogV2;
|
||||
use crate::state::orderbook::Side;
|
||||
use crate::state::{oracle, TokenIndex};
|
||||
|
||||
use super::{orderbook, OracleConfig, Orderbook, StablePriceModel, DAY_I80F48};
|
||||
use super::{orderbook, OracleConfig, OracleState, Orderbook, StablePriceModel, DAY_I80F48};
|
||||
|
||||
pub type PerpMarketIndex = u16;
|
||||
|
||||
|
@ -246,7 +246,7 @@ impl PerpMarket {
|
|||
staleness_slot: Option<u64>,
|
||||
) -> Result<I80F48> {
|
||||
require_keys_eq!(self.oracle, *oracle_acc.key());
|
||||
let (price, _) = oracle::oracle_price_and_slot(
|
||||
let (price, _) = oracle::oracle_price_and_state(
|
||||
oracle_acc,
|
||||
&self.oracle_config,
|
||||
self.base_decimals,
|
||||
|
@ -256,13 +256,13 @@ impl PerpMarket {
|
|||
Ok(price)
|
||||
}
|
||||
|
||||
pub fn oracle_price_and_slot(
|
||||
pub fn oracle_price_and_state(
|
||||
&self,
|
||||
oracle_acc: &impl KeyedAccountReader,
|
||||
staleness_slot: Option<u64>,
|
||||
) -> Result<(I80F48, u64)> {
|
||||
) -> Result<(I80F48, OracleState)> {
|
||||
require_keys_eq!(self.oracle, *oracle_acc.key());
|
||||
oracle::oracle_price_and_slot(
|
||||
oracle::oracle_price_and_state(
|
||||
oracle_acc,
|
||||
&self.oracle_config,
|
||||
self.base_decimals,
|
||||
|
@ -279,7 +279,7 @@ impl PerpMarket {
|
|||
&mut self,
|
||||
book: &Orderbook,
|
||||
oracle_price: I80F48,
|
||||
oracle_slot: u64,
|
||||
oracle_state: OracleState,
|
||||
now_ts: u64,
|
||||
) -> Result<()> {
|
||||
if now_ts <= self.funding_last_updated {
|
||||
|
@ -331,13 +331,15 @@ impl PerpMarket {
|
|||
self.stable_price_model
|
||||
.update(now_ts, oracle_price.to_num());
|
||||
|
||||
emit!(PerpUpdateFundingLog {
|
||||
emit!(PerpUpdateFundingLogV2 {
|
||||
mango_group: self.group,
|
||||
market_index: self.perp_market_index,
|
||||
long_funding: self.long_funding.to_bits(),
|
||||
short_funding: self.short_funding.to_bits(),
|
||||
price: oracle_price.to_bits(),
|
||||
oracle_slot: oracle_slot,
|
||||
oracle_slot: oracle_state.last_update_slot,
|
||||
oracle_confidence: oracle_state.confidence.to_bits(),
|
||||
oracle_type: oracle_state.oracle_type,
|
||||
stable_price: self.stable_price().to_bits(),
|
||||
fees_accrued: self.fees_accrued.to_bits(),
|
||||
fees_settled: self.fees_settled.to_bits(),
|
||||
|
|
|
@ -4,7 +4,7 @@ use mango_v4::accounts_ix::{Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side
|
|||
#[tokio::test]
|
||||
async fn test_health_wrap() -> Result<(), TransportError> {
|
||||
let mut test_builder = TestContextBuilder::new();
|
||||
test_builder.test().set_compute_max_units(135000);
|
||||
test_builder.test().set_compute_max_units(150000);
|
||||
let context = test_builder.start_default().await;
|
||||
let solana = &context.solana.clone();
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
#![allow(dead_code)]
|
||||
use super::*;
|
||||
|
||||
use mango_v4::accounts_ix::{Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side};
|
||||
|
@ -34,15 +35,25 @@ impl SerumOrderPlacer {
|
|||
None
|
||||
}
|
||||
|
||||
async fn bid(&mut self, limit_price: f64, max_base: u64) -> Option<(u128, u64)> {
|
||||
async fn try_bid(
|
||||
&mut self,
|
||||
limit_price: f64,
|
||||
max_base: u64,
|
||||
taker: bool,
|
||||
) -> Result<mango_v4::accounts::Serum3PlaceOrder, TransportError> {
|
||||
let client_order_id = self.inc_client_order_id();
|
||||
let fees = if taker { 0.0004 } else { 0.0 };
|
||||
send_tx(
|
||||
&self.solana,
|
||||
Serum3PlaceOrderInstruction {
|
||||
side: Serum3Side::Bid,
|
||||
limit_price: (limit_price * 100.0 / 10.0) as u64, // in quote_lot (10) per base lot (100)
|
||||
max_base_qty: max_base / 100, // in base lot (100)
|
||||
max_native_quote_qty_including_fees: (limit_price * (max_base as f64)) as u64,
|
||||
// 4 bps taker fees added in
|
||||
max_native_quote_qty_including_fees: (limit_price
|
||||
* (max_base as f64)
|
||||
* (1.0 + fees))
|
||||
.ceil() as u64,
|
||||
self_trade_behavior: Serum3SelfTradeBehavior::AbortTransaction,
|
||||
order_type: Serum3OrderType::Limit,
|
||||
client_order_id,
|
||||
|
@ -53,12 +64,25 @@ impl SerumOrderPlacer {
|
|||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
self.find_order_id_for_client_order_id(client_order_id)
|
||||
}
|
||||
|
||||
async fn bid_maker(&mut self, limit_price: f64, max_base: u64) -> Option<(u128, u64)> {
|
||||
self.try_bid(limit_price, max_base, false).await.unwrap();
|
||||
self.find_order_id_for_client_order_id(self.next_client_order_id - 1)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn ask(&mut self, limit_price: f64, max_base: u64) -> Option<(u128, u64)> {
|
||||
async fn bid_taker(&mut self, limit_price: f64, max_base: u64) -> Option<(u128, u64)> {
|
||||
self.try_bid(limit_price, max_base, true).await.unwrap();
|
||||
self.find_order_id_for_client_order_id(self.next_client_order_id - 1)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn try_ask(
|
||||
&mut self,
|
||||
limit_price: f64,
|
||||
max_base: u64,
|
||||
) -> Result<mango_v4::accounts::Serum3PlaceOrder, TransportError> {
|
||||
let client_order_id = self.inc_client_order_id();
|
||||
send_tx(
|
||||
&self.solana,
|
||||
|
@ -77,8 +101,11 @@ impl SerumOrderPlacer {
|
|||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
self.find_order_id_for_client_order_id(client_order_id)
|
||||
}
|
||||
|
||||
async fn ask(&mut self, limit_price: f64, max_base: u64) -> Option<(u128, u64)> {
|
||||
self.try_ask(limit_price, max_base).await.unwrap();
|
||||
self.find_order_id_for_client_order_id(self.next_client_order_id - 1)
|
||||
.await
|
||||
}
|
||||
|
||||
|
@ -262,7 +289,7 @@ async fn test_serum_basics() -> Result<(), TransportError> {
|
|||
//
|
||||
// TEST: Place an order
|
||||
//
|
||||
let (order_id, _) = order_placer.bid(1.0, 100).await.unwrap();
|
||||
let (order_id, _) = order_placer.bid_maker(1.0, 100).await.unwrap();
|
||||
check_prev_instruction_post_health(&solana, account).await;
|
||||
|
||||
let native0 = account_position(solana, account, base_token.bank).await;
|
||||
|
@ -362,7 +389,7 @@ async fn test_serum_loan_origination_fees() -> Result<(), TransportError> {
|
|||
// TEST: Placing and canceling an order does not take loan origination fees even if borrows are needed
|
||||
//
|
||||
{
|
||||
let (bid_order_id, _) = order_placer.bid(1.0, 200000).await.unwrap();
|
||||
let (bid_order_id, _) = order_placer.bid_maker(1.0, 200000).await.unwrap();
|
||||
let (ask_order_id, _) = order_placer.ask(2.0, 200000).await.unwrap();
|
||||
|
||||
let o = order_placer.mango_serum_orders().await;
|
||||
|
@ -377,7 +404,7 @@ async fn test_serum_loan_origination_fees() -> Result<(), TransportError> {
|
|||
assert_eq!(o.quote_borrows_without_fee, 19999);
|
||||
|
||||
// placing new, slightly larger orders increases the borrow_without_fee amount only by a small amount
|
||||
let (bid_order_id, _) = order_placer.bid(1.0, 210000).await.unwrap();
|
||||
let (bid_order_id, _) = order_placer.bid_maker(1.0, 210000).await.unwrap();
|
||||
let (ask_order_id, _) = order_placer.ask(2.0, 300000).await.unwrap();
|
||||
|
||||
let o = order_placer.mango_serum_orders().await;
|
||||
|
@ -429,7 +456,10 @@ async fn test_serum_loan_origination_fees() -> Result<(), TransportError> {
|
|||
.collected_fees_native;
|
||||
|
||||
// account2 has an order on the book
|
||||
order_placer2.bid(1.0, bid_amount as u64).await.unwrap();
|
||||
order_placer2
|
||||
.bid_maker(1.0, bid_amount as u64)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// account takes
|
||||
order_placer.ask(1.0, ask_amount as u64).await.unwrap();
|
||||
|
@ -566,7 +596,7 @@ async fn test_serum_settle_v1() -> Result<(), TransportError> {
|
|||
let base2_start = account_position(solana, account2, base_bank).await;
|
||||
|
||||
// account2 has an order on the book, account takes
|
||||
order_placer2.bid(1.0, amount as u64).await.unwrap();
|
||||
order_placer2.bid_maker(1.0, amount as u64).await.unwrap();
|
||||
order_placer.ask(1.0, amount as u64).await.unwrap();
|
||||
|
||||
context
|
||||
|
@ -663,7 +693,7 @@ async fn test_serum_settle_v2_to_dao() -> Result<(), TransportError> {
|
|||
let base2_start = account_position(solana, account2, base_bank).await;
|
||||
|
||||
// account2 has an order on the book, account takes
|
||||
order_placer2.bid(1.0, amount as u64).await.unwrap();
|
||||
order_placer2.bid_maker(1.0, amount as u64).await.unwrap();
|
||||
order_placer.ask(1.0, amount as u64).await.unwrap();
|
||||
|
||||
context
|
||||
|
@ -756,7 +786,7 @@ async fn test_serum_settle_v2_to_account() -> Result<(), TransportError> {
|
|||
let base2_start = account_position(solana, account2, base_bank).await;
|
||||
|
||||
// account2 has an order on the book, account takes
|
||||
order_placer2.bid(1.0, amount as u64).await.unwrap();
|
||||
order_placer2.bid_maker(1.0, amount as u64).await.unwrap();
|
||||
order_placer.ask(1.0, amount as u64).await.unwrap();
|
||||
|
||||
context
|
||||
|
@ -805,6 +835,173 @@ async fn test_serum_settle_v2_to_account() -> Result<(), TransportError> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_serum_reduce_only_borrows() -> Result<(), TransportError> {
|
||||
let mut test_builder = TestContextBuilder::new();
|
||||
test_builder.test().set_compute_max_units(95_000); // Serum3PlaceOrder needs 92.8k
|
||||
let context = test_builder.start_default().await;
|
||||
let solana = &context.solana.clone();
|
||||
|
||||
//
|
||||
// SETUP: Create a group, accounts, market etc
|
||||
//
|
||||
let deposit_amount = 1000;
|
||||
let CommonSetup {
|
||||
group_with_tokens,
|
||||
base_token,
|
||||
mut order_placer,
|
||||
..
|
||||
} = common_setup(&context, deposit_amount).await;
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
TokenMakeReduceOnly {
|
||||
group: group_with_tokens.group,
|
||||
admin: group_with_tokens.admin,
|
||||
mint: base_token.mint.pubkey,
|
||||
reduce_only: 2,
|
||||
force_close: false,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
//
|
||||
// TEST: Cannot borrow tokens when bank is reduce only
|
||||
//
|
||||
|
||||
let err = order_placer.try_ask(1.0, 1100).await;
|
||||
assert_mango_error(&err, MangoError::TokenInReduceOnlyMode.into(), "".into());
|
||||
|
||||
order_placer.try_ask(0.5, 500).await.unwrap();
|
||||
|
||||
let err = order_placer.try_ask(1.0, 600).await;
|
||||
assert_mango_error(&err, MangoError::TokenInReduceOnlyMode.into(), "".into());
|
||||
|
||||
order_placer.try_ask(2.0, 500).await.unwrap();
|
||||
|
||||
let err = order_placer.try_ask(1.0, 100).await;
|
||||
assert_mango_error(&err, MangoError::TokenInReduceOnlyMode.into(), "".into());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_serum_reduce_only_deposits1() -> Result<(), TransportError> {
|
||||
let mut test_builder = TestContextBuilder::new();
|
||||
test_builder.test().set_compute_max_units(95_000); // Serum3PlaceOrder needs 92.8k
|
||||
let context = test_builder.start_default().await;
|
||||
let solana = &context.solana.clone();
|
||||
|
||||
//
|
||||
// SETUP: Create a group, accounts, market etc
|
||||
//
|
||||
let deposit_amount = 1000;
|
||||
let CommonSetup {
|
||||
group_with_tokens,
|
||||
base_token,
|
||||
mut order_placer,
|
||||
mut order_placer2,
|
||||
..
|
||||
} = common_setup(&context, deposit_amount).await;
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
TokenMakeReduceOnly {
|
||||
group: group_with_tokens.group,
|
||||
admin: group_with_tokens.admin,
|
||||
mint: base_token.mint.pubkey,
|
||||
reduce_only: 1,
|
||||
force_close: false,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
//
|
||||
// TEST: Cannot buy tokens when deposits are already >0
|
||||
//
|
||||
|
||||
// fails to place on the book
|
||||
let err = order_placer.try_bid(1.0, 1000, false).await;
|
||||
assert_mango_error(&err, MangoError::TokenInReduceOnlyMode.into(), "".into());
|
||||
|
||||
// also fails as a taker order
|
||||
order_placer2.ask(1.0, 500).await.unwrap();
|
||||
let err = order_placer.try_bid(1.0, 100, true).await;
|
||||
assert_mango_error(&err, MangoError::TokenInReduceOnlyMode.into(), "".into());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_serum_reduce_only_deposits2() -> Result<(), TransportError> {
|
||||
let mut test_builder = TestContextBuilder::new();
|
||||
test_builder.test().set_compute_max_units(95_000); // Serum3PlaceOrder needs 92.8k
|
||||
let context = test_builder.start_default().await;
|
||||
let solana = &context.solana.clone();
|
||||
|
||||
//
|
||||
// SETUP: Create a group, accounts, market etc
|
||||
//
|
||||
let deposit_amount = 1000;
|
||||
let CommonSetup {
|
||||
group_with_tokens,
|
||||
base_token,
|
||||
mut order_placer,
|
||||
mut order_placer2,
|
||||
..
|
||||
} = common_setup(&context, deposit_amount).await;
|
||||
|
||||
// Give account some base token borrows (-500)
|
||||
send_tx(
|
||||
solana,
|
||||
TokenWithdrawInstruction {
|
||||
amount: 1500,
|
||||
allow_borrow: true,
|
||||
account: order_placer.account,
|
||||
owner: order_placer.owner,
|
||||
token_account: context.users[0].token_accounts[1],
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
//
|
||||
// TEST: Cannot buy tokens when deposits are already >0
|
||||
//
|
||||
send_tx(
|
||||
solana,
|
||||
TokenMakeReduceOnly {
|
||||
group: group_with_tokens.group,
|
||||
admin: group_with_tokens.admin,
|
||||
mint: base_token.mint.pubkey,
|
||||
reduce_only: 1,
|
||||
force_close: false,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// cannot place a large order on the book that would deposit too much
|
||||
let err = order_placer.try_bid(1.0, 600, false).await;
|
||||
assert_mango_error(&err, MangoError::TokenInReduceOnlyMode.into(), "".into());
|
||||
|
||||
// a small order is fine
|
||||
order_placer.try_bid(1.0, 100, false).await.unwrap();
|
||||
|
||||
// taking some is fine too
|
||||
order_placer2.ask(1.0, 800).await.unwrap();
|
||||
order_placer.try_bid(1.0, 100, true).await.unwrap();
|
||||
|
||||
// the limit for orders is reduced now, 100 received, 100 on the book
|
||||
let err = order_placer.try_bid(1.0, 400, true).await;
|
||||
assert_mango_error(&err, MangoError::TokenInReduceOnlyMode.into(), "".into());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct CommonSetup {
|
||||
group_with_tokens: GroupWithTokens,
|
||||
serum_market_cookie: SpotMarketCookie,
|
||||
|
|
|
@ -81,7 +81,7 @@ export class Serum3Market {
|
|||
this.name === 'USDT/USDC'
|
||||
? { maker: -0.5, taker: 1 }
|
||||
: { maker: -2, taker: 4 };
|
||||
return taker ? ratesBps.maker * 0.0001 : ratesBps.taker * 0.0001;
|
||||
return taker ? ratesBps.taker * 0.0001 : ratesBps.maker * 0.0001;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1609,10 +1609,12 @@ export class MangoClient {
|
|||
const maxBaseQuantity = serum3MarketExternal.baseSizeNumberToLots(size);
|
||||
const isTaker = orderType !== Serum3OrderType.postOnly;
|
||||
const maxQuoteQuantity = new BN(
|
||||
serum3MarketExternal.decoded.quoteLotSize.toNumber() *
|
||||
(1 + Math.max(serum3Market.getFeeRates(isTaker), 0)) *
|
||||
serum3MarketExternal.baseSizeNumberToLots(size).toNumber() *
|
||||
serum3MarketExternal.priceNumberToLots(price).toNumber(),
|
||||
Math.ceil(
|
||||
serum3MarketExternal.decoded.quoteLotSize.toNumber() *
|
||||
(1 + Math.max(serum3Market.getFeeRates(isTaker), 0)) *
|
||||
serum3MarketExternal.baseSizeNumberToLots(size).toNumber() *
|
||||
serum3MarketExternal.priceNumberToLots(price).toNumber(),
|
||||
),
|
||||
);
|
||||
|
||||
const payerTokenIndex = ((): TokenIndex => {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
export type MangoV4 = {
|
||||
"version": "0.14.0",
|
||||
"version": "0.15.0",
|
||||
"name": "mango_v4",
|
||||
"instructions": [
|
||||
{
|
||||
|
@ -7881,6 +7881,78 @@ export type MangoV4 = {
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "PerpUpdateFundingLogV2",
|
||||
"fields": [
|
||||
{
|
||||
"name": "mangoGroup",
|
||||
"type": "publicKey",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "marketIndex",
|
||||
"type": "u16",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "longFunding",
|
||||
"type": "i128",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "shortFunding",
|
||||
"type": "i128",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "price",
|
||||
"type": "i128",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "oracleSlot",
|
||||
"type": "u64",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "oracleConfidence",
|
||||
"type": "i128",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "oracleType",
|
||||
"type": {
|
||||
"defined": "OracleType"
|
||||
},
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "stablePrice",
|
||||
"type": "i128",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "feesAccrued",
|
||||
"type": "i128",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "feesSettled",
|
||||
"type": "i128",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "openInterest",
|
||||
"type": "i64",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "instantaneousFundingRate",
|
||||
"type": "i128",
|
||||
"index": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "UpdateIndexLog",
|
||||
"fields": [
|
||||
|
@ -9009,12 +9081,17 @@ export type MangoV4 = {
|
|||
"code": 6046,
|
||||
"name": "TokenInForceClose",
|
||||
"msg": "token is in force close"
|
||||
},
|
||||
{
|
||||
"code": 6047,
|
||||
"name": "InvalidHealthAccountCount",
|
||||
"msg": "incorrect number of health accounts"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export const IDL: MangoV4 = {
|
||||
"version": "0.14.0",
|
||||
"version": "0.15.0",
|
||||
"name": "mango_v4",
|
||||
"instructions": [
|
||||
{
|
||||
|
@ -16896,6 +16973,78 @@ export const IDL: MangoV4 = {
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "PerpUpdateFundingLogV2",
|
||||
"fields": [
|
||||
{
|
||||
"name": "mangoGroup",
|
||||
"type": "publicKey",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "marketIndex",
|
||||
"type": "u16",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "longFunding",
|
||||
"type": "i128",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "shortFunding",
|
||||
"type": "i128",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "price",
|
||||
"type": "i128",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "oracleSlot",
|
||||
"type": "u64",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "oracleConfidence",
|
||||
"type": "i128",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "oracleType",
|
||||
"type": {
|
||||
"defined": "OracleType"
|
||||
},
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "stablePrice",
|
||||
"type": "i128",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "feesAccrued",
|
||||
"type": "i128",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "feesSettled",
|
||||
"type": "i128",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "openInterest",
|
||||
"type": "i64",
|
||||
"index": false
|
||||
},
|
||||
{
|
||||
"name": "instantaneousFundingRate",
|
||||
"type": "i128",
|
||||
"index": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "UpdateIndexLog",
|
||||
"fields": [
|
||||
|
@ -18024,6 +18173,11 @@ export const IDL: MangoV4 = {
|
|||
"code": 6046,
|
||||
"name": "TokenInForceClose",
|
||||
"msg": "token is in force close"
|
||||
},
|
||||
{
|
||||
"code": 6047,
|
||||
"name": "InvalidHealthAccountCount",
|
||||
"msg": "incorrect number of health accounts"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue