Merge branch 'main' into deploy

This commit is contained in:
microwavedcola1 2023-05-11 13:31:51 +02:00
commit 59fe0ffcd9
23 changed files with 774 additions and 101 deletions

View File

@ -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

2
Cargo.lock generated
View File

@ -3005,7 +3005,7 @@ dependencies = [
[[package]]
name = "mango-v4"
version = "0.14.0"
version = "0.15.0"
dependencies = [
"anchor-lang",
"anchor-spl",

View File

@ -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"
}
]
}

View File

@ -1,6 +1,6 @@
[package]
name = "mango-v4"
version = "0.14.0"
version = "0.15.0"
description = "Created with Anchor"
edition = "2021"

View File

@ -99,6 +99,8 @@ pub enum MangoError {
HealthRegionBadInnerInstruction,
#[msg("token is in force close")]
TokenInForceClose,
#[msg("incorrect number of health accounts")]
InvalidHealthAccountCount,
}
impl MangoError {

View File

@ -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)?,

View File

@ -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> {

View File

@ -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
);
}

View File

@ -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()?;

View File

@ -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(())
}

View File

@ -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(())
}

View File

@ -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],
};

View File

@ -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,

View File

@ -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,

View File

@ -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,
},
)
}
})
}

View File

@ -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);

View File

@ -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);
}
}

View File

@ -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(),

View File

@ -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();

View File

@ -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,

View File

@ -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;
}
/**

View File

@ -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 => {

View File

@ -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"
}
]
};