Net borrow limits: Limit in quote, not native (#312)

* Net borrow limits: Limit in quote, not native
* make perp settle limit disableable
This commit is contained in:
Christian Kamm 2022-12-02 12:24:11 +01:00 committed by GitHub
parent cf34a5b4b7
commit c8f1f3c821
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 453 additions and 274 deletions

View File

@ -602,6 +602,7 @@ impl MangoClient {
open_orders,
payer_bank: payer_mint_info.first_bank(),
payer_vault: payer_mint_info.first_vault(),
payer_oracle: payer_mint_info.oracle,
serum_market: s3.market.address,
serum_program: s3.market.market.serum_program,
serum_market_external: s3.market.market.serum_market_external,

View File

@ -360,8 +360,8 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>(
let health_cache = new_health_cache(&account.borrow(), &retriever)?;
let pre_health = account.check_health_pre(&health_cache)?;
// Prices for logging
let mut prices = vec![];
// Prices for logging and net borrow checks
let mut oracle_prices = vec![];
for change in &changes {
let (_, oracle_price) = retriever.bank_and_oracle(
&account.fixed.group,
@ -369,7 +369,7 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>(
change.token_index,
)?;
prices.push(oracle_price);
oracle_prices.push(oracle_price);
}
// Drop retriever as mut bank below uses health_ais
drop(retriever);
@ -377,7 +377,7 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>(
// Apply the vault diffs to the bank positions
let mut deactivated_token_positions = vec![];
let mut token_loan_details = Vec::with_capacity(changes.len());
for (change, price) in changes.iter().zip(prices.iter()) {
for (change, oracle_price) in changes.iter().zip(oracle_prices.iter()) {
let mut bank = health_ais[change.bank_index].load_mut::<Bank>()?;
let position = account.token_position_mut_by_raw_index(change.raw_token_index);
let native = position.native(&bank);
@ -396,6 +396,7 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>(
position,
cm!(change.amount - loan_origination_fee),
Clock::get()?.unix_timestamp.try_into().unwrap(),
*oracle_price,
)?;
if !is_active {
deactivated_token_positions.push(change.raw_token_index);
@ -411,7 +412,7 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>(
loan_origination_fee: loan_origination_fee.to_bits(),
deposit_index: bank.deposit_index.to_bits(),
borrow_index: bank.borrow_index.to_bits(),
price: price.to_bits(),
price: oracle_price.to_bits(),
});
emit!(TokenBalanceLog {

View File

@ -70,7 +70,7 @@ pub fn perp_create_market(
settle_fee_amount_threshold: f32,
settle_fee_fraction_low_health: f32,
settle_pnl_limit_factor: f32,
settle_pnl_limit_factor_window_size_ts: u64,
settle_pnl_limit_window_size_ts: u64,
) -> Result<()> {
// Settlement tokens that aren't USDC aren't fully implemented, the main missing steps are:
// - In health: the perp health needs to be adjusted by the settlement token weights.
@ -127,7 +127,7 @@ pub fn perp_create_market(
settle_fee_fraction_low_health,
stable_price_model: StablePriceModel::default(),
settle_pnl_limit_factor,
settle_pnl_limit_factor_window_size_ts,
settle_pnl_limit_window_size_ts: settle_pnl_limit_window_size_ts,
reserved: [0; 1944],
};

View File

@ -48,7 +48,7 @@ pub fn perp_edit_market(
stable_price_delay_growth_limit_opt: Option<f32>,
stable_price_growth_limit_opt: Option<f32>,
settle_pnl_limit_factor_opt: Option<f32>,
settle_pnl_limit_factor_window_size_ts_opt: Option<u64>,
settle_pnl_limit_window_size_ts_opt: Option<u64>,
) -> Result<()> {
let mut perp_market = ctx.accounts.perp_market.load_mut()?;
@ -167,9 +167,8 @@ pub fn perp_edit_market(
if let Some(settle_pnl_limit_factor_opt) = settle_pnl_limit_factor_opt {
perp_market.settle_pnl_limit_factor = settle_pnl_limit_factor_opt;
}
if let Some(settle_pnl_limit_factor_window_size_ts) = settle_pnl_limit_factor_window_size_ts_opt
{
perp_market.settle_pnl_limit_factor_window_size_ts = settle_pnl_limit_factor_window_size_ts;
if let Some(settle_pnl_limit_window_size_ts) = settle_pnl_limit_window_size_ts_opt {
perp_market.settle_pnl_limit_window_size_ts = settle_pnl_limit_window_size_ts;
}
emit!(PerpMarketMetaDataLog {

View File

@ -111,6 +111,7 @@ pub fn perp_settle_fees(ctx: Context<PerpSettleFees>, max_settle_amount: u64) ->
token_position,
settlement,
Clock::get()?.unix_timestamp.try_into().unwrap(),
oracle_price,
)?;
// Update the settled balance on the market itself
perp_market.fees_settled = cm!(perp_market.fees_settled + settlement);

View File

@ -130,7 +130,7 @@ pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>) -> Result<()> {
a_perp_position.update_and_get_used_settle_limit(&perp_market, now_ts);
b_perp_position.update_and_get_used_settle_limit(&perp_market, now_ts);
let a_settleable_pnl = {
let a_settleable_pnl = if perp_market.settle_pnl_limit_factor >= 0.0 {
let realized_pnl = a_perp_position.realized_pnl_native;
let unrealized_pnl = cm!(a_pnl - realized_pnl);
let a_base_lots = I80F48::from(a_perp_position.base_position_lots());
@ -146,6 +146,8 @@ pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>) -> Result<()> {
a_pnl
.min(cm!(realized_pnl + unrealized_pnl_capped_for_window))
.max(I80F48::ZERO)
} else {
a_pnl
};
require!(
@ -219,7 +221,7 @@ pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>) -> Result<()> {
let a_token_position = account_a.token_position_mut(settle_token_index)?.0;
let b_token_position = account_b.token_position_mut(settle_token_index)?.0;
bank.deposit(a_token_position, cm!(settlement - fee), now_ts)?;
bank.withdraw_with_fee(b_token_position, settlement, now_ts)?;
bank.withdraw_with_fee(b_token_position, settlement, now_ts, oracle_price)?;
emit!(TokenBalanceLog {
mango_group: ctx.accounts.group.key(),

View File

@ -217,6 +217,7 @@ pub fn serum3_liq_force_cancel_orders(
&mut base_bank,
after_base_vault,
before_base_vault,
None, // guaranteed to deposit into bank
)?
.adjust_health_cache(&mut health_cache, &base_bank)?;
apply_vault_difference(
@ -226,6 +227,7 @@ pub fn serum3_liq_force_cancel_orders(
&mut quote_bank,
after_quote_vault,
before_quote_vault,
None, // guaranteed to deposit into bank
)?
.adjust_health_cache(&mut health_cache, &quote_bank)?;

View File

@ -1,3 +1,4 @@
use crate::accounts_zerocopy::AccountInfoRef;
use crate::error::*;
use crate::logs::{Serum3OpenOrdersBalanceLog, TokenBalanceLog};
@ -190,6 +191,9 @@ pub struct Serum3PlaceOrder<'info> {
/// The bank vault that pays for the order, if necessary
#[account(mut)]
pub payer_vault: Box<Account<'info, TokenAccount>>,
/// CHECK: The oracle can be one of several different account types
#[account(address = payer_bank.load()?.oracle)]
pub payer_oracle: UncheckedAccount<'info>,
pub token_program: Program<'info, Token>,
}
@ -342,6 +346,8 @@ pub fn serum3_place_order(
// Charge the difference in vault balance to the user's account
let mut payer_bank = ctx.accounts.payer_bank.load_mut()?;
let vault_difference = {
let oracle_price =
payer_bank.oracle_price(&AccountInfoRef::borrow(&ctx.accounts.payer_oracle)?, None)?;
apply_vault_difference(
ctx.accounts.account.key(),
&mut account.borrow_mut(),
@ -349,6 +355,7 @@ pub fn serum3_place_order(
&mut payer_bank,
after_vault,
before_vault,
Some(oracle_price),
)?
};
@ -425,16 +432,23 @@ pub fn apply_vault_difference(
bank: &mut Bank,
vault_after: u64,
vault_before: u64,
oracle_price: Option<I80F48>,
) -> Result<VaultDifference> {
let needed_change = cm!(I80F48::from(vault_after) - I80F48::from(vault_before));
let (position, _) = account.token_position_mut(bank.token_index)?;
let native_before = position.native(bank);
bank.change_without_fee(
position,
needed_change,
Clock::get()?.unix_timestamp.try_into().unwrap(),
)?;
let now_ts = Clock::get()?.unix_timestamp.try_into().unwrap();
if needed_change >= 0 {
bank.deposit(position, needed_change, now_ts)?;
} else {
bank.withdraw_without_fee(
position,
-needed_change,
now_ts,
oracle_price.unwrap(), // required for withdraws
)?;
}
let native_after = position.native(bank);
let native_change = cm!(native_after - native_before);
let new_borrows = native_change

View File

@ -165,6 +165,10 @@ pub fn serum3_settle_funds(ctx: Context<Serum3SettleFunds>) -> Result<()> {
&mut base_bank,
after_base_vault,
before_base_vault,
// Since after >= before, we know this can be a deposit
// and no net borrow check will be necessary, meaning
// we don't need an oracle price.
None,
)?;
apply_vault_difference(
ctx.accounts.account.key(),
@ -173,6 +177,7 @@ pub fn serum3_settle_funds(ctx: Context<Serum3SettleFunds>) -> Result<()> {
&mut quote_bank,
after_quote_vault,
before_quote_vault,
None,
)?;
}

View File

@ -50,8 +50,10 @@ pub fn token_edit(
stable_price_delay_growth_limit_opt: Option<f32>,
stable_price_growth_limit_opt: Option<f32>,
min_vault_to_deposits_ratio_opt: Option<f64>,
net_borrows_limit_native_opt: Option<i64>,
net_borrows_limit_quote_opt: Option<i64>,
net_borrows_window_size_ts_opt: Option<u64>,
reset_stable_price: bool,
reset_net_borrow_limit: bool,
) -> Result<()> {
let mut mint_info = ctx.accounts.mint_info.load_mut()?;
mint_info.verify_banks_ais(ctx.remaining_accounts)?;
@ -75,8 +77,9 @@ pub fn token_edit(
if let Some(oracle) = oracle_opt {
bank.oracle = oracle;
mint_info.oracle = oracle;
require_keys_eq!(oracle, ctx.accounts.oracle.key());
}
if reset_stable_price {
require_keys_eq!(bank.oracle, ctx.accounts.oracle.key());
let oracle_price =
bank.oracle_price(&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?, None)?;
bank.stable_price_model.reset_to_price(
@ -148,12 +151,16 @@ pub fn token_edit(
if let Some(min_vault_to_deposits_ratio) = min_vault_to_deposits_ratio_opt {
bank.min_vault_to_deposits_ratio = min_vault_to_deposits_ratio;
}
if let Some(net_borrows_limit_native) = net_borrows_limit_native_opt {
bank.net_borrows_limit_native = net_borrows_limit_native;
if let Some(net_borrows_limit_quote) = net_borrows_limit_quote_opt {
bank.net_borrows_limit_quote = net_borrows_limit_quote;
}
if let Some(net_borrows_window_size_ts) = net_borrows_window_size_ts_opt {
bank.net_borrows_window_size_ts = net_borrows_window_size_ts;
}
if reset_net_borrow_limit {
bank.net_borrows_in_window = 0;
bank.last_net_borrows_window_start_ts = 0;
}
// unchanged -
// dust

View File

@ -188,7 +188,7 @@ pub fn token_liq_bankruptcy(
let (liqor_liab, liqor_liab_raw_token_index, _) =
liqor.ensure_token_position(liab_token_index)?;
let (liqor_liab_active, loan_origination_fee) =
liab_bank.withdraw_with_fee(liqor_liab, liab_transfer, now_ts)?;
liab_bank.withdraw_with_fee(liqor_liab, liab_transfer, now_ts, liab_price)?;
// liqor liab
emit!(TokenBalanceLog {

View File

@ -164,6 +164,7 @@ pub fn token_liq_with_token(
liqor_liab_position,
liab_transfer,
Clock::get()?.unix_timestamp.try_into().unwrap(),
liab_price,
)?;
let liqor_liab_indexed_position = liqor_liab_position.indexed_position;
let liqee_liab_native_after = liqee_liab_position.native(liab_bank);
@ -182,6 +183,7 @@ pub fn token_liq_with_token(
liqee_asset_position,
asset_transfer,
Clock::get()?.unix_timestamp.try_into().unwrap(),
asset_price,
)?;
let liqee_asset_indexed_position = liqee_asset_position.indexed_position;
let liqee_assets_native_after = liqee_asset_position.native(asset_bank);

View File

@ -92,7 +92,7 @@ pub fn token_register(
liquidation_fee: f32,
min_vault_to_deposits_ratio: f64,
net_borrows_window_size_ts: u64,
net_borrows_limit_native: i64,
net_borrows_limit_quote: i64,
) -> Result<()> {
// Require token 0 to be in the insurance token
if token_index == QUOTE_TOKEN_INDEX {
@ -149,8 +149,8 @@ pub fn token_register(
net_borrows_window_size_ts,
last_net_borrows_window_start_ts: now_ts / net_borrows_window_size_ts
* net_borrows_window_size_ts,
net_borrows_limit_native,
net_borrows_window_native: 0,
net_borrows_limit_quote,
net_borrows_in_window: 0,
borrow_limit_quote: f64::MAX,
collateral_limit_quote: f64::MAX,
reserved: [0; 2120],

View File

@ -124,8 +124,8 @@ pub fn token_register_trustless(
net_borrows_window_size_ts,
last_net_borrows_window_start_ts: now_ts / net_borrows_window_size_ts
* net_borrows_window_size_ts,
net_borrows_limit_native: 1_000_000,
net_borrows_window_native: 0,
net_borrows_limit_quote: 1_000_000_000_000, // 1M USD
net_borrows_in_window: 0,
borrow_limit_quote: f64::MAX,
collateral_limit_quote: f64::MAX,
reserved: [0; 2120],

View File

@ -98,11 +98,18 @@ pub fn token_withdraw(ctx: Context<TokenWithdraw>, amount: u64, allow_borrow: bo
let amount_i80f48 = I80F48::from(amount);
let now_slot = Clock::get()?.slot;
let oracle_price = bank.oracle_price(
&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?,
Some(now_slot),
)?;
// Update the bank and position
let (position_is_active, loan_origination_fee) = bank.withdraw_with_fee(
position,
amount_i80f48,
Clock::get()?.unix_timestamp.try_into().unwrap(),
oracle_price,
)?;
// Provide a readable error message in case the vault doesn't have enough tokens
@ -123,11 +130,6 @@ pub fn token_withdraw(ctx: Context<TokenWithdraw>, amount: u64, allow_borrow: bo
)?;
let native_position_after = position.native(&bank);
let now_slot = Clock::get()?.slot;
let oracle_price = bank.oracle_price(
&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?,
Some(now_slot),
)?;
emit!(TokenBalanceLog {
mango_group: ctx.accounts.group.key(),

View File

@ -76,7 +76,7 @@ pub mod mango_v4 {
liquidation_fee: f32,
min_vault_to_deposits_ratio: f64,
net_borrows_window_size_ts: u64,
net_borrows_limit_native: i64,
net_borrows_limit_quote: i64,
) -> Result<()> {
instructions::token_register(
ctx,
@ -93,7 +93,7 @@ pub mod mango_v4 {
liquidation_fee,
min_vault_to_deposits_ratio,
net_borrows_window_size_ts,
net_borrows_limit_native,
net_borrows_limit_quote,
)
}
@ -123,8 +123,10 @@ pub mod mango_v4 {
stable_price_delay_growth_limit_opt: Option<f32>,
stable_price_growth_limit_opt: Option<f32>,
min_vault_to_deposits_ratio_opt: Option<f64>,
net_borrows_limit_native_opt: Option<i64>,
net_borrows_limit_quote_opt: Option<i64>,
net_borrows_window_size_ts_opt: Option<u64>,
reset_stable_price: bool,
reset_net_borrow_limit: bool,
) -> Result<()> {
instructions::token_edit(
ctx,
@ -143,8 +145,10 @@ pub mod mango_v4 {
stable_price_delay_growth_limit_opt,
stable_price_growth_limit_opt,
min_vault_to_deposits_ratio_opt,
net_borrows_limit_native_opt,
net_borrows_limit_quote_opt,
net_borrows_window_size_ts_opt,
reset_stable_price,
reset_net_borrow_limit,
)
}
@ -425,7 +429,7 @@ pub mod mango_v4 {
settle_fee_fraction_low_health: f32,
settle_token_index: TokenIndex,
settle_pnl_limit_factor: f32,
settle_pnl_limit_factor_window_size_ts: u64,
settle_pnl_limit_window_size_ts: u64,
) -> Result<()> {
instructions::perp_create_market(
ctx,
@ -453,7 +457,7 @@ pub mod mango_v4 {
settle_fee_amount_threshold,
settle_fee_fraction_low_health,
settle_pnl_limit_factor,
settle_pnl_limit_factor_window_size_ts,
settle_pnl_limit_window_size_ts,
)
}
@ -483,7 +487,7 @@ pub mod mango_v4 {
stable_price_delay_growth_limit_opt: Option<f32>,
stable_price_growth_limit_opt: Option<f32>,
settle_pnl_limit_factor_opt: Option<f32>,
settle_pnl_limit_factor_window_size_ts: Option<u64>,
settle_pnl_limit_window_size_ts: Option<u64>,
) -> Result<()> {
instructions::perp_edit_market(
ctx,
@ -510,7 +514,7 @@ pub mod mango_v4 {
stable_price_delay_growth_limit_opt,
stable_price_growth_limit_opt,
settle_pnl_limit_factor_opt,
settle_pnl_limit_factor_window_size_ts,
settle_pnl_limit_window_size_ts,
)
}

View File

@ -107,11 +107,17 @@ pub struct Bank {
pub stable_price_model: StablePriceModel,
/// Min fraction of deposits that must remain in the vault when borrowing.
pub min_vault_to_deposits_ratio: f64,
/// Size in seconds of a net borrows window
pub net_borrows_window_size_ts: u64,
/// Timestamp at which the last net borrows window started
pub last_net_borrows_window_start_ts: u64,
pub net_borrows_limit_native: i64,
pub net_borrows_window_native: i64,
/// Net borrow limit per window in quote native; set to -1 to disable.
pub net_borrows_limit_quote: i64,
/// Sum of all deposits and borrows in the last window, in native units.
pub net_borrows_in_window: i64,
/// Soft borrow limit in native quote
///
@ -188,10 +194,10 @@ impl Bank {
oracle_config: existing_bank.oracle_config.clone(),
stable_price_model: StablePriceModel::default(),
min_vault_to_deposits_ratio: existing_bank.min_vault_to_deposits_ratio,
net_borrows_limit_native: existing_bank.net_borrows_limit_native,
net_borrows_limit_quote: existing_bank.net_borrows_limit_quote,
net_borrows_window_size_ts: existing_bank.net_borrows_window_size_ts,
last_net_borrows_window_start_ts: existing_bank.last_net_borrows_window_start_ts,
net_borrows_window_native: 0,
net_borrows_in_window: 0,
borrow_limit_quote: f64::MAX,
collateral_limit_quote: f64::MAX,
reserved: [0; 2120],
@ -251,7 +257,7 @@ impl Bank {
allow_dusting: bool,
now_ts: u64,
) -> Result<bool> {
self.update_net_borrows(-native_amount, now_ts)?;
self.update_net_borrows(-native_amount, now_ts, None)?;
let opening_indexed_position = position.indexed_position;
let result = self.deposit_internal(position, native_amount, allow_dusting)?;
self.update_cumulative_interest(position, opening_indexed_position);
@ -331,6 +337,7 @@ impl Bank {
position: &mut TokenPosition,
native_amount: I80F48,
now_ts: u64,
oracle_price: I80F48,
) -> Result<bool> {
let (position_is_active, _) = self.withdraw_internal_wrapper(
position,
@ -338,6 +345,7 @@ impl Bank {
false,
!position.is_in_use(),
now_ts,
Some(oracle_price),
)?;
Ok(position_is_active)
@ -351,9 +359,17 @@ impl Bank {
position: &mut TokenPosition,
native_amount: I80F48,
now_ts: u64,
oracle_price: I80F48,
) -> Result<bool> {
self.withdraw_internal_wrapper(position, native_amount, false, true, now_ts)
.map(|(not_dusted, _)| not_dusted || position.is_in_use())
self.withdraw_internal_wrapper(
position,
native_amount,
false,
true,
now_ts,
Some(oracle_price),
)
.map(|(not_dusted, _)| not_dusted || position.is_in_use())
}
/// Withdraws `native_amount` while applying the loan origination fee if a borrow is created.
@ -369,8 +385,16 @@ impl Bank {
position: &mut TokenPosition,
native_amount: I80F48,
now_ts: u64,
oracle_price: I80F48,
) -> Result<(bool, I80F48)> {
self.withdraw_internal_wrapper(position, native_amount, true, !position.is_in_use(), now_ts)
self.withdraw_internal_wrapper(
position,
native_amount,
true,
!position.is_in_use(),
now_ts,
Some(oracle_price),
)
}
/// Internal function to withdraw funds
@ -381,6 +405,7 @@ impl Bank {
with_loan_origination_fee: bool,
allow_dusting: bool,
now_ts: u64,
oracle_price: Option<I80F48>,
) -> Result<(bool, I80F48)> {
let opening_indexed_position = position.indexed_position;
let res = self.withdraw_internal(
@ -389,6 +414,7 @@ impl Bank {
with_loan_origination_fee,
allow_dusting,
now_ts,
oracle_price,
);
self.update_cumulative_interest(position, opening_indexed_position);
res
@ -402,6 +428,7 @@ impl Bank {
with_loan_origination_fee: bool,
allow_dusting: bool,
now_ts: u64,
oracle_price: Option<I80F48>,
) -> Result<(bool, I80F48)> {
require_gte!(native_amount, 0);
let native_position = position.native(self);
@ -446,7 +473,7 @@ impl Bank {
// net borrows requires updating in only this case, since other branches of the method deal with
// withdraws and not borrows
self.update_net_borrows(native_amount, now_ts)?;
self.update_net_borrows(native_amount, now_ts, oracle_price)?;
Ok((true, loan_origination_fee))
}
@ -468,6 +495,7 @@ impl Bank {
false,
!position.is_in_use(),
now_ts,
None,
)?;
Ok((position_is_active, loan_origination_fee))
@ -479,11 +507,12 @@ impl Bank {
position: &mut TokenPosition,
native_amount: I80F48,
now_ts: u64,
oracle_price: I80F48,
) -> Result<bool> {
if native_amount >= 0 {
self.deposit(position, native_amount, now_ts)
} else {
self.withdraw_without_fee(position, -native_amount, now_ts)
self.withdraw_without_fee(position, -native_amount, now_ts, oracle_price)
}
}
@ -493,34 +522,53 @@ impl Bank {
position: &mut TokenPosition,
native_amount: I80F48,
now_ts: u64,
oracle_price: I80F48,
) -> Result<(bool, I80F48)> {
if native_amount >= 0 {
Ok((self.deposit(position, native_amount, now_ts)?, I80F48::ZERO))
} else {
self.withdraw_with_fee(position, -native_amount, now_ts)
self.withdraw_with_fee(position, -native_amount, now_ts, oracle_price)
}
}
pub fn update_net_borrows(&mut self, native_amount: I80F48, now_ts: u64) -> Result<()> {
/// Update the bank's net_borrows fields.
///
/// If oracle_price is set, also do a net borrows check and error if the threshold is exceeded.
pub fn update_net_borrows(
&mut self,
native_amount: I80F48,
now_ts: u64,
oracle_price: Option<I80F48>,
) -> Result<()> {
let in_new_window =
now_ts >= self.last_net_borrows_window_start_ts + self.net_borrows_window_size_ts;
self.net_borrows_window_native = if in_new_window {
self.net_borrows_in_window = if in_new_window {
// reset to latest window
self.last_net_borrows_window_start_ts =
now_ts / self.net_borrows_window_size_ts * self.net_borrows_window_size_ts;
native_amount.checked_to_num::<i64>().unwrap()
} else {
cm!(self.net_borrows_window_native + native_amount.checked_to_num().unwrap())
cm!(self.net_borrows_in_window + native_amount.checked_to_num().unwrap())
};
if self.net_borrows_window_native > self.net_borrows_limit_native {
return err!(MangoError::BankNetBorrowsLimitReached).with_context(|| {
format!(
"net_borrows_window_native ({:?}) exceeds net_borrows_limit_native ({:?}) for last_net_borrows_window_start_ts ({:?}) ",
self.net_borrows_window_native, self.net_borrows_limit_native, self.last_net_borrows_window_start_ts
)
});
if native_amount < 0 || self.net_borrows_limit_quote < 0 {
return Ok(());
}
if let Some(oracle_price) = oracle_price {
let price = oracle_price.max(self.stable_price());
let net_borrows_quote = price
.checked_mul_int(self.net_borrows_in_window.into())
.unwrap();
if net_borrows_quote > self.net_borrows_limit_quote {
return err!(MangoError::BankNetBorrowsLimitReached).with_context(|| {
format!(
"net_borrows_in_window ({:?}) exceeds net_borrows_limit_quote ({:?}) for last_net_borrows_window_start_ts ({:?}) ",
self.net_borrows_in_window, self.net_borrows_limit_quote, self.last_net_borrows_window_start_ts
)
});
}
}
Ok(())
@ -819,7 +867,7 @@ mod tests {
let mut bank = Bank::zeroed();
bank.net_borrows_window_size_ts = 1; // dummy
bank.net_borrows_limit_native = i64::MAX; // max since we don't want this to interfere
bank.net_borrows_limit_quote = i64::MAX; // max since we don't want this to interfere
bank.deposit_index = I80F48::from_num(100.0);
bank.borrow_index = I80F48::from_num(10.0);
bank.loan_origination_fee_rate = I80F48::from_num(0.1);
@ -858,7 +906,9 @@ mod tests {
let change = I80F48::from(change);
let dummy_now_ts = 1 as u64;
let (is_active, _) = bank.change_with_fee(&mut account, change, dummy_now_ts)?;
let dummy_price = I80F48::ZERO;
let (is_active, _) =
bank.change_with_fee(&mut account, change, dummy_now_ts, dummy_price)?;
let mut expected_native = start_native + change;
if expected_native >= 0.0 && expected_native < 1.0 && !is_in_use {

View File

@ -758,6 +758,7 @@ mod tests {
account.ensure_token_position(4).unwrap().0,
I80F48::from(10),
DUMMY_NOW_TS,
DUMMY_PRICE,
)
.unwrap();
@ -841,6 +842,7 @@ mod tests {
account.ensure_token_position(1).unwrap().0,
I80F48::from(testcase.token1),
DUMMY_NOW_TS,
DUMMY_PRICE,
)
.unwrap();
bank2
@ -849,6 +851,7 @@ mod tests {
account.ensure_token_position(4).unwrap().0,
I80F48::from(testcase.token2),
DUMMY_NOW_TS,
DUMMY_PRICE,
)
.unwrap();
bank3
@ -857,6 +860,7 @@ mod tests {
account.ensure_token_position(5).unwrap().0,
I80F48::from(testcase.token3),
DUMMY_NOW_TS,
DUMMY_PRICE,
)
.unwrap();
for (settings, bank) in testcase

View File

@ -64,7 +64,7 @@ impl HealthCache {
let mut source_bank = source_bank.clone();
source_bank
.withdraw_with_fee(&mut source_position, amount, 0)
.withdraw_with_fee(&mut source_position, amount, 0, I80F48::ZERO)
.unwrap();
let mut target_bank = target_bank.clone();
target_bank
@ -756,6 +756,7 @@ mod tests {
account.ensure_token_position(1).unwrap().0,
I80F48::from(100),
DUMMY_NOW_TS,
DUMMY_PRICE,
)
.unwrap();
@ -838,6 +839,7 @@ mod tests {
account.ensure_token_position(1).unwrap().0,
I80F48::from(100),
DUMMY_NOW_TS,
DUMMY_PRICE,
)
.unwrap();
bank1
@ -846,6 +848,7 @@ mod tests {
account2.ensure_token_position(1).unwrap().0,
I80F48::from(-100),
DUMMY_NOW_TS,
DUMMY_PRICE,
)
.unwrap();

View File

@ -10,6 +10,7 @@ use std::rc::Rc;
use crate::state::*;
pub const DUMMY_NOW_TS: u64 = 0;
pub const DUMMY_PRICE: I80F48 = I80F48::ZERO;
// Implementing TestAccount directly for ZeroCopy + Owner leads to a conflict
// because OpenOrders may add impls for those in the future.
@ -98,7 +99,7 @@ pub fn mock_bank_and_oracle(
bank.data().collateral_limit_quote = f64::MAX;
bank.data().borrow_limit_quote = f64::MAX;
bank.data().net_borrows_window_size_ts = 1; // dummy
bank.data().net_borrows_limit_native = i64::MAX; // max since we don't want this to interfere
bank.data().net_borrows_limit_quote = i64::MAX; // max since we don't want this to interfere
(bank, oracle)
}

View File

@ -441,7 +441,7 @@ impl PerpPosition {
/// Side-effect: updates the windowing
pub fn update_and_get_used_settle_limit(&mut self, market: &PerpMarket, now_ts: u64) -> i64 {
assert_eq!(self.market_index, market.perp_market_index);
let window_size = market.settle_pnl_limit_factor_window_size_ts;
let window_size = market.settle_pnl_limit_window_size_ts;
let new_window = now_ts >= cm!((self.settle_pnl_limit_window + 1) as u64 * window_size);
if new_window {
self.settle_pnl_limit_window = cm!(now_ts / window_size).try_into().unwrap();

View File

@ -111,8 +111,11 @@ pub struct PerpMarket {
pub stable_price_model: StablePriceModel,
/// Fraction of perp base value that can be settled each window.
/// Set to a negative value to disable the limit.
pub settle_pnl_limit_factor: f32,
pub settle_pnl_limit_factor_window_size_ts: u64,
/// Window size in seconds for the perp settlement limit
pub settle_pnl_limit_window_size_ts: u64,
pub reserved: [u8; 1944],
}
@ -321,7 +324,7 @@ impl PerpMarket {
settle_fee_fraction_low_health: 0.0,
stable_price_model: StablePriceModel::default(),
settle_pnl_limit_factor: 0.2,
settle_pnl_limit_factor_window_size_ts: 24 * 60 * 60,
settle_pnl_limit_window_size_ts: 24 * 60 * 60,
reserved: [0; 1944],
}
}

View File

@ -773,7 +773,7 @@ pub struct TokenRegisterInstruction {
pub liquidation_fee: f32,
pub min_vault_to_deposits_ratio: f64,
pub net_borrows_limit_native: i64,
pub net_borrows_limit_quote: i64,
pub net_borrows_window_size_ts: u64,
pub group: Pubkey,
@ -817,7 +817,7 @@ impl ClientInstruction for TokenRegisterInstruction {
init_liab_weight: self.init_liab_weight,
liquidation_fee: self.liquidation_fee,
min_vault_to_deposits_ratio: self.min_vault_to_deposits_ratio,
net_borrows_limit_native: self.net_borrows_limit_native,
net_borrows_limit_quote: self.net_borrows_limit_quote,
net_borrows_window_size_ts: self.net_borrows_window_size_ts,
};
@ -1062,7 +1062,7 @@ impl ClientInstruction for TokenResetStablePriceModel {
let mint_info: MintInfo = account_loader.load(&mint_info_key).await.unwrap();
let instruction = Self::Instruction {
oracle_opt: Some(mint_info.oracle),
oracle_opt: None,
oracle_config_opt: None,
group_insurance_fund_opt: None,
interest_rate_params_opt: None,
@ -1077,8 +1077,10 @@ impl ClientInstruction for TokenResetStablePriceModel {
stable_price_delay_growth_limit_opt: None,
stable_price_growth_limit_opt: None,
min_vault_to_deposits_ratio_opt: None,
net_borrows_limit_native_opt: None,
net_borrows_limit_quote_opt: None,
net_borrows_window_size_ts_opt: None,
reset_stable_price: true,
reset_net_borrow_limit: false,
};
let accounts = Self::Accounts {
@ -1104,17 +1106,17 @@ impl ClientInstruction for TokenResetStablePriceModel {
}
}
pub struct TokenEditNetBorrows {
pub struct TokenResetNetBorrows {
pub group: Pubkey,
pub admin: TestKeypair,
pub mint: Pubkey,
pub min_vault_to_deposits_ratio_opt: Option<f64>,
pub net_borrows_limit_native_opt: Option<i64>,
pub net_borrows_limit_quote_opt: Option<i64>,
pub net_borrows_window_size_ts_opt: Option<u64>,
}
#[async_trait::async_trait(?Send)]
impl ClientInstruction for TokenEditNetBorrows {
impl ClientInstruction for TokenResetNetBorrows {
type Accounts = mango_v4::accounts::TokenEdit;
type Instruction = mango_v4::instruction::TokenEdit;
async fn to_instruction(
@ -1150,8 +1152,10 @@ impl ClientInstruction for TokenEditNetBorrows {
stable_price_delay_growth_limit_opt: None,
stable_price_growth_limit_opt: None,
min_vault_to_deposits_ratio_opt: self.min_vault_to_deposits_ratio_opt,
net_borrows_limit_native_opt: self.net_borrows_limit_native_opt,
net_borrows_limit_quote_opt: self.net_borrows_limit_quote_opt,
net_borrows_window_size_ts_opt: self.net_borrows_window_size_ts_opt,
reset_stable_price: false,
reset_net_borrow_limit: true,
};
let accounts = Self::Accounts {
@ -1907,17 +1911,18 @@ impl ClientInstruction for Serum3PlaceOrderInstruction {
)
.await;
let (payer_bank, payer_vault) = match self.side {
Serum3Side::Bid => (quote_info.first_bank(), quote_info.first_vault()),
Serum3Side::Ask => (base_info.first_bank(), base_info.first_vault()),
let payer_info = &match self.side {
Serum3Side::Bid => &quote_info,
Serum3Side::Ask => &base_info,
};
let accounts = Self::Accounts {
group: account.fixed.group,
account: self.account,
open_orders,
payer_bank,
payer_vault,
payer_bank: payer_info.first_bank(),
payer_vault: payer_info.first_vault(),
payer_oracle: payer_info.oracle,
serum_market: self.serum_market,
serum_program: serum_market.serum_program,
serum_market_external: serum_market.serum_market_external,
@ -2423,7 +2428,7 @@ pub struct PerpCreateMarketInstruction {
pub settle_fee_amount_threshold: f32,
pub settle_fee_fraction_low_health: f32,
pub settle_pnl_limit_factor: f32,
pub settle_pnl_limit_factor_window_size_ts: u64,
pub settle_pnl_limit_window_size_ts: u64,
}
impl PerpCreateMarketInstruction {
pub async fn with_new_book_and_queue(
@ -2480,7 +2485,7 @@ impl ClientInstruction for PerpCreateMarketInstruction {
settle_fee_amount_threshold: self.settle_fee_amount_threshold,
settle_fee_fraction_low_health: self.settle_fee_fraction_low_health,
settle_pnl_limit_factor: self.settle_pnl_limit_factor,
settle_pnl_limit_factor_window_size_ts: self.settle_pnl_limit_factor_window_size_ts,
settle_pnl_limit_window_size_ts: self.settle_pnl_limit_window_size_ts,
};
let perp_market = Pubkey::find_program_address(
@ -2555,7 +2560,7 @@ impl ClientInstruction for PerpResetStablePriceModel {
stable_price_delay_growth_limit_opt: None,
stable_price_growth_limit_opt: None,
settle_pnl_limit_factor_opt: None,
settle_pnl_limit_factor_window_size_ts: None,
settle_pnl_limit_window_size_ts: None,
};
let accounts = Self::Accounts {

View File

@ -106,7 +106,7 @@ impl<'a> GroupWithTokensConfig {
mint: mint.pubkey,
payer,
min_vault_to_deposits_ratio: 0.2,
net_borrows_limit_native: 1_000_000_000_000,
net_borrows_limit_quote: 1_000_000_000_000,
net_borrows_window_size_ts: 24 * 60 * 60,
},
)

View File

@ -33,78 +33,31 @@ async fn test_bank_utilization_based_borrow_limit() -> Result<(), TransportError
.create(solana)
.await;
let account_0 = send_tx(
solana,
AccountCreateInstruction {
account_num: 0,
token_count: 2,
serum3_count: 0,
perp_count: 0,
perp_oo_count: 0,
group,
owner,
payer,
},
)
.await
.unwrap()
.account;
let account_1 = send_tx(
solana,
AccountCreateInstruction {
account_num: 1,
token_count: 2,
serum3_count: 0,
perp_count: 0,
perp_oo_count: 0,
group,
owner,
payer,
},
)
.await
.unwrap()
.account;
//
// SETUP: Deposit user funds
// SETUP: Prepare accounts
//
{
let deposit_amount = initial_token_deposit;
// account_0 deposits mint_0
send_tx(
solana,
TokenDepositInstruction {
amount: deposit_amount,
account: account_0,
owner,
token_account: payer_mint_accounts[0],
token_authority: payer,
bank_index: 0,
},
)
.await
.unwrap();
solana.advance_clock().await;
// account_1 deposits mint_1
send_tx(
solana,
TokenDepositInstruction {
amount: deposit_amount * 10,
account: account_1,
owner,
token_account: payer_mint_accounts[1],
token_authority: payer,
bank_index: 1,
},
)
.await
.unwrap();
solana.advance_clock().await;
}
let account_0 = create_funded_account(
&solana,
group,
owner,
0,
&context.users[1],
&mints[0..1],
initial_token_deposit,
0,
)
.await;
let account_1 = create_funded_account(
&solana,
group,
owner,
1,
&context.users[1],
&mints[1..2],
initial_token_deposit * 10,
1,
)
.await;
{
let deposit_amount = initial_token_deposit;
@ -187,94 +140,55 @@ async fn test_bank_net_borrows_based_borrow_limit() -> Result<(), TransportError
.create(solana)
.await;
let account_0 = send_tx(
solana,
AccountCreateInstruction {
account_num: 0,
token_count: 2,
serum3_count: 0,
perp_count: 0,
perp_oo_count: 0,
group,
owner,
payer,
},
)
.await
.unwrap()
.account;
let reset_net_borrows = || {
let mint = tokens[0].mint.pubkey;
async move {
send_tx(
solana,
TokenResetNetBorrows {
group,
admin,
mint,
// we want to test net borrow limits in isolation
min_vault_to_deposits_ratio_opt: Some(0.0),
net_borrows_limit_quote_opt: Some(6000),
net_borrows_window_size_ts_opt: Some(1000),
},
)
.await
.unwrap();
}
};
let account_1 = send_tx(
solana,
AccountCreateInstruction {
account_num: 1,
token_count: 2,
serum3_count: 0,
perp_count: 0,
perp_oo_count: 0,
group,
owner,
payer,
},
)
.await
.unwrap()
.account;
{
send_tx(
solana,
TokenEditNetBorrows {
group,
admin,
mint: tokens[0].mint.pubkey,
// we want to test net borrow limits in isolation
min_vault_to_deposits_ratio_opt: Some(0.0),
net_borrows_limit_native_opt: Some(6000),
net_borrows_window_size_ts_opt: Some(3),
},
)
.await
.unwrap();
}
reset_net_borrows().await;
//
// SETUP: Deposit user funds
// SETUP: Prepare accounts
//
{
// account_0 deposits mint_0
send_tx(
solana,
TokenDepositInstruction {
amount: 10_000,
account: account_0,
owner,
token_account: payer_mint_accounts[0],
token_authority: payer,
bank_index: 0,
},
)
.await
.unwrap();
let account_0 = create_funded_account(
&solana,
group,
owner,
0,
&context.users[1],
&mints[0..1],
100_000,
0,
)
.await;
let account_1 = create_funded_account(
&solana,
group,
owner,
1,
&context.users[1],
&mints[1..2],
1_000_000,
1,
)
.await;
// account_1 deposits mint_1
send_tx(
solana,
TokenDepositInstruction {
amount: 10_000 * 10,
account: account_1,
owner,
token_account: payer_mint_accounts[1],
token_authority: payer,
bank_index: 1,
},
)
.await
.unwrap();
}
// Go to the next net borrow limit window
solana.advance_clock_to_next_multiple(3).await;
reset_net_borrows().await;
{
// succeeds because borrow is less than net borrow limit
@ -324,15 +238,68 @@ async fn test_bank_net_borrows_based_borrow_limit() -> Result<(), TransportError
.unwrap();
}
// Go to the next net borrow limit window
solana.advance_clock_to_next_multiple(3).await;
reset_net_borrows().await;
// succeeds because borrow is less than net borrow limit in a fresh window
//
// TEST: If the price goes up, the borrow limit is hit more quickly - it's in USD
//
{
// succeeds because borrow is less than net borrow limit in a fresh window
{
send_tx(
solana,
TokenWithdrawInstruction {
amount: 1000,
allow_borrow: true,
account: account_1,
owner,
token_account: payer_mint_accounts[0],
bank_index: 0,
},
)
.await
.unwrap();
}
set_bank_stub_oracle_price(solana, group, &tokens[0], admin, 10.0).await;
// cannot borrow anything: net borrowed 1000 * price 10.0 > limit 6000
let res = send_tx(
solana,
TokenWithdrawInstruction {
amount: 1,
allow_borrow: true,
account: account_1,
owner,
token_account: payer_mint_accounts[0],
bank_index: 0,
},
)
.await;
assert!(res.is_err());
set_bank_stub_oracle_price(solana, group, &tokens[0], admin, 5.0).await;
// cannot borrow this much: (net borrowed 1000 + new borrow 201) * price 5.0 > limit 6000
let res = send_tx(
solana,
TokenWithdrawInstruction {
amount: 201,
allow_borrow: true,
account: account_1,
owner,
token_account: payer_mint_accounts[0],
bank_index: 0,
},
)
.await;
assert!(res.is_err());
// can borrow smaller amounts: (net borrowed 1000 + new borrow 199) * price 5.0 < limit 6000
send_tx(
solana,
TokenWithdrawInstruction {
amount: 1000,
amount: 199,
allow_borrow: true,
account: account_1,
owner,
@ -342,7 +309,6 @@ async fn test_bank_net_borrows_based_borrow_limit() -> Result<(), TransportError
)
.await
.unwrap();
solana.advance_clock().await;
}
Ok(())

View File

@ -222,7 +222,7 @@ async fn test_health_compute_perp() -> Result<(), TransportError> {
maker_fee: 0.0002,
taker_fee: 0.000,
settle_pnl_limit_factor: 0.2,
settle_pnl_limit_factor_window_size_ts: 24 * 60 * 60,
settle_pnl_limit_window_size_ts: 24 * 60 * 60,
..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, &token).await
},
)

View File

@ -63,7 +63,7 @@ async fn test_liq_perps_force_cancel() -> Result<(), TransportError> {
maker_fee: 0.0,
taker_fee: 0.0,
settle_pnl_limit_factor: 0.2,
settle_pnl_limit_factor_window_size_ts: 24 * 60 * 60,
settle_pnl_limit_window_size_ts: 24 * 60 * 60,
..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, base_token).await
},
)
@ -266,7 +266,7 @@ async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportEr
taker_fee: 0.0,
group_insurance_fund: true,
settle_pnl_limit_factor: 0.2,
settle_pnl_limit_factor_window_size_ts: 24 * 60 * 60,
settle_pnl_limit_window_size_ts: 24 * 60 * 60,
..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, base_token).await
},
)

View File

@ -86,7 +86,7 @@ async fn test_perp_fixed() -> Result<(), TransportError> {
maker_fee: -0.0001,
taker_fee: 0.0002,
settle_pnl_limit_factor: 0.2,
settle_pnl_limit_factor_window_size_ts: 24 * 60 * 60,
settle_pnl_limit_window_size_ts: 24 * 60 * 60,
..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, &tokens[0]).await
},
)
@ -524,7 +524,7 @@ async fn test_perp_oracle_peg() -> Result<(), TransportError> {
maker_fee: -0.0001,
taker_fee: 0.0002,
settle_pnl_limit_factor: 0.2,
settle_pnl_limit_factor_window_size_ts: 24 * 60 * 60,
settle_pnl_limit_window_size_ts: 24 * 60 * 60,
..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, &tokens[0]).await
},
)

View File

@ -81,7 +81,7 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> {
maker_fee: 0.0002,
taker_fee: 0.000,
settle_pnl_limit_factor: 0.2,
settle_pnl_limit_factor_window_size_ts: 24 * 60 * 60,
settle_pnl_limit_window_size_ts: 24 * 60 * 60,
..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, &tokens[1]).await
},
)
@ -111,7 +111,7 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> {
maker_fee: 0.0002,
taker_fee: 0.000,
settle_pnl_limit_factor: 0.2,
settle_pnl_limit_factor_window_size_ts: 24 * 60 * 60,
settle_pnl_limit_window_size_ts: 24 * 60 * 60,
..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, &tokens[2]).await
},
)
@ -585,7 +585,7 @@ async fn test_perp_settle_pnl_fees() -> Result<(), TransportError> {
settle_fee_amount_threshold: 2000.0,
settle_fee_fraction_low_health: fee_low_health,
settle_pnl_limit_factor: 0.2,
settle_pnl_limit_factor_window_size_ts: 24 * 60 * 60,
settle_pnl_limit_window_size_ts: 24 * 60 * 60,
..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, &tokens[1]).await
},
)
@ -851,7 +851,7 @@ async fn test_perp_pnl_settle_limit() -> Result<(), TransportError> {
maker_fee: 0.0002,
taker_fee: 0.000,
settle_pnl_limit_factor: 0.2,
settle_pnl_limit_factor_window_size_ts: 24 * 60 * 60,
settle_pnl_limit_window_size_ts: 24 * 60 * 60,
..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, &tokens[1]).await
},
)

View File

@ -156,7 +156,7 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> {
maker_fee: 0.0002,
taker_fee: 0.000,
settle_pnl_limit_factor: 0.2,
settle_pnl_limit_factor_window_size_ts: 24 * 60 * 60,
settle_pnl_limit_window_size_ts: 24 * 60 * 60,
..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, &tokens[0]).await
},
)
@ -186,7 +186,7 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> {
maker_fee: 0.0002,
taker_fee: 0.000,
settle_pnl_limit_factor: 0.2,
settle_pnl_limit_factor_window_size_ts: 24 * 60 * 60,
settle_pnl_limit_window_size_ts: 24 * 60 * 60,
..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, &tokens[1]).await
},
)

View File

@ -296,9 +296,14 @@ export class MangoClient {
maintLiabWeight: number | null,
initLiabWeight: number | null,
liquidationFee: number | null,
stablePriceDelayIntervalSeconds: number | null,
stablePriceDelayGrowthLimit: number | null,
stablePriceGrowthLimit: number | null,
minVaultToDepositsRatio: number | null,
netBorrowsLimitNative: number | null,
netBorrowsLimitQuote: number | null,
netBorrowsWindowSizeTs: number | null,
resetStablePrice: boolean | null,
resetNetBorrowLimit: boolean | null,
): Promise<TransactionSignature> {
const bank = group.getFirstBankByMint(mintPk);
const mintInfo = group.mintInfosMapByTokenIndex.get(bank.tokenIndex)!;
@ -316,9 +321,14 @@ export class MangoClient {
maintLiabWeight,
initLiabWeight,
liquidationFee,
stablePriceDelayIntervalSeconds,
stablePriceDelayGrowthLimit,
stablePriceGrowthLimit,
minVaultToDepositsRatio,
netBorrowsLimitNative,
netBorrowsWindowSizeTs,
netBorrowsLimitQuote !== null ? new BN(netBorrowsLimitQuote) : null,
netBorrowsWindowSizeTs !== null ? new BN(netBorrowsWindowSizeTs) : null,
resetStablePrice ?? false,
resetNetBorrowLimit ?? false,
)
.accounts({
group: group.publicKey,
@ -1377,6 +1387,8 @@ export class MangoClient {
settleFeeAmountThreshold: number,
settleFeeFractionLowHealth: number,
settleTokenIndex: number,
settlePnlLimitFactor: number,
settlePnlLimitWindowSize: number,
): Promise<TransactionSignature> {
const orderbook = new Keypair();
const eventQueue = new Keypair();
@ -1413,6 +1425,8 @@ export class MangoClient {
settleFeeAmountThreshold,
settleFeeFractionLowHealth,
settleTokenIndex,
settlePnlLimitFactor,
new BN(settlePnlLimitWindowSize),
)
.accounts({
group: group.publicKey,
@ -1477,6 +1491,8 @@ export class MangoClient {
stablePriceDelayIntervalSeconds: number | null,
stablePriceDelayGrowthLimit: number | null,
stablePriceGrowthLimit: number | null,
settlePnlLimitFactor: number | null,
settlePnlLimitWindowSize: number | null,
): Promise<TransactionSignature> {
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
@ -1494,7 +1510,7 @@ export class MangoClient {
takerFee,
minFunding,
maxFunding,
impactQuantity ? new BN(impactQuantity) : null,
impactQuantity !== null ? new BN(impactQuantity) : null,
groupInsuranceFund,
trustedMarket,
feePenalty,
@ -1504,6 +1520,10 @@ export class MangoClient {
stablePriceDelayIntervalSeconds,
stablePriceDelayGrowthLimit,
stablePriceGrowthLimit,
settlePnlLimitFactor,
settlePnlLimitWindowSize !== null
? new BN(settlePnlLimitWindowSize)
: null,
)
.accounts({
group: group.publicKey,

View File

@ -351,7 +351,7 @@ export type MangoV4 = {
"type": "u64"
},
{
"name": "netBorrowsLimitNative",
"name": "netBorrowsLimitQuote",
"type": "i64"
}
]
@ -614,7 +614,7 @@ export type MangoV4 = {
}
},
{
"name": "netBorrowsLimitNativeOpt",
"name": "netBorrowsLimitQuoteOpt",
"type": {
"option": "i64"
}
@ -624,6 +624,14 @@ export type MangoV4 = {
"type": {
"option": "u64"
}
},
{
"name": "resetStablePrice",
"type": "bool"
},
{
"name": "resetNetBorrowLimit",
"type": "bool"
}
]
},
@ -1735,6 +1743,11 @@ export type MangoV4 = {
"The bank vault that pays for the order, if necessary"
]
},
{
"name": "payerOracle",
"isMut": false,
"isSigner": false
},
{
"name": "tokenProgram",
"isMut": false,
@ -2450,7 +2463,7 @@ export type MangoV4 = {
"type": "f32"
},
{
"name": "settlePnlLimitFactorWindowSizeTs",
"name": "settlePnlLimitWindowSizeTs",
"type": "u64"
}
]
@ -2621,7 +2634,7 @@ export type MangoV4 = {
}
},
{
"name": "settlePnlLimitFactorWindowSizeTs",
"name": "settlePnlLimitWindowSizeTs",
"type": {
"option": "u64"
}
@ -3621,22 +3634,37 @@ export type MangoV4 = {
},
{
"name": "minVaultToDepositsRatio",
"docs": [
"Min fraction of deposits that must remain in the vault when borrowing."
],
"type": "f64"
},
{
"name": "netBorrowsWindowSizeTs",
"docs": [
"Size in seconds of a net borrows window"
],
"type": "u64"
},
{
"name": "lastNetBorrowsWindowStartTs",
"docs": [
"Timestamp at which the last net borrows window started"
],
"type": "u64"
},
{
"name": "netBorrowsLimitNative",
"name": "netBorrowsLimitQuote",
"docs": [
"Net borrow limit per window in quote native; set to -1 to disable."
],
"type": "i64"
},
{
"name": "netBorrowsWindowNative",
"name": "netBorrowsInWindow",
"docs": [
"Sum of all deposits and borrows in the last window, in native units."
],
"type": "i64"
},
{
@ -4294,7 +4322,7 @@ export type MangoV4 = {
"type": "f32"
},
{
"name": "settlePnlLimitFactorWindowSizeTs",
"name": "settlePnlLimitWindowSizeTs",
"type": "u64"
},
{
@ -7550,7 +7578,7 @@ export const IDL: MangoV4 = {
"type": "u64"
},
{
"name": "netBorrowsLimitNative",
"name": "netBorrowsLimitQuote",
"type": "i64"
}
]
@ -7813,7 +7841,7 @@ export const IDL: MangoV4 = {
}
},
{
"name": "netBorrowsLimitNativeOpt",
"name": "netBorrowsLimitQuoteOpt",
"type": {
"option": "i64"
}
@ -7823,6 +7851,14 @@ export const IDL: MangoV4 = {
"type": {
"option": "u64"
}
},
{
"name": "resetStablePrice",
"type": "bool"
},
{
"name": "resetNetBorrowLimit",
"type": "bool"
}
]
},
@ -8934,6 +8970,11 @@ export const IDL: MangoV4 = {
"The bank vault that pays for the order, if necessary"
]
},
{
"name": "payerOracle",
"isMut": false,
"isSigner": false
},
{
"name": "tokenProgram",
"isMut": false,
@ -9649,7 +9690,7 @@ export const IDL: MangoV4 = {
"type": "f32"
},
{
"name": "settlePnlLimitFactorWindowSizeTs",
"name": "settlePnlLimitWindowSizeTs",
"type": "u64"
}
]
@ -9820,7 +9861,7 @@ export const IDL: MangoV4 = {
}
},
{
"name": "settlePnlLimitFactorWindowSizeTs",
"name": "settlePnlLimitWindowSizeTs",
"type": {
"option": "u64"
}
@ -10820,22 +10861,37 @@ export const IDL: MangoV4 = {
},
{
"name": "minVaultToDepositsRatio",
"docs": [
"Min fraction of deposits that must remain in the vault when borrowing."
],
"type": "f64"
},
{
"name": "netBorrowsWindowSizeTs",
"docs": [
"Size in seconds of a net borrows window"
],
"type": "u64"
},
{
"name": "lastNetBorrowsWindowStartTs",
"docs": [
"Timestamp at which the last net borrows window started"
],
"type": "u64"
},
{
"name": "netBorrowsLimitNative",
"name": "netBorrowsLimitQuote",
"docs": [
"Net borrow limit per window in quote native; set to -1 to disable."
],
"type": "i64"
},
{
"name": "netBorrowsWindowNative",
"name": "netBorrowsInWindow",
"docs": [
"Sum of all deposits and borrows in the last window, in native units."
],
"type": "i64"
},
{
@ -11493,7 +11549,7 @@ export const IDL: MangoV4 = {
"type": "f32"
},
{
"name": "settlePnlLimitFactorWindowSizeTs",
"name": "settlePnlLimitWindowSizeTs",
"type": "u64"
},
{

View File

@ -396,6 +396,8 @@ async function main() {
1000000,
0.05,
0,
1.0,
2 * 60 * 60,
);
console.log('done');
} catch (error) {
@ -425,9 +427,14 @@ async function main() {
1,
1,
0,
null,
null,
null,
MIN_VAULT_TO_DEPOSITS_RATIO,
NET_BORROWS_WINDOW_SIZE_TS,
NET_BORROWS_LIMIT_NATIVE,
false,
false,
);
console.log(`https://explorer.solana.com/tx/${sig}?cluster=devnet`);
await group.reloadAll(client);
@ -452,9 +459,14 @@ async function main() {
1.1,
1.2,
0.05,
null,
null,
null,
MIN_VAULT_TO_DEPOSITS_RATIO,
NET_BORROWS_WINDOW_SIZE_TS,
NET_BORROWS_LIMIT_NATIVE,
false,
false,
);
console.log(`https://explorer.solana.com/tx/${sig}?cluster=devnet`);
await group.reloadAll(client);
@ -479,9 +491,14 @@ async function main() {
1.1,
1.2,
0.05,
null,
null,
null,
MIN_VAULT_TO_DEPOSITS_RATIO,
NET_BORROWS_WINDOW_SIZE_TS,
NET_BORROWS_LIMIT_NATIVE,
false,
false,
);
console.log(`https://explorer.solana.com/tx/${sig}?cluster=devnet`);
await group.reloadAll(client);
@ -517,6 +534,8 @@ async function main() {
MIN_VAULT_TO_DEPOSITS_RATIO,
NET_BORROWS_WINDOW_SIZE_TS,
NET_BORROWS_LIMIT_NATIVE,
1.0,
2 * 60 * 60,
);
console.log(`https://explorer.solana.com/tx/${sig}?cluster=devnet`);
await group.reloadAll(client);

View File

@ -235,6 +235,8 @@ async function main() {
0,
0,
0,
1.0,
2 * 60 * 60,
);
} catch (error) {
console.log(error);

View File

@ -170,6 +170,11 @@ async function main() {
null,
null,
null,
null,
null,
null,
null,
null,
);
try {
// At a price of $1/ui-SOL we can buy 0.1 ui-SOL for the 100k native-USDC we have.
@ -205,6 +210,11 @@ async function main() {
null,
null,
null,
null,
null,
null,
null,
null,
);
}
}