diff --git a/programs/mango-v4/src/error.rs b/programs/mango-v4/src/error.rs index d248764cc..9bc5bb6c3 100644 --- a/programs/mango-v4/src/error.rs +++ b/programs/mango-v4/src/error.rs @@ -57,6 +57,10 @@ pub enum MangoError { OracleStale, #[msg("settlement amount must always be positive")] SettlementAmountMustBePositive, + #[msg("bank utilization has reached limit")] + BankBorrowLimitReached, + #[msg("bank net borrows has reached limit - this is an intermittent error - the limit will reset regularly")] + BankNetBorrowsLimitReached, } pub trait Contextable { diff --git a/programs/mango-v4/src/instructions/flash_loan.rs b/programs/mango-v4/src/instructions/flash_loan.rs index a9b77dbec..da91bbab1 100644 --- a/programs/mango-v4/src/instructions/flash_loan.rs +++ b/programs/mango-v4/src/instructions/flash_loan.rs @@ -392,8 +392,11 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>( let loan_origination_fee = cm!(loan * bank.loan_origination_fee_rate); cm!(bank.collected_fees_native += loan_origination_fee); - let is_active = - bank.change_without_fee(position, cm!(change.amount - loan_origination_fee))?; + let is_active = bank.change_without_fee( + position, + cm!(change.amount - loan_origination_fee), + Clock::get()?.unix_timestamp.try_into().unwrap(), + )?; if !is_active { deactivated_token_positions.push(change.raw_token_index); } diff --git a/programs/mango-v4/src/instructions/perp_liq_bankruptcy.rs b/programs/mango-v4/src/instructions/perp_liq_bankruptcy.rs index f92ec8477..e7d887e1c 100644 --- a/programs/mango-v4/src/instructions/perp_liq_bankruptcy.rs +++ b/programs/mango-v4/src/instructions/perp_liq_bankruptcy.rs @@ -159,7 +159,11 @@ pub fn perp_liq_bankruptcy(ctx: Context, max_liab_transfer: u // credit the liqor with quote tokens let (liqor_quote, _, _) = liqor.ensure_token_position(settle_token_index)?; - settle_bank.deposit(liqor_quote, insurance_transfer_i80f48)?; + settle_bank.deposit( + liqor_quote, + insurance_transfer_i80f48, + Clock::get()?.unix_timestamp.try_into().unwrap(), + )?; emit!(TokenBalanceLog { mango_group: ctx.accounts.group.key(), diff --git a/programs/mango-v4/src/instructions/perp_settle_fees.rs b/programs/mango-v4/src/instructions/perp_settle_fees.rs index 90e08e1a6..a3f0141d1 100644 --- a/programs/mango-v4/src/instructions/perp_settle_fees.rs +++ b/programs/mango-v4/src/instructions/perp_settle_fees.rs @@ -107,7 +107,11 @@ pub fn perp_settle_fees(ctx: Context, max_settle_amount: u64) -> let token_position = account .token_position_mut(perp_market.settle_token_index)? .0; - bank.withdraw_with_fee(token_position, settlement)?; + bank.withdraw_with_fee( + token_position, + settlement, + Clock::get()?.unix_timestamp.try_into().unwrap(), + )?; // Update the settled balance on the market itself perp_market.fees_settled = cm!(perp_market.fees_settled + settlement); diff --git a/programs/mango-v4/src/instructions/perp_settle_pnl.rs b/programs/mango-v4/src/instructions/perp_settle_pnl.rs index f106ec81d..9055ec0ff 100644 --- a/programs/mango-v4/src/instructions/perp_settle_pnl.rs +++ b/programs/mango-v4/src/instructions/perp_settle_pnl.rs @@ -180,12 +180,14 @@ pub fn perp_settle_pnl(ctx: Context) -> Result<()> { cm!(account_a.fixed.perp_spot_transfers += settlement_i64 - fee_i64); cm!(account_b.fixed.perp_spot_transfers -= settlement_i64); + let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap(); + // Transfer token balances // The fee is paid by the account with positive unsettled pnl 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))?; - bank.withdraw_with_fee(b_token_position, settlement)?; + bank.deposit(a_token_position, cm!(settlement - fee), now_ts)?; + bank.withdraw_with_fee(b_token_position, settlement, now_ts)?; emit!(TokenBalanceLog { mango_group: ctx.accounts.group.key(), @@ -220,7 +222,7 @@ pub fn perp_settle_pnl(ctx: Context) -> Result<()> { let (settler_token_position, settler_token_raw_index, _) = settler.ensure_token_position(settle_token_index)?; - let settler_token_position_active = bank.deposit(settler_token_position, fee)?; + let settler_token_position_active = bank.deposit(settler_token_position, fee, now_ts)?; emit!(TokenBalanceLog { mango_group: ctx.accounts.group.key(), diff --git a/programs/mango-v4/src/instructions/serum3_place_order.rs b/programs/mango-v4/src/instructions/serum3_place_order.rs index 16d84e12f..263a3af86 100644 --- a/programs/mango-v4/src/instructions/serum3_place_order.rs +++ b/programs/mango-v4/src/instructions/serum3_place_order.rs @@ -429,7 +429,11 @@ pub fn apply_vault_difference( let (position, _) = account.token_position_mut(bank.token_index)?; let native_before = position.native(bank); - bank.change_without_fee(position, needed_change)?; + bank.change_without_fee( + position, + needed_change, + Clock::get()?.unix_timestamp.try_into().unwrap(), + )?; let native_after = position.native(bank); let native_change = cm!(native_after - native_before); let new_borrows = native_change diff --git a/programs/mango-v4/src/instructions/serum3_settle_funds.rs b/programs/mango-v4/src/instructions/serum3_settle_funds.rs index d82865a8b..553deba9e 100644 --- a/programs/mango-v4/src/instructions/serum3_settle_funds.rs +++ b/programs/mango-v4/src/instructions/serum3_settle_funds.rs @@ -206,6 +206,8 @@ pub fn charge_loan_origination_fees( ) -> Result<()> { let serum3_account = account.serum3_orders_mut(market_index).unwrap(); + let now_ts = Clock::get()?.unix_timestamp.try_into().unwrap(); + let oo_base_total = before_oo.native_base_total(); let actualized_base_loan = I80F48::from_num( serum3_account @@ -218,8 +220,11 @@ pub fn charge_loan_origination_fees( // now that the loan is actually materialized, charge the loan origination fee // note: the withdraw has already happened while placing the order let base_token_account = account.token_position_mut(base_bank.token_index)?.0; - let (_, fee) = - base_bank.withdraw_loan_origination_fee(base_token_account, actualized_base_loan)?; + let (_, fee) = base_bank.withdraw_loan_origination_fee( + base_token_account, + actualized_base_loan, + now_ts, + )?; emit!(WithdrawLoanOriginationFeeLog { mango_group: *group_pubkey, @@ -243,8 +248,11 @@ pub fn charge_loan_origination_fees( // now that the loan is actually materialized, charge the loan origination fee // note: the withdraw has already happened while placing the order let quote_token_account = account.token_position_mut(quote_bank.token_index)?.0; - let (_, fee) = - quote_bank.withdraw_loan_origination_fee(quote_token_account, actualized_quote_loan)?; + let (_, fee) = quote_bank.withdraw_loan_origination_fee( + quote_token_account, + actualized_quote_loan, + now_ts, + )?; emit!(WithdrawLoanOriginationFeeLog { mango_group: *group_pubkey, diff --git a/programs/mango-v4/src/instructions/token_add_bank.rs b/programs/mango-v4/src/instructions/token_add_bank.rs index 87d51d952..a6e412b83 100644 --- a/programs/mango-v4/src/instructions/token_add_bank.rs +++ b/programs/mango-v4/src/instructions/token_add_bank.rs @@ -69,7 +69,13 @@ pub fn token_add_bank( let existing_bank = ctx.accounts.existing_bank.load()?; let mut bank = ctx.accounts.bank.load_init()?; let bump = *ctx.bumps.get("bank").ok_or(MangoError::SomeError)?; - *bank = Bank::from_existing_bank(&existing_bank, ctx.accounts.vault.key(), bank_num, bump); + *bank = Bank::from_existing_bank( + &existing_bank, + ctx.accounts.vault.key(), + bank_num, + bump, + Clock::get()?.unix_timestamp as u64, + ); let mut mint_info = ctx.accounts.mint_info.load_mut()?; let free_slot = mint_info diff --git a/programs/mango-v4/src/instructions/token_deposit.rs b/programs/mango-v4/src/instructions/token_deposit.rs index 5b71a05b7..ffbe7de8c 100644 --- a/programs/mango-v4/src/instructions/token_deposit.rs +++ b/programs/mango-v4/src/instructions/token_deposit.rs @@ -113,7 +113,11 @@ impl<'a, 'info> DepositCommon<'a, 'info> { let amount_i80f48 = I80F48::from(amount); let position_is_active = { let mut bank = self.bank.load_mut()?; - bank.deposit(position, amount_i80f48)? + bank.deposit( + position, + amount_i80f48, + Clock::get()?.unix_timestamp.try_into().unwrap(), + )? }; // Transfer the actual tokens diff --git a/programs/mango-v4/src/instructions/token_edit.rs b/programs/mango-v4/src/instructions/token_edit.rs index 48ba75e1d..200261b26 100644 --- a/programs/mango-v4/src/instructions/token_edit.rs +++ b/programs/mango-v4/src/instructions/token_edit.rs @@ -49,6 +49,9 @@ pub fn token_edit( stable_price_delay_interval_seconds_opt: Option, stable_price_delay_growth_limit_opt: Option, stable_price_growth_limit_opt: Option, + min_vault_to_deposits_ratio_opt: Option, + net_borrows_limit_native_opt: Option, + net_borrows_window_size_ts_opt: Option, ) -> Result<()> { let mut mint_info = ctx.accounts.mint_info.load_mut()?; mint_info.verify_banks_ais(ctx.remaining_accounts)?; @@ -142,6 +145,16 @@ pub fn token_edit( bank.stable_price_model.stable_growth_limit = stable_price_growth_limit; } + 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_window_size_ts) = net_borrows_window_size_ts_opt { + bank.net_borrows_window_size_ts = net_borrows_window_size_ts; + } + // unchanged - // dust // flash_loan_token_account_initial diff --git a/programs/mango-v4/src/instructions/token_liq_bankruptcy.rs b/programs/mango-v4/src/instructions/token_liq_bankruptcy.rs index cd8d2727b..79ed7eefb 100644 --- a/programs/mango-v4/src/instructions/token_liq_bankruptcy.rs +++ b/programs/mango-v4/src/instructions/token_liq_bankruptcy.rs @@ -143,11 +143,13 @@ pub fn token_liq_bankruptcy( // liquidators to exploit the insurance fund for 1 native token each call. let liab_transfer = cm!(insurance_transfer_i80f48 / liab_price_adjusted); + let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap(); + let mut liqee_liab_active = true; if insurance_transfer > 0 { // liqee gets liab assets (enable dusting to prevent a case where the position is brought // to +I80F48::DELTA) - liqee_liab_active = liab_bank.deposit_with_dusting(liqee_liab, liab_transfer)?; + liqee_liab_active = liab_bank.deposit_with_dusting(liqee_liab, liab_transfer, now_ts)?; remaining_liab_loss = -liqee_liab.native(liab_bank); // move insurance assets into quote bank @@ -169,7 +171,8 @@ pub fn token_liq_bankruptcy( // credit the liqor let (liqor_quote, liqor_quote_raw_token_index, _) = liqor.ensure_token_position(QUOTE_TOKEN_INDEX)?; - let liqor_quote_active = quote_bank.deposit(liqor_quote, insurance_transfer_i80f48)?; + let liqor_quote_active = + quote_bank.deposit(liqor_quote, insurance_transfer_i80f48, now_ts)?; // liqor quote emit!(TokenBalanceLog { @@ -185,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)?; + liab_bank.withdraw_with_fee(liqor_liab, liab_transfer, now_ts)?; // liqor liab emit!(TokenBalanceLog { @@ -268,7 +271,8 @@ pub fn token_liq_bankruptcy( if amount_for_bank.is_positive() { // enable dusting, because each deposit() is allowed to round up. thus multiple deposit // could bring the total position slightly above zero otherwise - liqee_liab_active = bank.deposit_with_dusting(liqee_liab, amount_for_bank)?; + liqee_liab_active = + bank.deposit_with_dusting(liqee_liab, amount_for_bank, now_ts)?; cm!(amount_to_credit -= amount_for_bank); if amount_to_credit <= 0 { break; diff --git a/programs/mango-v4/src/instructions/token_liq_with_token.rs b/programs/mango-v4/src/instructions/token_liq_with_token.rs index 01702544c..9280b92a7 100644 --- a/programs/mango-v4/src/instructions/token_liq_with_token.rs +++ b/programs/mango-v4/src/instructions/token_liq_with_token.rs @@ -142,24 +142,38 @@ pub fn token_liq_with_token( // Apply the balance changes to the liqor and liqee accounts let liqee_liab_position = liqee.token_position_mut_by_raw_index(liqee_liab_raw_index); - let liqee_liab_active = liab_bank.deposit_with_dusting(liqee_liab_position, liab_transfer)?; + let liqee_liab_active = liab_bank.deposit_with_dusting( + liqee_liab_position, + liab_transfer, + Clock::get()?.unix_timestamp.try_into().unwrap(), + )?; let liqee_liab_indexed_position = liqee_liab_position.indexed_position; let (liqor_liab_position, liqor_liab_raw_index, _) = liqor.ensure_token_position(liab_token_index)?; - let (liqor_liab_active, loan_origination_fee) = - liab_bank.withdraw_with_fee(liqor_liab_position, liab_transfer)?; + let (liqor_liab_active, loan_origination_fee) = liab_bank.withdraw_with_fee( + liqor_liab_position, + liab_transfer, + Clock::get()?.unix_timestamp.try_into().unwrap(), + )?; let liqor_liab_indexed_position = liqor_liab_position.indexed_position; let liqee_liab_native_after = liqee_liab_position.native(liab_bank); let (liqor_asset_position, liqor_asset_raw_index, _) = liqor.ensure_token_position(asset_token_index)?; - let liqor_asset_active = asset_bank.deposit(liqor_asset_position, asset_transfer)?; + let liqor_asset_active = asset_bank.deposit( + liqor_asset_position, + asset_transfer, + Clock::get()?.unix_timestamp.try_into().unwrap(), + )?; let liqor_asset_indexed_position = liqor_asset_position.indexed_position; let liqee_asset_position = liqee.token_position_mut_by_raw_index(liqee_asset_raw_index); - let liqee_asset_active = - asset_bank.withdraw_without_fee_with_dusting(liqee_asset_position, asset_transfer)?; + let liqee_asset_active = asset_bank.withdraw_without_fee_with_dusting( + liqee_asset_position, + asset_transfer, + Clock::get()?.unix_timestamp.try_into().unwrap(), + )?; let liqee_asset_indexed_position = liqee_asset_position.indexed_position; let liqee_assets_native_after = liqee_asset_position.native(asset_bank); diff --git a/programs/mango-v4/src/instructions/token_register.rs b/programs/mango-v4/src/instructions/token_register.rs index 41cd9df9f..f2418a2d7 100644 --- a/programs/mango-v4/src/instructions/token_register.rs +++ b/programs/mango-v4/src/instructions/token_register.rs @@ -90,6 +90,9 @@ pub fn token_register( maint_liab_weight: f32, init_liab_weight: f32, liquidation_fee: f32, + min_vault_to_deposits_ratio: f64, + net_borrows_window_size_ts: u64, + net_borrows_limit_native: i64, ) -> Result<()> { // Require token 0 to be in the insurance token if token_index == QUOTE_TOKEN_INDEX { @@ -99,7 +102,7 @@ pub fn token_register( ); } - let now_ts = Clock::get()?.unix_timestamp; + let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap(); let mut bank = ctx.accounts.bank.load_init()?; *bank = Bank { @@ -142,7 +145,13 @@ pub fn token_register( oracle_conf_filter: oracle_config.to_oracle_config().conf_filter, oracle_config: oracle_config.to_oracle_config(), stable_price_model: StablePriceModel::default(), - reserved: [0; 2176], + min_vault_to_deposits_ratio, + 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, + reserved: [0; 2136], }; require_gt!(bank.max_rate, MINIMUM_MAX_RATE); diff --git a/programs/mango-v4/src/instructions/token_register_trustless.rs b/programs/mango-v4/src/instructions/token_register_trustless.rs index e040e62cd..9fe6e4d97 100644 --- a/programs/mango-v4/src/instructions/token_register_trustless.rs +++ b/programs/mango-v4/src/instructions/token_register_trustless.rs @@ -72,7 +72,8 @@ pub fn token_register_trustless( ) -> Result<()> { require_neq!(token_index, 0); - let now_ts = Clock::get()?.unix_timestamp; + let net_borrows_window_size_ts = 24 * 60 * 60 as u64; + let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap(); let mut bank = ctx.accounts.bank.load_init()?; *bank = Bank { @@ -119,7 +120,13 @@ pub fn token_register_trustless( reserved: [0; 72], }, stable_price_model: StablePriceModel::default(), - reserved: [0; 2176], + min_vault_to_deposits_ratio: 0.2, + 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, + reserved: [0; 2136], }; require_gt!(bank.max_rate, MINIMUM_MAX_RATE); diff --git a/programs/mango-v4/src/instructions/token_update_index_and_rate.rs b/programs/mango-v4/src/instructions/token_update_index_and_rate.rs index 92abdc4b2..0eedf2fc2 100644 --- a/programs/mango-v4/src/instructions/token_update_index_and_rate.rs +++ b/programs/mango-v4/src/instructions/token_update_index_and_rate.rs @@ -79,7 +79,7 @@ pub fn token_update_index_and_rate(ctx: Context) -> Res .verify_banks_ais(ctx.remaining_accounts)?; let clock = Clock::get()?; - let now_ts = clock.unix_timestamp; + let now_ts: u64 = clock.unix_timestamp.try_into().unwrap(); // compute indexed_total let mut indexed_total_deposits = I80F48::ZERO; diff --git a/programs/mango-v4/src/instructions/token_withdraw.rs b/programs/mango-v4/src/instructions/token_withdraw.rs index 59919b45d..3ebb31e18 100644 --- a/programs/mango-v4/src/instructions/token_withdraw.rs +++ b/programs/mango-v4/src/instructions/token_withdraw.rs @@ -93,16 +93,17 @@ pub fn token_withdraw(ctx: Context, amount: u64, allow_borrow: bo amount }; - require!( - allow_borrow || amount <= native_position, - MangoError::SomeError - ); + let is_borrow = amount > native_position; + require!(allow_borrow || !is_borrow, MangoError::SomeError); let amount_i80f48 = I80F48::from(amount); // Update the bank and position - let (position_is_active, loan_origination_fee) = - bank.withdraw_with_fee(position, amount_i80f48)?; + let (position_is_active, loan_origination_fee) = bank.withdraw_with_fee( + position, + amount_i80f48, + Clock::get()?.unix_timestamp.try_into().unwrap(), + )?; // Provide a readable error message in case the vault doesn't have enough tokens if ctx.accounts.vault.amount < amount { @@ -178,5 +179,21 @@ pub fn token_withdraw(ctx: Context, amount: u64, allow_borrow: bo }); } + // Prevent borrowing away the full bank vault. Keep some in reserve to satisfy non-borrow withdraws + let bank_native_deposits = bank.native_deposits(); + if bank_native_deposits != I80F48::ZERO && is_borrow { + ctx.accounts.vault.reload()?; + let bank_native_deposits: f64 = bank_native_deposits.checked_to_num().unwrap(); + let vault_amount = ctx.accounts.vault.amount as f64; + if vault_amount < bank.min_vault_to_deposits_ratio * bank_native_deposits { + return err!(MangoError::BankBorrowLimitReached).with_context(|| { + format!( + "vault_amount ({:?}) below min_vault_to_deposits_ratio * bank_native_deposits ({:?})", + vault_amount, bank.min_vault_to_deposits_ratio * bank_native_deposits, + ) + }); + } + } + Ok(()) } diff --git a/programs/mango-v4/src/lib.rs b/programs/mango-v4/src/lib.rs index 946fa8f5b..d283d9b6d 100644 --- a/programs/mango-v4/src/lib.rs +++ b/programs/mango-v4/src/lib.rs @@ -74,6 +74,9 @@ pub mod mango_v4 { maint_liab_weight: f32, init_liab_weight: f32, liquidation_fee: f32, + min_vault_to_deposits_ratio: f64, + net_borrows_window_size_ts: u64, + net_borrows_limit_native: i64, ) -> Result<()> { instructions::token_register( ctx, @@ -88,6 +91,9 @@ pub mod mango_v4 { maint_liab_weight, init_liab_weight, liquidation_fee, + min_vault_to_deposits_ratio, + net_borrows_window_size_ts, + net_borrows_limit_native, ) } @@ -116,6 +122,9 @@ pub mod mango_v4 { stable_price_delay_interval_seconds_opt: Option, stable_price_delay_growth_limit_opt: Option, stable_price_growth_limit_opt: Option, + min_vault_to_deposits_ratio_opt: Option, + net_borrows_limit_native_opt: Option, + net_borrows_window_size_ts_opt: Option, ) -> Result<()> { instructions::token_edit( ctx, @@ -133,6 +142,9 @@ pub mod mango_v4 { stable_price_delay_interval_seconds_opt, stable_price_delay_growth_limit_opt, stable_price_growth_limit_opt, + min_vault_to_deposits_ratio_opt, + net_borrows_limit_native_opt, + net_borrows_window_size_ts_opt, ) } diff --git a/programs/mango-v4/src/state/bank.rs b/programs/mango-v4/src/state/bank.rs index a73884478..646345144 100644 --- a/programs/mango-v4/src/state/bank.rs +++ b/programs/mango-v4/src/state/bank.rs @@ -1,5 +1,6 @@ use super::{OracleConfig, TokenIndex, TokenPosition}; use crate::accounts_zerocopy::KeyedAccountReader; +use crate::error::{Contextable, MangoError}; use crate::state::{oracle, StablePriceModel}; use crate::util; use crate::util::checked_math as cm; @@ -57,8 +58,8 @@ pub struct Bank { pub indexed_deposits: I80F48, pub indexed_borrows: I80F48, - pub index_last_updated: i64, - pub bank_rate_last_updated: i64, + pub index_last_updated: u64, + pub bank_rate_last_updated: u64, pub avg_utilization: I80F48, @@ -106,8 +107,14 @@ pub struct Bank { pub stable_price_model: StablePriceModel, + pub min_vault_to_deposits_ratio: f64, + pub net_borrows_window_size_ts: u64, + pub last_net_borrows_window_start_ts: u64, + pub net_borrows_limit_native: i64, + pub net_borrows_window_native: i64, + #[derivative(Debug = "ignore")] - pub reserved: [u8; 2176], + pub reserved: [u8; 2136], } const_assert_eq!(size_of::(), 3112); const_assert_eq!(size_of::() % 8, 0); @@ -118,6 +125,7 @@ impl Bank { vault: Pubkey, bank_num: u32, bump: u8, + now_ts: u64, ) -> Self { Self { // values that must be reset/changed @@ -163,7 +171,12 @@ impl Bank { mint_decimals: existing_bank.mint_decimals, oracle_config: existing_bank.oracle_config.clone(), stable_price_model: StablePriceModel::default(), - reserved: [0; 2176], + min_vault_to_deposits_ratio: existing_bank.min_vault_to_deposits_ratio, + net_borrows_limit_native: existing_bank.net_borrows_limit_native, + 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, + reserved: [0; 2136], } } @@ -189,8 +202,13 @@ impl Bank { /// /// native_amount must be >= 0 /// fractional deposits can be relevant during liquidation, for example - pub fn deposit(&mut self, position: &mut TokenPosition, native_amount: I80F48) -> Result { - self.deposit_internal(position, native_amount, !position.is_in_use()) + pub fn deposit( + &mut self, + position: &mut TokenPosition, + native_amount: I80F48, + now_ts: u64, + ) -> Result { + self.deposit_internal_wrapper(position, native_amount, !position.is_in_use(), now_ts) } /// Like `deposit()`, but allows dusting of in-use accounts. @@ -200,11 +218,26 @@ impl Bank { &mut self, position: &mut TokenPosition, native_amount: I80F48, + now_ts: u64, ) -> Result { - self.deposit_internal(position, native_amount, true) + self.deposit_internal_wrapper(position, native_amount, true, now_ts) .map(|not_dusted| not_dusted || position.is_in_use()) } + pub fn deposit_internal_wrapper( + &mut self, + position: &mut TokenPosition, + native_amount: I80F48, + allow_dusting: bool, + now_ts: u64, + ) -> Result { + self.update_net_borrows(-native_amount, now_ts)?; + 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); + Ok(result) + } + /// Internal function to deposit funds pub fn deposit_internal( &mut self, @@ -213,6 +246,7 @@ impl Bank { allow_dusting: bool, ) -> Result { require_gte!(native_amount, 0); + let native_position = position.native(self); let opening_indexed_position = position.indexed_position; @@ -240,14 +274,12 @@ impl Bank { // pay back borrows only, leaving a negative position cm!(self.indexed_borrows -= indexed_change); position.indexed_position = new_indexed_value; - self.update_cumulative_interest(position, opening_indexed_position); return Ok(true); } else if new_native_position < I80F48::ONE && allow_dusting { // if there's less than one token deposited, zero the position cm!(self.dust += new_native_position); cm!(self.indexed_borrows += position.indexed_position); position.indexed_position = I80F48::ZERO; - self.update_cumulative_interest(position, opening_indexed_position); return Ok(false); } @@ -263,7 +295,6 @@ impl Bank { let indexed_change = div_rounding_up(native_amount, self.deposit_index); cm!(self.indexed_deposits += indexed_change); cm!(position.indexed_position += indexed_change); - self.update_cumulative_interest(position, opening_indexed_position); Ok(true) } @@ -280,9 +311,15 @@ impl Bank { &mut self, position: &mut TokenPosition, native_amount: I80F48, + now_ts: u64, ) -> Result { - let (position_is_active, _) = - self.withdraw_internal(position, native_amount, false, !position.is_in_use())?; + let (position_is_active, _) = self.withdraw_internal_wrapper( + position, + native_amount, + false, + !position.is_in_use(), + now_ts, + )?; Ok(position_is_active) } @@ -294,8 +331,9 @@ impl Bank { &mut self, position: &mut TokenPosition, native_amount: I80F48, + now_ts: u64, ) -> Result { - self.withdraw_internal(position, native_amount, false, true) + self.withdraw_internal_wrapper(position, native_amount, false, true, now_ts) .map(|(not_dusted, _)| not_dusted || position.is_in_use()) } @@ -311,8 +349,30 @@ impl Bank { &mut self, position: &mut TokenPosition, native_amount: I80F48, + now_ts: u64, ) -> Result<(bool, I80F48)> { - self.withdraw_internal(position, native_amount, true, !position.is_in_use()) + self.withdraw_internal_wrapper(position, native_amount, true, !position.is_in_use(), now_ts) + } + + /// Internal function to withdraw funds + fn withdraw_internal_wrapper( + &mut self, + position: &mut TokenPosition, + mut native_amount: I80F48, + with_loan_origination_fee: bool, + allow_dusting: bool, + now_ts: u64, + ) -> Result<(bool, I80F48)> { + let opening_indexed_position = position.indexed_position; + let res = self.withdraw_internal( + position, + native_amount, + with_loan_origination_fee, + allow_dusting, + now_ts, + ); + self.update_cumulative_interest(position, opening_indexed_position); + res } /// Internal function to withdraw funds @@ -322,6 +382,7 @@ impl Bank { mut native_amount: I80F48, with_loan_origination_fee: bool, allow_dusting: bool, + now_ts: u64, ) -> Result<(bool, I80F48)> { require_gte!(native_amount, 0); let native_position = position.native(self); @@ -336,14 +397,12 @@ impl Bank { cm!(self.dust += new_native_position); cm!(self.indexed_deposits -= position.indexed_position); position.indexed_position = I80F48::ZERO; - self.update_cumulative_interest(position, opening_indexed_position); return Ok((false, I80F48::ZERO)); } else { // withdraw some deposits leaving a positive balance let indexed_change = cm!(native_amount / self.deposit_index); cm!(self.indexed_deposits -= indexed_change); cm!(position.indexed_position -= indexed_change); - self.update_cumulative_interest(position, opening_indexed_position); return Ok((true, I80F48::ZERO)); } } @@ -366,7 +425,10 @@ impl Bank { let indexed_change = cm!(native_amount / self.borrow_index); cm!(self.indexed_borrows += indexed_change); cm!(position.indexed_position -= indexed_change); - self.update_cumulative_interest(position, opening_indexed_position); + + // 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)?; Ok((true, loan_origination_fee)) } @@ -376,13 +438,19 @@ impl Bank { &mut self, position: &mut TokenPosition, already_borrowed_native_amount: I80F48, + now_ts: u64, ) -> Result<(bool, I80F48)> { let loan_origination_fee = cm!(self.loan_origination_fee_rate * already_borrowed_native_amount); cm!(self.collected_fees_native += loan_origination_fee); - let (position_is_active, _) = - self.withdraw_internal(position, loan_origination_fee, false, !position.is_in_use())?; + let (position_is_active, _) = self.withdraw_internal_wrapper( + position, + loan_origination_fee, + false, + !position.is_in_use(), + now_ts, + )?; Ok((position_is_active, loan_origination_fee)) } @@ -392,11 +460,12 @@ impl Bank { &mut self, position: &mut TokenPosition, native_amount: I80F48, + now_ts: u64, ) -> Result { if native_amount >= 0 { - self.deposit(position, native_amount) + self.deposit(position, native_amount, now_ts) } else { - self.withdraw_without_fee(position, -native_amount) + self.withdraw_without_fee(position, -native_amount, now_ts) } } @@ -405,14 +474,40 @@ impl Bank { &mut self, position: &mut TokenPosition, native_amount: I80F48, + now_ts: u64, ) -> Result<(bool, I80F48)> { if native_amount >= 0 { - Ok((self.deposit(position, native_amount)?, I80F48::ZERO)) + Ok((self.deposit(position, native_amount, now_ts)?, I80F48::ZERO)) } else { - self.withdraw_with_fee(position, -native_amount) + self.withdraw_with_fee(position, -native_amount, now_ts) } } + pub fn update_net_borrows(&mut self, native_amount: I80F48, now_ts: u64) -> 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 { + // 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::().unwrap() + } else { + cm!(self.net_borrows_window_native + 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 + ) + }); + } + + Ok(()) + } + pub fn update_cumulative_interest( &self, position: &mut TokenPosition, @@ -524,9 +619,9 @@ impl Bank { &self, indexed_total_deposits: I80F48, indexed_total_borrows: I80F48, - now_ts: i64, + now_ts: u64, ) -> I80F48 { - if now_ts <= 0 { + if now_ts == 0 { return I80F48::ZERO; } @@ -664,6 +759,8 @@ 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.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); @@ -701,7 +798,8 @@ mod tests { // let change = I80F48::from(change); - let (is_active, _) = bank.change_with_fee(&mut account, change)?; + let dummy_now_ts = 1 as u64; + let (is_active, _) = bank.change_with_fee(&mut account, change, dummy_now_ts)?; let mut expected_native = start_native + change; if expected_native >= 0.0 && expected_native < 1.0 && !is_in_use { @@ -742,7 +840,7 @@ mod tests { bank.index_last_updated = 1000; let compute_new_avg_utilization_runner = - |bank: &mut Bank, utilization: I80F48, now_ts: i64| { + |bank: &mut Bank, utilization: I80F48, now_ts: u64| { bank.avg_utilization = bank.compute_new_avg_utilization(I80F48::ONE, utilization, now_ts); bank.index_last_updated = now_ts; diff --git a/programs/mango-v4/src/state/health.rs b/programs/mango-v4/src/state/health.rs index a067ee4b0..18ab30019 100644 --- a/programs/mango-v4/src/state/health.rs +++ b/programs/mango-v4/src/state/health.rs @@ -1422,6 +1422,8 @@ mod tests { use std::rc::Rc; use std::str::FromStr; + pub const DUMMY_NOW_TS: u64 = 0; + #[test] fn test_precision() { // I80F48 can only represent until 1/2^48 @@ -1529,6 +1531,8 @@ mod tests { bank.data().maint_asset_weight = I80F48::from_num(1.0 - maint_weights); bank.data().maint_liab_weight = I80F48::from_num(1.0 + maint_weights); bank.data().stable_price_model.reset_to_price(price, 0); + 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, oracle) } @@ -1578,6 +1582,7 @@ mod tests { .deposit( account.ensure_token_position(1).unwrap().0, I80F48::from(100), + DUMMY_NOW_TS, ) .unwrap(); bank2 @@ -1585,6 +1590,7 @@ mod tests { .withdraw_without_fee( account.ensure_token_position(4).unwrap().0, I80F48::from(10), + DUMMY_NOW_TS, ) .unwrap(); @@ -1760,6 +1766,7 @@ mod tests { .change_without_fee( account.ensure_token_position(1).unwrap().0, I80F48::from(testcase.token1), + DUMMY_NOW_TS, ) .unwrap(); bank2 @@ -1767,6 +1774,7 @@ mod tests { .change_without_fee( account.ensure_token_position(4).unwrap().0, I80F48::from(testcase.token2), + DUMMY_NOW_TS, ) .unwrap(); bank3 @@ -1774,6 +1782,7 @@ mod tests { .change_without_fee( account.ensure_token_position(5).unwrap().0, I80F48::from(testcase.token3), + DUMMY_NOW_TS, ) .unwrap(); @@ -2285,6 +2294,7 @@ mod tests { .change_without_fee( account.ensure_token_position(1).unwrap().0, I80F48::from(100), + 0, ) .unwrap(); @@ -2366,6 +2376,7 @@ mod tests { .change_without_fee( account.ensure_token_position(1).unwrap().0, I80F48::from(100), + DUMMY_NOW_TS, ) .unwrap(); bank1 @@ -2373,6 +2384,7 @@ mod tests { .change_without_fee( account2.ensure_token_position(1).unwrap().0, I80F48::from(-100), + DUMMY_NOW_TS, ) .unwrap(); diff --git a/programs/mango-v4/tests/program_test/mango_client.rs b/programs/mango-v4/tests/program_test/mango_client.rs index 88f04a27b..d15f86613 100644 --- a/programs/mango-v4/tests/program_test/mango_client.rs +++ b/programs/mango-v4/tests/program_test/mango_client.rs @@ -772,6 +772,10 @@ pub struct TokenRegisterInstruction { pub init_liab_weight: f32, pub liquidation_fee: f32, + pub min_vault_to_deposits_ratio: f64, + pub net_borrows_limit_native: i64, + pub net_borrows_window_size_ts: u64, + pub group: Pubkey, pub admin: TestKeypair, pub mint: Pubkey, @@ -812,6 +816,9 @@ impl ClientInstruction for TokenRegisterInstruction { maint_liab_weight: self.maint_liab_weight, 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_window_size_ts: self.net_borrows_window_size_ts, }; let bank = Pubkey::find_program_address( @@ -1069,6 +1076,82 @@ impl ClientInstruction for TokenResetStablePriceModel { stable_price_delay_interval_seconds_opt: None, 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_window_size_ts_opt: None, + }; + + let accounts = Self::Accounts { + group: self.group, + admin: self.admin.pubkey(), + mint_info: mint_info_key, + oracle: mint_info.oracle, + }; + + let mut instruction = make_instruction(program_id, &accounts, instruction); + instruction + .accounts + .extend(mint_info.banks().iter().map(|&k| AccountMeta { + pubkey: k, + is_signer: false, + is_writable: true, + })); + (accounts, instruction) + } + + fn signers(&self) -> Vec { + vec![self.admin] + } +} + +pub struct TokenEditNetBorrows { + pub group: Pubkey, + pub admin: TestKeypair, + pub mint: Pubkey, + pub min_vault_to_deposits_ratio_opt: Option, + pub net_borrows_limit_native_opt: Option, + pub net_borrows_window_size_ts_opt: Option, +} + +#[async_trait::async_trait(?Send)] +impl ClientInstruction for TokenEditNetBorrows { + type Accounts = mango_v4::accounts::TokenEdit; + type Instruction = mango_v4::instruction::TokenEdit; + async fn to_instruction( + &self, + account_loader: impl ClientAccountLoader + 'async_trait, + ) -> (Self::Accounts, instruction::Instruction) { + let program_id = mango_v4::id(); + + let mint_info_key = Pubkey::find_program_address( + &[ + b"MintInfo".as_ref(), + self.group.as_ref(), + self.mint.as_ref(), + ], + &program_id, + ) + .0; + let mint_info: MintInfo = account_loader.load(&mint_info_key).await.unwrap(); + + let instruction = Self::Instruction { + oracle_opt: None, + oracle_config_opt: None, + group_insurance_fund_opt: None, + interest_rate_params_opt: None, + loan_fee_rate_opt: None, + loan_origination_fee_rate_opt: None, + maint_asset_weight_opt: None, + init_asset_weight_opt: None, + maint_liab_weight_opt: None, + init_liab_weight_opt: None, + liquidation_fee_opt: None, + stable_price_delay_interval_seconds_opt: None, + 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_window_size_ts_opt: self.net_borrows_window_size_ts_opt, }; let accounts = Self::Accounts { diff --git a/programs/mango-v4/tests/program_test/mango_setup.rs b/programs/mango-v4/tests/program_test/mango_setup.rs index 9dcce80a9..9f93272ac 100644 --- a/programs/mango-v4/tests/program_test/mango_setup.rs +++ b/programs/mango-v4/tests/program_test/mango_setup.rs @@ -105,6 +105,9 @@ impl<'a> GroupWithTokensConfig { admin, mint: mint.pubkey, payer, + min_vault_to_deposits_ratio: 0.2, + net_borrows_limit_native: 1_000_000_000_000, + net_borrows_window_size_ts: 24 * 60 * 60, }, ) .await diff --git a/programs/mango-v4/tests/test_borrow_limits.rs b/programs/mango-v4/tests/test_borrow_limits.rs new file mode 100644 index 000000000..9bf3cb9ea --- /dev/null +++ b/programs/mango-v4/tests/test_borrow_limits.rs @@ -0,0 +1,352 @@ +#![cfg(all(feature = "test-bpf"))] + +use mango_setup::*; +use program_test::*; +use solana_program_test::*; +use solana_sdk::transport::TransportError; + +mod program_test; + +#[tokio::test] +async fn test_bank_utilization_based_borrow_limit() -> Result<(), TransportError> { + let context = TestContext::new().await; + let solana = &context.solana.clone(); + + let admin = TestKeypair::new(); + let owner = context.users[0].key; + let payer = context.users[1].key; + let mints = &context.mints[0..2]; + let payer_mint_accounts = &context.users[1].token_accounts[0..=2]; + + let initial_token_deposit = 10_000; + + // + // SETUP: Create a group and an account + // + + let GroupWithTokens { group, .. } = GroupWithTokensConfig { + admin, + payer, + mints: mints.to_vec(), + ..GroupWithTokensConfig::default() + } + .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 + // + { + 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 deposit_amount = initial_token_deposit; + + // account_1 tries to borrow all existing deposits on mint_0 + // should fail because borrow limit would be reached + let res = send_tx( + solana, + TokenWithdrawInstruction { + amount: deposit_amount, + allow_borrow: true, + account: account_1, + owner, + token_account: payer_mint_accounts[0], + bank_index: 0, + }, + ) + .await; + assert!(res.is_err()); + solana.advance_clock().await; + + // account_1 tries to borrow < limit on mint_0 + // should succeed because borrow limit won't be reached + send_tx( + solana, + TokenWithdrawInstruction { + amount: deposit_amount / 10 * 7, + allow_borrow: true, + account: account_1, + owner, + token_account: payer_mint_accounts[0], + bank_index: 0, + }, + ) + .await + .unwrap(); + solana.advance_clock().await; + + // account_0 tries to withdraw all remaining on mint_0 + // should succeed because withdraws without borrows are not limited + send_tx( + solana, + TokenWithdrawInstruction { + amount: deposit_amount / 10 * 3, + allow_borrow: false, + account: account_0, + owner, + token_account: payer_mint_accounts[0], + bank_index: 0, + }, + ) + .await + .unwrap(); + } + + Ok(()) +} + +#[tokio::test] +async fn test_bank_net_borrows_based_borrow_limit() -> Result<(), TransportError> { + let context = TestContext::new().await; + let solana = &context.solana.clone(); + + let admin = TestKeypair::new(); + let owner = context.users[0].key; + let payer = context.users[1].key; + let mints = &context.mints[0..2]; + let payer_mint_accounts = &context.users[1].token_accounts[0..=2]; + + // + // SETUP: Create a group and an account + // + + let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig { + admin, + payer, + mints: mints.to_vec(), + ..GroupWithTokensConfig::default() + } + .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; + + { + 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(); + } + + // + // SETUP: Deposit user funds + // + { + // 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(); + + // 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(); + } + + // We elapse at least 3 seconds, so that next block is in new window + solana.advance_clock().await; + solana.advance_clock().await; + solana.advance_clock().await; + + { + // succeeds because borrow is less than net borrow limit + send_tx( + solana, + TokenWithdrawInstruction { + amount: 5000, + allow_borrow: true, + account: account_1, + owner, + token_account: payer_mint_accounts[0], + bank_index: 0, + }, + ) + .await + .unwrap(); + + // fails because borrow is greater than remaining margin in net borrow limit + let res = send_tx( + solana, + TokenWithdrawInstruction { + amount: 4000, + allow_borrow: true, + account: account_1, + owner, + token_account: payer_mint_accounts[0], + bank_index: 0, + }, + ) + .await; + assert!(res.is_err()); + + // succeeds because is not a borrow + send_tx( + solana, + TokenWithdrawInstruction { + amount: 4000, + allow_borrow: false, + account: account_0, + owner, + token_account: payer_mint_accounts[0], + bank_index: 0, + }, + ) + .await + .unwrap(); + } + + // We elapse at least 3 seconds, so that next block is in new window + solana.advance_clock().await; + solana.advance_clock().await; + solana.advance_clock().await; + + // 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(); + solana.advance_clock().await; + } + + Ok(()) +} diff --git a/ts/client/src/client.ts b/ts/client/src/client.ts index 1f6d7ad9a..61af2b56c 100644 --- a/ts/client/src/client.ts +++ b/ts/client/src/client.ts @@ -229,6 +229,9 @@ export class MangoClient { maintLiabWeight: number, initLiabWeight: number, liquidationFee: number, + minVaultToDepositsRatio: number, + netBorrowsWindowSizeTs: number, + netBorrowsLimitNative: number, ): Promise { return await this.program.methods .tokenRegister( @@ -243,6 +246,9 @@ export class MangoClient { maintLiabWeight, initLiabWeight, liquidationFee, + minVaultToDepositsRatio, + new BN(netBorrowsWindowSizeTs), + new BN(netBorrowsLimitNative), ) .accounts({ group: group.publicKey, @@ -290,6 +296,9 @@ export class MangoClient { maintLiabWeight: number | null, initLiabWeight: number | null, liquidationFee: number | null, + minVaultToDepositsRatio: number | null, + netBorrowsLimitNative: number | null, + netBorrowsWindowSizeTs: number | null, ): Promise { const bank = group.getFirstBankByMint(mintPk); const mintInfo = group.mintInfosMapByTokenIndex.get(bank.tokenIndex)!; @@ -307,6 +316,9 @@ export class MangoClient { maintLiabWeight, initLiabWeight, liquidationFee, + minVaultToDepositsRatio, + netBorrowsLimitNative, + netBorrowsWindowSizeTs, ) .accounts({ group: group.publicKey, @@ -467,7 +479,6 @@ export class MangoClient { group: group.publicKey, admin: (this.program.provider as AnchorProvider).wallet.publicKey, oracle: oraclePk, - payer: (this.program.provider as AnchorProvider).wallet.publicKey, }) .rpc(); } @@ -1444,25 +1455,28 @@ export class MangoClient { public async perpEditMarket( group: Group, perpMarketIndex: PerpMarketIndex, - oracle: PublicKey, - oracleConfig: OracleConfigParams, - baseDecimals: number, - maintAssetWeight: number, - initAssetWeight: number, - maintLiabWeight: number, - initLiabWeight: number, - liquidationFee: number, - makerFee: number, - takerFee: number, - feePenalty: number, - minFunding: number, - maxFunding: number, - impactQuantity: number, - groupInsuranceFund: boolean, - trustedMarket: boolean, - settleFeeFlat: number, - settleFeeAmountThreshold: number, - settleFeeFractionLowHealth: number, + oracle: PublicKey | null, + oracleConfig: OracleConfigParams | null, + baseDecimals: number | null, + maintAssetWeight: number | null, + initAssetWeight: number | null, + maintLiabWeight: number | null, + initLiabWeight: number | null, + liquidationFee: number | null, + makerFee: number | null, + takerFee: number | null, + feePenalty: number | null, + minFunding: number | null, + maxFunding: number | null, + impactQuantity: number | null, + groupInsuranceFund: boolean | null, + trustedMarket: boolean | null, + settleFeeFlat: number | null, + settleFeeAmountThreshold: number | null, + settleFeeFractionLowHealth: number | null, + stablePriceDelayIntervalSeconds: number | null, + stablePriceDelayGrowthLimit: number | null, + stablePriceGrowthLimit: number | null, ): Promise { const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex); @@ -1480,13 +1494,16 @@ export class MangoClient { takerFee, minFunding, maxFunding, - new BN(impactQuantity), + impactQuantity ? new BN(impactQuantity) : null, groupInsuranceFund, trustedMarket, feePenalty, settleFeeFlat, settleFeeAmountThreshold, settleFeeFractionLowHealth, + stablePriceDelayIntervalSeconds, + stablePriceDelayGrowthLimit, + stablePriceGrowthLimit, ) .accounts({ group: group.publicKey, diff --git a/ts/client/src/deployment-scripts/mainnet.ts b/ts/client/src/deployment-scripts/mainnet.ts index 5664ac861..1e768e780 100644 --- a/ts/client/src/deployment-scripts/mainnet.ts +++ b/ts/client/src/deployment-scripts/mainnet.ts @@ -33,6 +33,10 @@ const MAINNET_ORACLES = new Map([ ['MNGO', '79wm3jjcPr6RaNQ4DGvP5KxG1mNd3gEBsg6FsNVFezK4'], ]); +const MIN_VAULT_TO_DEPOSITS_RATIO = 0.2; +const NET_BORROWS_WINDOW_SIZE_TS = 24 * 60 * 60; +const NET_BORROWS_LIMIT_NATIVE = 1 * Math.pow(10, 7) * Math.pow(10, 6); + async function createGroup() { const admin = Keypair.fromSecretKey( Buffer.from( @@ -129,6 +133,9 @@ async function registerTokens() { 1, 1, 0, + MIN_VAULT_TO_DEPOSITS_RATIO, + NET_BORROWS_WINDOW_SIZE_TS, + NET_BORROWS_LIMIT_NATIVE, ); console.log(`Registering USDT...`); @@ -149,6 +156,9 @@ async function registerTokens() { 1.05, 1.1, 0.025, // rule of thumb used - half of maintLiabWeight + MIN_VAULT_TO_DEPOSITS_RATIO, + NET_BORROWS_WINDOW_SIZE_TS, + NET_BORROWS_LIMIT_NATIVE, ); console.log(`Registering BTC...`); @@ -169,6 +179,9 @@ async function registerTokens() { 1.1, 1.2, 0.05, + MIN_VAULT_TO_DEPOSITS_RATIO, + NET_BORROWS_WINDOW_SIZE_TS, + NET_BORROWS_LIMIT_NATIVE, ); console.log(`Registering ETH...`); @@ -189,6 +202,9 @@ async function registerTokens() { 1.1, 1.2, 0.05, + MIN_VAULT_TO_DEPOSITS_RATIO, + NET_BORROWS_WINDOW_SIZE_TS, + NET_BORROWS_LIMIT_NATIVE, ); console.log(`Registering soETH...`); @@ -209,6 +225,9 @@ async function registerTokens() { 1.1, 1.2, 0.05, + MIN_VAULT_TO_DEPOSITS_RATIO, + NET_BORROWS_WINDOW_SIZE_TS, + NET_BORROWS_LIMIT_NATIVE, ); console.log(`Registering SOL...`); @@ -229,6 +248,9 @@ async function registerTokens() { 1.1, 1.2, 0.05, + MIN_VAULT_TO_DEPOSITS_RATIO, + NET_BORROWS_WINDOW_SIZE_TS, + NET_BORROWS_LIMIT_NATIVE, ); console.log(`Registering mSOL...`); @@ -249,6 +271,9 @@ async function registerTokens() { 1.1, 1.2, 0.05, + MIN_VAULT_TO_DEPOSITS_RATIO, + NET_BORROWS_WINDOW_SIZE_TS, + NET_BORROWS_LIMIT_NATIVE, ); // log tokens/banks diff --git a/ts/client/src/mango_v4.ts b/ts/client/src/mango_v4.ts index b8ec3e820..72e59f58b 100644 --- a/ts/client/src/mango_v4.ts +++ b/ts/client/src/mango_v4.ts @@ -341,6 +341,18 @@ export type MangoV4 = { { "name": "liquidationFee", "type": "f32" + }, + { + "name": "minVaultToDepositsRatio", + "type": "f64" + }, + { + "name": "netBorrowsWindowSizeTs", + "type": "u64" + }, + { + "name": "netBorrowsLimitNative", + "type": "i64" } ] }, @@ -499,6 +511,11 @@ export type MangoV4 = { "name": "mintInfo", "isMut": true, "isSigner": false + }, + { + "name": "oracle", + "isMut": false, + "isSigner": false } ], "args": [ @@ -571,6 +588,24 @@ export type MangoV4 = { "type": { "option": "f32" } + }, + { + "name": "stablePriceDelayIntervalSecondsOpt", + "type": { + "option": "u32" + } + }, + { + "name": "stablePriceDelayGrowthLimitOpt", + "type": { + "option": "f32" + } + }, + { + "name": "stablePriceGrowthLimitOpt", + "type": { + "option": "f32" + } } ] }, @@ -1058,11 +1093,6 @@ export type MangoV4 = { "name": "oracle", "isMut": true, "isSigner": false - }, - { - "name": "payer", - "isMut": true, - "isSigner": true } ], "args": [ @@ -2416,6 +2446,11 @@ export type MangoV4 = { "name": "perpMarket", "isMut": true, "isSigner": false + }, + { + "name": "oracle", + "isMut": false, + "isSigner": false } ], "args": [ @@ -2534,6 +2569,24 @@ export type MangoV4 = { "type": { "option": "f32" } + }, + { + "name": "stablePriceDelayIntervalSecondsOpt", + "type": { + "option": "u32" + } + }, + { + "name": "stablePriceDelayGrowthLimitOpt", + "type": { + "option": "f32" + } + }, + { + "name": "stablePriceGrowthLimitOpt", + "type": { + "option": "f32" + } } ] }, @@ -3131,11 +3184,6 @@ export type MangoV4 = { "name": "orderbook", "isMut": true, "isSigner": false - }, - { - "name": "oracle", - "isMut": false, - "isSigner": false } ], "args": [ @@ -3391,11 +3439,11 @@ export type MangoV4 = { }, { "name": "indexLastUpdated", - "type": "i64" + "type": "u64" }, { "name": "bankRateLastUpdated", - "type": "i64" + "type": "u64" }, { "name": "avgUtilization", @@ -3523,12 +3571,38 @@ export type MangoV4 = { "defined": "OracleConfig" } }, + { + "name": "stablePriceModel", + "type": { + "defined": "StablePriceModel" + } + }, + { + "name": "minVaultToDepositsRatio", + "type": "f64" + }, + { + "name": "netBorrowsWindowSizeTs", + "type": "u64" + }, + { + "name": "lastNetBorrowsWindowStartTs", + "type": "u64" + }, + { + "name": "netBorrowsLimitNative", + "type": "i64" + }, + { + "name": "netBorrowsWindowNative", + "type": "i64" + }, { "name": "reserved", "type": { "array": [ "u8", - 2464 + 2136 ] } } @@ -4167,12 +4241,18 @@ export type MangoV4 = { ], "type": "f32" }, + { + "name": "stablePriceModel", + "type": { + "defined": "StablePriceModel" + } + }, { "name": "reserved", "type": { "array": [ "u8", - 2244 + 1956 ] } } @@ -4410,6 +4490,35 @@ export type MangoV4 = { ] } }, + { + "name": "Prices", + "docs": [ + "Information about prices for a bank or perp market." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "oracle", + "docs": [ + "The current oracle price" + ], + "type": { + "defined": "I80F48" + } + }, + { + "name": "stable", + "docs": [ + "A \"stable\" price, provided by StablePriceModel" + ], + "type": { + "defined": "I80F48" + } + } + ] + } + }, { "name": "TokenInfo", "type": { @@ -4444,19 +4553,13 @@ export type MangoV4 = { } }, { - "name": "oraclePrice", + "name": "prices", "type": { - "defined": "I80F48" + "defined": "Prices" } }, { - "name": "balance", - "type": { - "defined": "I80F48" - } - }, - { - "name": "serum3MaxReserved", + "name": "balanceNative", "type": { "defined": "I80F48" } @@ -4470,7 +4573,13 @@ export type MangoV4 = { "kind": "struct", "fields": [ { - "name": "reserved", + "name": "reservedBase", + "type": { + "defined": "I80F48" + } + }, + { + "name": "reservedQuote", "type": { "defined": "I80F48" } @@ -4524,10 +4633,20 @@ export type MangoV4 = { } }, { - "name": "base", - "type": { - "defined": "I80F48" - } + "name": "baseLotSize", + "type": "i64" + }, + { + "name": "baseLots", + "type": "i64" + }, + { + "name": "bidsBaseLots", + "type": "i64" + }, + { + "name": "asksBaseLots", + "type": "i64" }, { "name": "quote", @@ -4536,9 +4655,9 @@ export type MangoV4 = { } }, { - "name": "oraclePrice", + "name": "prices", "type": { - "defined": "I80F48" + "defined": "Prices" } }, { @@ -5387,6 +5506,116 @@ export type MangoV4 = { ] } }, + { + "name": "StablePriceModel", + "docs": [ + "Maintains a \"stable_price\" based on the oracle price.", + "", + "The stable price follows the oracle price, but its relative rate of", + "change is limited (to `stable_growth_limit`) and futher reduced if", + "the oracle price is far from the `delay_price`.", + "", + "Conceptually the `delay_price` is itself a time delayed", + "(`24 * delay_interval_seconds`, assume 24h) and relative rate of change limited", + "function of the oracle price. It is implemented as averaging the oracle", + "price over every `delay_interval_seconds` (assume 1h) and then applying the", + "`delay_growth_limit` between intervals." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "stablePrice", + "docs": [ + "Current stable price to use in health" + ], + "type": "f64" + }, + { + "name": "lastUpdateTimestamp", + "type": "u64" + }, + { + "name": "delayPrices", + "docs": [ + "Stored delay_price for each delay_interval.", + "If we want the delay_price to be 24h delayed, we would store one for each hour.", + "This is used in a cyclical way: We use the maximally-delayed value at delay_interval_index", + "and once enough time passes to move to the next delay interval, that gets overwritten and", + "we use the next one." + ], + "type": { + "array": [ + "f64", + 24 + ] + } + }, + { + "name": "delayAccumulatorPrice", + "docs": [ + "The delay price is based on an average over each delay_interval. The contributions", + "to the average are summed up here." + ], + "type": "f64" + }, + { + "name": "delayAccumulatorTime", + "docs": [ + "Accumulating the total time for the above average." + ], + "type": "u32" + }, + { + "name": "delayIntervalSeconds", + "docs": [ + "Length of a delay_interval" + ], + "type": "u32" + }, + { + "name": "delayGrowthLimit", + "docs": [ + "Maximal relative difference between two delay_price in consecutive intervals." + ], + "type": "f32" + }, + { + "name": "stableGrowthLimit", + "docs": [ + "Maximal per-second relative difference of the stable price.", + "It gets further reduced if stable and delay price disagree." + ], + "type": "f32" + }, + { + "name": "lastDelayIntervalIndex", + "docs": [ + "The delay_interval_index that update() was last called on." + ], + "type": "u8" + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 7 + ] + } + }, + { + "name": "reserved", + "type": { + "array": [ + "u8", + 48 + ] + } + } + ] + } + }, { "name": "TokenIndex", "docs": [ @@ -6140,6 +6369,11 @@ export type MangoV4 = { "type": "i128", "index": false }, + { + "name": "stablePrice", + "type": "i128", + "index": false + }, { "name": "feesAccrued", "type": "i128", @@ -6185,6 +6419,11 @@ export type MangoV4 = { "type": "i128", "index": false }, + { + "name": "stablePrice", + "type": "i128", + "index": false + }, { "name": "collectedFees", "type": "i128", @@ -6880,6 +7119,21 @@ export type MangoV4 = { "code": 6024, "name": "OracleStale", "msg": "an oracle is stale" + }, + { + "code": 6025, + "name": "SettlementAmountMustBePositive", + "msg": "settlement amount must always be positive" + }, + { + "code": 6026, + "name": "BankBorrowLimitReached", + "msg": "bank utilization has reached limit" + }, + { + "code": 6027, + "name": "BankNetBorrowsLimitReached", + "msg": "bank net borrows has reached limit" } ] }; @@ -7227,6 +7481,18 @@ export const IDL: MangoV4 = { { "name": "liquidationFee", "type": "f32" + }, + { + "name": "minVaultToDepositsRatio", + "type": "f64" + }, + { + "name": "netBorrowsWindowSizeTs", + "type": "u64" + }, + { + "name": "netBorrowsLimitNative", + "type": "i64" } ] }, @@ -7385,6 +7651,11 @@ export const IDL: MangoV4 = { "name": "mintInfo", "isMut": true, "isSigner": false + }, + { + "name": "oracle", + "isMut": false, + "isSigner": false } ], "args": [ @@ -7457,6 +7728,24 @@ export const IDL: MangoV4 = { "type": { "option": "f32" } + }, + { + "name": "stablePriceDelayIntervalSecondsOpt", + "type": { + "option": "u32" + } + }, + { + "name": "stablePriceDelayGrowthLimitOpt", + "type": { + "option": "f32" + } + }, + { + "name": "stablePriceGrowthLimitOpt", + "type": { + "option": "f32" + } } ] }, @@ -7944,11 +8233,6 @@ export const IDL: MangoV4 = { "name": "oracle", "isMut": true, "isSigner": false - }, - { - "name": "payer", - "isMut": true, - "isSigner": true } ], "args": [ @@ -9302,6 +9586,11 @@ export const IDL: MangoV4 = { "name": "perpMarket", "isMut": true, "isSigner": false + }, + { + "name": "oracle", + "isMut": false, + "isSigner": false } ], "args": [ @@ -9420,6 +9709,24 @@ export const IDL: MangoV4 = { "type": { "option": "f32" } + }, + { + "name": "stablePriceDelayIntervalSecondsOpt", + "type": { + "option": "u32" + } + }, + { + "name": "stablePriceDelayGrowthLimitOpt", + "type": { + "option": "f32" + } + }, + { + "name": "stablePriceGrowthLimitOpt", + "type": { + "option": "f32" + } } ] }, @@ -10017,11 +10324,6 @@ export const IDL: MangoV4 = { "name": "orderbook", "isMut": true, "isSigner": false - }, - { - "name": "oracle", - "isMut": false, - "isSigner": false } ], "args": [ @@ -10277,11 +10579,11 @@ export const IDL: MangoV4 = { }, { "name": "indexLastUpdated", - "type": "i64" + "type": "u64" }, { "name": "bankRateLastUpdated", - "type": "i64" + "type": "u64" }, { "name": "avgUtilization", @@ -10409,12 +10711,38 @@ export const IDL: MangoV4 = { "defined": "OracleConfig" } }, + { + "name": "stablePriceModel", + "type": { + "defined": "StablePriceModel" + } + }, + { + "name": "minVaultToDepositsRatio", + "type": "f64" + }, + { + "name": "netBorrowsWindowSizeTs", + "type": "u64" + }, + { + "name": "lastNetBorrowsWindowStartTs", + "type": "u64" + }, + { + "name": "netBorrowsLimitNative", + "type": "i64" + }, + { + "name": "netBorrowsWindowNative", + "type": "i64" + }, { "name": "reserved", "type": { "array": [ "u8", - 2464 + 2136 ] } } @@ -11053,12 +11381,18 @@ export const IDL: MangoV4 = { ], "type": "f32" }, + { + "name": "stablePriceModel", + "type": { + "defined": "StablePriceModel" + } + }, { "name": "reserved", "type": { "array": [ "u8", - 2244 + 1956 ] } } @@ -11296,6 +11630,35 @@ export const IDL: MangoV4 = { ] } }, + { + "name": "Prices", + "docs": [ + "Information about prices for a bank or perp market." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "oracle", + "docs": [ + "The current oracle price" + ], + "type": { + "defined": "I80F48" + } + }, + { + "name": "stable", + "docs": [ + "A \"stable\" price, provided by StablePriceModel" + ], + "type": { + "defined": "I80F48" + } + } + ] + } + }, { "name": "TokenInfo", "type": { @@ -11330,19 +11693,13 @@ export const IDL: MangoV4 = { } }, { - "name": "oraclePrice", + "name": "prices", "type": { - "defined": "I80F48" + "defined": "Prices" } }, { - "name": "balance", - "type": { - "defined": "I80F48" - } - }, - { - "name": "serum3MaxReserved", + "name": "balanceNative", "type": { "defined": "I80F48" } @@ -11356,7 +11713,13 @@ export const IDL: MangoV4 = { "kind": "struct", "fields": [ { - "name": "reserved", + "name": "reservedBase", + "type": { + "defined": "I80F48" + } + }, + { + "name": "reservedQuote", "type": { "defined": "I80F48" } @@ -11410,10 +11773,20 @@ export const IDL: MangoV4 = { } }, { - "name": "base", - "type": { - "defined": "I80F48" - } + "name": "baseLotSize", + "type": "i64" + }, + { + "name": "baseLots", + "type": "i64" + }, + { + "name": "bidsBaseLots", + "type": "i64" + }, + { + "name": "asksBaseLots", + "type": "i64" }, { "name": "quote", @@ -11422,9 +11795,9 @@ export const IDL: MangoV4 = { } }, { - "name": "oraclePrice", + "name": "prices", "type": { - "defined": "I80F48" + "defined": "Prices" } }, { @@ -12273,6 +12646,116 @@ export const IDL: MangoV4 = { ] } }, + { + "name": "StablePriceModel", + "docs": [ + "Maintains a \"stable_price\" based on the oracle price.", + "", + "The stable price follows the oracle price, but its relative rate of", + "change is limited (to `stable_growth_limit`) and futher reduced if", + "the oracle price is far from the `delay_price`.", + "", + "Conceptually the `delay_price` is itself a time delayed", + "(`24 * delay_interval_seconds`, assume 24h) and relative rate of change limited", + "function of the oracle price. It is implemented as averaging the oracle", + "price over every `delay_interval_seconds` (assume 1h) and then applying the", + "`delay_growth_limit` between intervals." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "stablePrice", + "docs": [ + "Current stable price to use in health" + ], + "type": "f64" + }, + { + "name": "lastUpdateTimestamp", + "type": "u64" + }, + { + "name": "delayPrices", + "docs": [ + "Stored delay_price for each delay_interval.", + "If we want the delay_price to be 24h delayed, we would store one for each hour.", + "This is used in a cyclical way: We use the maximally-delayed value at delay_interval_index", + "and once enough time passes to move to the next delay interval, that gets overwritten and", + "we use the next one." + ], + "type": { + "array": [ + "f64", + 24 + ] + } + }, + { + "name": "delayAccumulatorPrice", + "docs": [ + "The delay price is based on an average over each delay_interval. The contributions", + "to the average are summed up here." + ], + "type": "f64" + }, + { + "name": "delayAccumulatorTime", + "docs": [ + "Accumulating the total time for the above average." + ], + "type": "u32" + }, + { + "name": "delayIntervalSeconds", + "docs": [ + "Length of a delay_interval" + ], + "type": "u32" + }, + { + "name": "delayGrowthLimit", + "docs": [ + "Maximal relative difference between two delay_price in consecutive intervals." + ], + "type": "f32" + }, + { + "name": "stableGrowthLimit", + "docs": [ + "Maximal per-second relative difference of the stable price.", + "It gets further reduced if stable and delay price disagree." + ], + "type": "f32" + }, + { + "name": "lastDelayIntervalIndex", + "docs": [ + "The delay_interval_index that update() was last called on." + ], + "type": "u8" + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 7 + ] + } + }, + { + "name": "reserved", + "type": { + "array": [ + "u8", + 48 + ] + } + } + ] + } + }, { "name": "TokenIndex", "docs": [ @@ -13026,6 +13509,11 @@ export const IDL: MangoV4 = { "type": "i128", "index": false }, + { + "name": "stablePrice", + "type": "i128", + "index": false + }, { "name": "feesAccrued", "type": "i128", @@ -13071,6 +13559,11 @@ export const IDL: MangoV4 = { "type": "i128", "index": false }, + { + "name": "stablePrice", + "type": "i128", + "index": false + }, { "name": "collectedFees", "type": "i128", @@ -13766,6 +14259,21 @@ export const IDL: MangoV4 = { "code": 6024, "name": "OracleStale", "msg": "an oracle is stale" + }, + { + "code": 6025, + "name": "SettlementAmountMustBePositive", + "msg": "settlement amount must always be positive" + }, + { + "code": 6026, + "name": "BankBorrowLimitReached", + "msg": "bank utilization has reached limit" + }, + { + "code": 6027, + "name": "BankNetBorrowsLimitReached", + "msg": "bank net borrows has reached limit" } ] }; diff --git a/ts/client/src/scripts/devnet-admin.ts b/ts/client/src/scripts/devnet-admin.ts index 398ebb3a3..0ece80b35 100644 --- a/ts/client/src/scripts/devnet-admin.ts +++ b/ts/client/src/scripts/devnet-admin.ts @@ -44,6 +44,10 @@ const DEVNET_ORACLES = new Map([ ['SRM', '992moaMQKs32GKZ9dxi8keyM2bUmbrwBZpK4p2K6X5Vs'], ]); +const MIN_VAULT_TO_DEPOSITS_RATIO = 0.2; +const NET_BORROWS_WINDOW_SIZE_TS = 24 * 60 * 60; +const NET_BORROWS_LIMIT_NATIVE = 1 * Math.pow(10, 7) * Math.pow(10, 6); + const GROUP_NUM = Number(process.env.GROUP_NUM || 0); async function main() { @@ -122,6 +126,9 @@ async function main() { 1, 1, 0, + MIN_VAULT_TO_DEPOSITS_RATIO, + NET_BORROWS_WINDOW_SIZE_TS, + NET_BORROWS_LIMIT_NATIVE, ); await group.reloadAll(client); } catch (error) {} @@ -146,6 +153,9 @@ async function main() { 1.1, 1.2, 0.05, + MIN_VAULT_TO_DEPOSITS_RATIO, + NET_BORROWS_WINDOW_SIZE_TS, + NET_BORROWS_LIMIT_NATIVE, ); await group.reloadAll(client); } catch (error) { @@ -172,6 +182,9 @@ async function main() { 1.1, 1.2, 0.05, + MIN_VAULT_TO_DEPOSITS_RATIO, + NET_BORROWS_WINDOW_SIZE_TS, + NET_BORROWS_LIMIT_NATIVE, ); await group.reloadAll(client); } catch (error) { @@ -205,6 +218,9 @@ async function main() { 1.2, 1.4, 0.02, + MIN_VAULT_TO_DEPOSITS_RATIO, + NET_BORROWS_WINDOW_SIZE_TS, + NET_BORROWS_LIMIT_NATIVE, ); await group.reloadAll(client); } catch (error) { @@ -231,6 +247,9 @@ async function main() { 1.1, 1.2, 0.05, + MIN_VAULT_TO_DEPOSITS_RATIO, + NET_BORROWS_WINDOW_SIZE_TS, + NET_BORROWS_LIMIT_NATIVE, ); await group.reloadAll(client); } catch (error) { @@ -257,6 +276,9 @@ async function main() { 1.1, 1.2, 0.05, + MIN_VAULT_TO_DEPOSITS_RATIO, + NET_BORROWS_WINDOW_SIZE_TS, + NET_BORROWS_LIMIT_NATIVE, ); await group.reloadAll(client); } catch (error) { @@ -403,6 +425,9 @@ async function main() { 1, 1, 0, + MIN_VAULT_TO_DEPOSITS_RATIO, + NET_BORROWS_WINDOW_SIZE_TS, + NET_BORROWS_LIMIT_NATIVE, ); console.log(`https://explorer.solana.com/tx/${sig}?cluster=devnet`); await group.reloadAll(client); @@ -427,6 +452,9 @@ async function main() { 1.1, 1.2, 0.05, + MIN_VAULT_TO_DEPOSITS_RATIO, + NET_BORROWS_WINDOW_SIZE_TS, + NET_BORROWS_LIMIT_NATIVE, ); console.log(`https://explorer.solana.com/tx/${sig}?cluster=devnet`); await group.reloadAll(client); @@ -451,6 +479,9 @@ async function main() { 1.1, 1.2, 0.05, + MIN_VAULT_TO_DEPOSITS_RATIO, + NET_BORROWS_WINDOW_SIZE_TS, + NET_BORROWS_LIMIT_NATIVE, ); console.log(`https://explorer.solana.com/tx/${sig}?cluster=devnet`); await group.reloadAll(client); @@ -483,6 +514,9 @@ async function main() { 1000, 1000000, 0.05, + MIN_VAULT_TO_DEPOSITS_RATIO, + NET_BORROWS_WINDOW_SIZE_TS, + NET_BORROWS_LIMIT_NATIVE, ); console.log(`https://explorer.solana.com/tx/${sig}?cluster=devnet`); await group.reloadAll(client); diff --git a/ts/client/src/scripts/mb-example1-admin.ts b/ts/client/src/scripts/mb-example1-admin.ts index df66d12d1..029972e81 100644 --- a/ts/client/src/scripts/mb-example1-admin.ts +++ b/ts/client/src/scripts/mb-example1-admin.ts @@ -50,6 +50,10 @@ const MAINNET_SERUM3_MARKETS = new Map([ ['DUST/SOL', '8WCzJpSNcLUYXPYeUDAXpH4hgqxFJpkYkVT6GJDSpcGx'], ]); +const MIN_VAULT_TO_DEPOSITS_RATIO = 0.2; +const NET_BORROWS_WINDOW_SIZE_TS = 24 * 60 * 60; +const NET_BORROWS_LIMIT_NATIVE = 1 * Math.pow(10, 7) * Math.pow(10, 6); + const { MB_CLUSTER_URL, MB_PAYER_KEYPAIR, MB_USER_KEYPAIR, MB_USER2_KEYPAIR } = process.env; @@ -160,6 +164,9 @@ async function registerTokens() { 1, 1, 0, + MIN_VAULT_TO_DEPOSITS_RATIO, + NET_BORROWS_WINDOW_SIZE_TS, + NET_BORROWS_LIMIT_NATIVE, ); console.log(`Registering USDT...`); @@ -180,6 +187,9 @@ async function registerTokens() { 1.05, 1.1, 0.025, + MIN_VAULT_TO_DEPOSITS_RATIO, + NET_BORROWS_WINDOW_SIZE_TS, + NET_BORROWS_LIMIT_NATIVE, ); console.log(`Registering BTC...`); @@ -200,6 +210,9 @@ async function registerTokens() { 1.1, 1.2, 0.05, + MIN_VAULT_TO_DEPOSITS_RATIO, + NET_BORROWS_WINDOW_SIZE_TS, + NET_BORROWS_LIMIT_NATIVE, ); console.log(`Registering ETH...`); @@ -220,6 +233,9 @@ async function registerTokens() { 1.1, 1.2, 0.05, + MIN_VAULT_TO_DEPOSITS_RATIO, + NET_BORROWS_WINDOW_SIZE_TS, + NET_BORROWS_LIMIT_NATIVE, ); console.log(`Registering soETH...`); @@ -240,6 +256,9 @@ async function registerTokens() { 1.1, 1.2, 0.05, + MIN_VAULT_TO_DEPOSITS_RATIO, + NET_BORROWS_WINDOW_SIZE_TS, + NET_BORROWS_LIMIT_NATIVE, ); console.log(`Registering SOL...`); @@ -260,6 +279,9 @@ async function registerTokens() { 1.1, 1.2, 0.05, + MIN_VAULT_TO_DEPOSITS_RATIO, + NET_BORROWS_WINDOW_SIZE_TS, + NET_BORROWS_LIMIT_NATIVE, ); console.log(`Registering MSOL...`); @@ -280,6 +302,9 @@ async function registerTokens() { 1.1, 1.2, 0.05, + MIN_VAULT_TO_DEPOSITS_RATIO, + NET_BORROWS_WINDOW_SIZE_TS, + NET_BORROWS_LIMIT_NATIVE, ); console.log(`Registering RAY...`); const rayMainnetMint = new PublicKey(MAINNET_MINTS.get('RAY')!); @@ -306,6 +331,9 @@ async function registerTokens() { 8 / 7, 4 / 3, 1 / 16, + MIN_VAULT_TO_DEPOSITS_RATIO, + NET_BORROWS_WINDOW_SIZE_TS, + NET_BORROWS_LIMIT_NATIVE, ); console.log(`Registering DUST...`); @@ -333,6 +361,9 @@ async function registerTokens() { 81 / 80, 41 / 40, // 40x leverage so we can test something 1 / 160, // no liquidation fee + MIN_VAULT_TO_DEPOSITS_RATIO, + NET_BORROWS_WINDOW_SIZE_TS, + NET_BORROWS_LIMIT_NATIVE, ); // log tokens/banks diff --git a/ts/client/src/scripts/mb-liqtest-create-group.ts b/ts/client/src/scripts/mb-liqtest-create-group.ts index a0b43d1af..d296e0e4f 100644 --- a/ts/client/src/scripts/mb-liqtest-create-group.ts +++ b/ts/client/src/scripts/mb-liqtest-create-group.ts @@ -33,6 +33,10 @@ const MAINNET_SERUM3_MARKETS = new Map([ ['SOL/USDC', '9wFFyRfZBsuAha4YcuxcXLKwMxJR43S7fPfQLusDBzvT'], ]); +const MIN_VAULT_TO_DEPOSITS_RATIO = 0.2; +const NET_BORROWS_WINDOW_SIZE_TS = 24 * 60 * 60; +const NET_BORROWS_LIMIT_NATIVE = 1 * Math.pow(10, 7) * Math.pow(10, 6); + async function main() { const options = AnchorProvider.defaultOptions(); const connection = new Connection(process.env.CLUSTER_URL!, options); @@ -116,6 +120,9 @@ async function main() { 1, 1, 0, + MIN_VAULT_TO_DEPOSITS_RATIO, + NET_BORROWS_WINDOW_SIZE_TS, + NET_BORROWS_LIMIT_NATIVE, ); await group.reloadAll(client); } catch (error) { @@ -142,6 +149,9 @@ async function main() { 1.1, 1.2, 0.05, + MIN_VAULT_TO_DEPOSITS_RATIO, + NET_BORROWS_WINDOW_SIZE_TS, + NET_BORROWS_LIMIT_NATIVE, ); await group.reloadAll(client); } catch (error) { @@ -168,6 +178,9 @@ async function main() { 1.1, 1.2, 0.05, + MIN_VAULT_TO_DEPOSITS_RATIO, + NET_BORROWS_WINDOW_SIZE_TS, + NET_BORROWS_LIMIT_NATIVE, ); await group.reloadAll(client); } catch (error) { diff --git a/ts/client/src/scripts/mb-liqtest-make-candidates.ts b/ts/client/src/scripts/mb-liqtest-make-candidates.ts index a8656b90f..390f98256 100644 --- a/ts/client/src/scripts/mb-liqtest-make-candidates.ts +++ b/ts/client/src/scripts/mb-liqtest-make-candidates.ts @@ -167,6 +167,9 @@ async function main() { 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. @@ -199,6 +202,9 @@ async function main() { null, null, null, + null, + null, + null, ); } }