diff --git a/CHANGELOG.md b/CHANGELOG.md index d1403c5cf..24eabe1cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,60 @@ Update this for each program release and mainnet deployment. ## not on mainnet -### v0.20.0, 2023-10- +### v0.21.0, 2023-12- + +- Introduce deposit limits (#806) + + The DAO can now configure hard deposit limits per token. They can be used in + conjunction with the previous soft limits to restrict how much of a token can + be on the platform providing collateral weight. + +- Improve OpenBook order tracking and price bands (#805) + + In order for hard deposit limits to work, OpenBook orders need to be tracked + and potentially restricted. The DAO can now configure a band around the oracle + price and new bids and asks that don't fall within this band will be rejected. + +- SerumPlaceOrderV2 breaking change (#805) + + A new instruction for placing orders on OpenBook markets is introduced. The + old instruction should be disabled shortly after release. + +- Changing token maint weights over time (#780) + + The DAO can now trigger a gradual change in token maint weights. This allows + it to make maint weights less favorable without potentially causing many + liquidations at the same time. + +- Changed perp settlement incentives (#771) + + The incentives were too high when the user account was close to liquidation. + The DAO had previously reduced the percentage amount as a mitigation. + + With this change: + - low-health settlement incentives are capped at 2x the flat fee, removing + unlimited percentual incentive fees entirely + - incentives are only paid if at least 1% of position value is settled, + avoiding the incentive to settle accounts with large positions very frequently + +- More configurable token interest rate curve (#755) + + The scaling factor and target utilization are now stored separately, giving the + DAO more flexibility for configuration. + +- Delegates can now deposit even when a new token position needs to be created (#775) +- TokenRegister: Add argument for insurance (#782) +- Close zero token positions when user asks to withdraw everything (#793) +- Fix default parameters for fast listing tokens (#804) +- Disable TokenAddBank instruction, which was unused (#803) +- Significantly reduce program heap use (#787, #785) +- Reduce compute use of OpenBook health computations (#750) + +## mainnet + +### v0.20.0, 2023-11-8 + +Deployment: Nov 8, 2023 at 10:44:24 Central European Standard Time, https://explorer.solana.com/tx/4LM5NJAa71tjjKT4a7MXVVsautU1DNvszbXp2ufeps9gMrksRh9pURRiacoyCEgW9gdBYJb1W3TL6o7dzDcUVmVH - Token conditional swaps: Add two auction mechanisms (#717) @@ -46,8 +99,6 @@ Update this for each program release and mainnet deployment. - Fix computing maximum allowed amount when swapping zero asset-weight tokens (#699) - Fix too-strict validation of max rate on token edit (#734) -## mainnet - ### v0.19.1, 2023-9-16 Deployment: Sep 16, 2023 at 11:20:20 Central European Summer Time, https://explorer.solana.com/tx/K9BJ1uDBH6Xe8erhS6C8Rmz6k6V1cKJ8z6wNmf4DV2aF5Woin4H5xXKj1ypTNDSTccNvcsAUTHStoai3k2hYY5E diff --git a/Cargo.lock b/Cargo.lock index 825be7af7..ad3501cda 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3301,7 +3301,7 @@ dependencies = [ [[package]] name = "mango-v4" -version = "0.20.0" +version = "0.21.0" dependencies = [ "anchor-lang", "anchor-spl", diff --git a/bin/liquidator/src/liquidate.rs b/bin/liquidator/src/liquidate.rs index d754bb6cd..e551c39a6 100644 --- a/bin/liquidator/src/liquidate.rs +++ b/bin/liquidator/src/liquidate.rs @@ -309,7 +309,7 @@ impl<'a> LiquidateHelper<'a> { // TODO: This is where we could multiply in the liquidation fee factors let price = source_price / target_price; - util::max_swap_source( + util::max_swap_source_ignoring_limits( self.client, self.account_fetcher, &liqor, diff --git a/bin/liquidator/src/trigger_tcs.rs b/bin/liquidator/src/trigger_tcs.rs index f0e683b13..96e3267d9 100644 --- a/bin/liquidator/src/trigger_tcs.rs +++ b/bin/liquidator/src/trigger_tcs.rs @@ -26,10 +26,9 @@ use crate::{token_swap_info, util, ErrorTracking}; /// making the whole execution fail. const SLIPPAGE_BUFFER: f64 = 0.01; // 1% -/// If a tcs gets limited due to exhausted net borrows, don't trigger execution if -/// the possible value is below this amount. This avoids spamming executions when net -/// borrows are exhausted. -const NET_BORROW_EXECUTION_THRESHOLD: u64 = 1_000_000; // 1 USD +/// If a tcs gets limited due to exhausted net borrows or deposit limits, don't trigger execution if +/// the possible value is below this amount. This avoids spamming executions when limits are exhausted. +const EXECUTION_THRESHOLD: u64 = 1_000_000; // 1 USD #[derive(Clone, Debug, PartialEq, Eq)] pub enum Mode { @@ -440,12 +439,17 @@ impl Context { /// This includes /// - tcs restrictions (remaining buy/sell, create borrows/deposits) /// - reduce only banks - /// - net borrow limits on BOTH sides, even though the buy side is technically - /// a liqor limitation: the liqor could acquire the token before trying the - /// execution... but in practice the liqor will work on margin + /// - net borrow limits: + /// - the account may borrow the sell token (and the liqor side may not be a repay) + /// - the liqor may borrow the buy token (and the account side may not be a repay) + /// this is technically a liqor limitation: the liqor could acquire the token before trying the + /// execution... but in practice the liqor may work on margin + /// - deposit limits: + /// - the account may deposit the buy token (while the liqor borrowed it) + /// - the liqor may deposit the sell token (while the account borrowed it) /// /// Returns Some((native buy amount, native sell amount)) if execution is sensible - /// Returns None if the execution should be skipped (due to net borrow limits...) + /// Returns None if the execution should be skipped (due to limits) pub fn tcs_max_liqee_execution( &self, account: &MangoAccountValue, @@ -458,18 +462,18 @@ impl Context { let premium_price = tcs.premium_price(base_price.to_num(), self.now_ts); let maker_price = tcs.maker_price(premium_price); - let buy_position = account + let liqee_buy_position = account .token_position(tcs.buy_token_index) .map(|p| p.native(&buy_bank)) .unwrap_or(I80F48::ZERO); - let sell_position = account + let liqee_sell_position = account .token_position(tcs.sell_token_index) .map(|p| p.native(&sell_bank)) .unwrap_or(I80F48::ZERO); // this is in "buy token received per sell token given" units let swap_price = I80F48::from_num((1.0 - SLIPPAGE_BUFFER) / maker_price); - let max_sell_ignoring_net_borrows = util::max_swap_source_ignore_net_borrows( + let max_sell_ignoring_limits = util::max_swap_source_ignoring_limits( &self.mango_client, &self.account_fetcher, account, @@ -480,41 +484,31 @@ impl Context { )? .floor() .to_num::() - .min(tcs.max_sell_for_position(sell_position, &sell_bank)); + .min(tcs.max_sell_for_position(liqee_sell_position, &sell_bank)); - let max_buy_ignoring_net_borrows = tcs.max_buy_for_position(buy_position, &buy_bank); + let max_buy_ignoring_limits = tcs.max_buy_for_position(liqee_buy_position, &buy_bank); - // What follows is a complex manual handling of net borrow limits, for the following reason: + // What follows is a complex manual handling of net borrow/deposit limits, for + // the following reason: // Usually, we want to execute tcs even for small amounts because that will close the // tcs order: either due to full execution or due to the health threshold being reached. // - // However, when the net borrow limits are hit, it will not closed when no further execution - // is possible, because net borrow limit issues are considered transient. Furthermore, we - // don't even want to send a tiny tcs trigger transactions, because there's a good chance we - // would then be sending lot of those as oracle prices fluctuate. + // However, when the limits are hit, it will not closed when no further execution + // is possible, because limit issues are transient. Furthermore, we don't want to send + // tiny tcs trigger transactions, because there's a good chance we would then be sending + // lot of those as oracle prices fluctuate. // // Thus, we need to detect if the possible execution amount is tiny _because_ of the - // net borrow limits. Then skip. If it's tiny for other reasons we can proceed. + // limits. Then skip. If it's tiny for other reasons we can proceed. - fn available_borrows(bank: &Bank, price: I80F48) -> u64 { - (bank.remaining_net_borrows_quote(price) / price).clamp_to_u64() - } - let available_buy_borrows = available_borrows(&buy_bank, buy_token_price); - let available_sell_borrows = available_borrows(&sell_bank, sell_token_price); - - // New borrows if max_sell_ignoring_net_borrows was withdrawn on the liqee - let sell_borrows = (I80F48::from(max_sell_ignoring_net_borrows) - - sell_position.max(I80F48::ZERO)) - .clamp_to_u64(); - - // On the buy side, the liqor might need to borrow - let buy_borrows = match self.config.mode { + // Do the liqor buy tokens come from deposits or are they borrowed? + let mut liqor_buy_borrows = match self.config.mode { Mode::BorrowBuyToken => { // Assume that the liqor has enough buy token if it's collateral if tcs.buy_token_index == self.config.collateral_token_index { 0 } else { - max_buy_ignoring_net_borrows + max_buy_ignoring_limits } } Mode::SwapCollateralIntoBuy { .. } => 0, @@ -525,19 +519,77 @@ impl Context { } }; - // New maximums adjusted for net borrow limits - let max_sell = - max_sell_ignoring_net_borrows - sell_borrows + sell_borrows.min(available_sell_borrows); - let max_buy = - max_buy_ignoring_net_borrows - buy_borrows + buy_borrows.min(available_buy_borrows); + // First, net borrow limits + let max_sell_net_borrows; + let max_buy_net_borrows; + { + fn available_borrows(bank: &Bank, price: I80F48) -> u64 { + bank.remaining_net_borrows_quote(price) + .saturating_div(price) + .clamp_to_u64() + } + let available_buy_borrows = available_borrows(&buy_bank, buy_token_price); + let available_sell_borrows = available_borrows(&sell_bank, sell_token_price); - let tiny_due_to_net_borrows = { - let buy_threshold = I80F48::from(NET_BORROW_EXECUTION_THRESHOLD) / buy_token_price; - let sell_threshold = I80F48::from(NET_BORROW_EXECUTION_THRESHOLD) / sell_token_price; - max_buy < buy_threshold && max_buy_ignoring_net_borrows > buy_threshold - || max_sell < sell_threshold && max_sell_ignoring_net_borrows > sell_threshold + // New borrows if max_sell_ignoring_limits was withdrawn on the liqee + // We assume that on the liqor side the position is >= 0, so these are true + // new borrows. + let sell_borrows = (I80F48::from(max_sell_ignoring_limits) + - liqee_sell_position.max(I80F48::ZERO)) + .ceil() + .clamp_to_u64(); + + // On the buy side, the liqor might need to borrow, see liqor_buy_borrows. + // On the liqee side, the bought tokens may repay a borrow, reducing net borrows again + let buy_borrows = (I80F48::from(liqor_buy_borrows) + + liqee_buy_position.min(I80F48::ZERO)) + .ceil() + .clamp_to_u64(); + + // New maximums adjusted for net borrow limits + max_sell_net_borrows = max_sell_ignoring_limits + - (sell_borrows - sell_borrows.min(available_sell_borrows)); + max_buy_net_borrows = + max_buy_ignoring_limits - (buy_borrows - buy_borrows.min(available_buy_borrows)); + liqor_buy_borrows = liqor_buy_borrows.min(max_buy_net_borrows); + } + + // Second, deposit limits + let max_sell; + let max_buy; + { + let available_buy_deposits = buy_bank.remaining_deposits_until_limit().clamp_to_u64(); + let available_sell_deposits = sell_bank.remaining_deposits_until_limit().clamp_to_u64(); + + // New deposits on the liqee side (reduced by repaid borrows) + let liqee_buy_deposits = (I80F48::from(max_buy_net_borrows) + + liqee_buy_position.min(I80F48::ZERO)) + .ceil() + .clamp_to_u64(); + // the net new deposits can only be as big as the liqor borrows + // (assume no borrows, then deposits only move from liqor to liqee) + let buy_deposits = liqee_buy_deposits.min(liqor_buy_borrows); + + // We assume the liqor position is always >= 0, meaning there are new sell token deposits if + // the sell token gets borrowed on the liqee side. + let sell_deposits = (I80F48::from(max_sell_net_borrows) + - liqee_sell_position.max(I80F48::ZERO)) + .ceil() + .clamp_to_u64(); + + max_sell = + max_sell_net_borrows - (sell_deposits - sell_deposits.min(available_sell_deposits)); + max_buy = + max_buy_net_borrows - (buy_deposits - buy_deposits.min(available_buy_deposits)); + } + + let tiny_due_to_limits = { + let buy_threshold = I80F48::from(EXECUTION_THRESHOLD) / buy_token_price; + let sell_threshold = I80F48::from(EXECUTION_THRESHOLD) / sell_token_price; + max_buy < buy_threshold && max_buy_ignoring_limits > buy_threshold + || max_sell < sell_threshold && max_sell_ignoring_limits > sell_threshold }; - if tiny_due_to_net_borrows { + if tiny_due_to_limits { return Ok(None); } @@ -715,7 +767,7 @@ impl Context { .0 .native(&buy_bank); let liqor_available_buy_token = match mode { - Mode::BorrowBuyToken => util::max_swap_source( + Mode::BorrowBuyToken => util::max_swap_source_with_limits( &self.mango_client, &self.account_fetcher, &liqor, @@ -734,7 +786,7 @@ impl Context { self.token_bank_price_mint(collateral_token_index)?; let buy_per_collateral_price = (collateral_price / buy_token_price) * I80F48::from_num(jupiter_slippage_fraction); - let collateral_amount = util::max_swap_source( + let collateral_amount = util::max_swap_source_with_limits( &self.mango_client, &self.account_fetcher, &liqor, @@ -751,7 +803,7 @@ impl Context { // How big can the sell -> buy swap be? let buy_per_sell_price = (I80F48::from(1) / taker_price) * I80F48::from_num(jupiter_slippage_fraction); - let max_sell = util::max_swap_source( + let max_sell = util::max_swap_source_with_limits( &self.mango_client, &self.account_fetcher, &liqor, diff --git a/bin/liquidator/src/util.rs b/bin/liquidator/src/util.rs index 44d9348af..fbd31ba0f 100644 --- a/bin/liquidator/src/util.rs +++ b/bin/liquidator/src/util.rs @@ -38,7 +38,9 @@ pub fn is_perp_market<'a>( } /// Convenience wrapper for getting max swap amounts for a token pair -pub fn max_swap_source( +/// +/// This applies net borrow and deposit limits, which is useful for true swaps. +pub fn max_swap_source_with_limits( client: &MangoClient, account_fetcher: &chain_data::AccountFetcher, account: &MangoAccountValue, @@ -66,7 +68,7 @@ pub fn max_swap_source( let source_price = health_cache.token_info(source).unwrap().prices.oracle; let amount = health_cache - .max_swap_source_for_health_ratio( + .max_swap_source_for_health_ratio_with_limits( &account, &source_bank, source_price, @@ -79,7 +81,10 @@ pub fn max_swap_source( } /// Convenience wrapper for getting max swap amounts for a token pair -pub fn max_swap_source_ignore_net_borrows( +/// +/// This is useful for liquidations, which don't increase deposits or net borrows. +/// Tcs execution can also increase deposits/net borrows. +pub fn max_swap_source_ignoring_limits( client: &MangoClient, account_fetcher: &chain_data::AccountFetcher, account: &MangoAccountValue, @@ -99,17 +104,15 @@ pub fn max_swap_source_ignore_net_borrows( mango_v4_client::health_cache::new_sync(&client.context, account_fetcher, &account) .expect("always ok"); - let mut source_bank: Bank = + let source_bank: Bank = account_fetcher.fetch(&client.context.mint_info(source).first_bank())?; - source_bank.net_borrow_limit_per_window_quote = -1; - let mut target_bank: Bank = + let target_bank: Bank = account_fetcher.fetch(&client.context.mint_info(target).first_bank())?; - target_bank.net_borrow_limit_per_window_quote = -1; let source_price = health_cache.token_info(source).unwrap().prices.oracle; let amount = health_cache - .max_swap_source_for_health_ratio( + .max_swap_source_for_health_ratio_ignoring_limits( &account, &source_bank, source_price, diff --git a/lib/client/src/health_cache.rs b/lib/client/src/health_cache.rs index a54055176..160ce0e8b 100644 --- a/lib/client/src/health_cache.rs +++ b/lib/client/src/health_cache.rs @@ -5,6 +5,8 @@ use mango_v4::accounts_zerocopy::KeyedAccountSharedData; use mango_v4::health::{FixedOrderAccountRetriever, HealthCache}; use mango_v4::state::MangoAccountValue; +use std::time::{SystemTime, UNIX_EPOCH}; + pub async fn new( context: &MangoGroupContext, account_fetcher: &impl AccountFetcher, @@ -33,7 +35,9 @@ pub async fn new( begin_serum3: active_token_len * 2 + active_perp_len * 2, staleness_slot: None, }; - mango_v4::health::new_health_cache(&account.borrow(), &retriever).context("make health cache") + let now_ts = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); + mango_v4::health::new_health_cache(&account.borrow(), &retriever, now_ts) + .context("make health cache") } pub fn new_sync( @@ -64,5 +68,7 @@ pub fn new_sync( begin_serum3: active_token_len * 2 + active_perp_len * 2, staleness_slot: None, }; - mango_v4::health::new_health_cache(&account.borrow(), &retriever).context("make health cache") + let now_ts = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); + mango_v4::health::new_health_cache(&account.borrow(), &retriever, now_ts) + .context("make health cache") } diff --git a/mango_v4.json b/mango_v4.json index 385e5eeab..6b716ad3f 100644 --- a/mango_v4.json +++ b/mango_v4.json @@ -1,5 +1,5 @@ { - "version": "0.20.0", + "version": "0.21.0", "name": "mango_v4", "instructions": [ { @@ -602,6 +602,22 @@ { "name": "flashLoanSwapFeeRate", "type": "f32" + }, + { + "name": "interestCurveScaling", + "type": "f32" + }, + { + "name": "interestTargetUtilization", + "type": "f32" + }, + { + "name": "groupInsuranceFund", + "type": "bool" + }, + { + "name": "depositLimit", + "type": "u64" } ] }, @@ -936,6 +952,56 @@ "type": { "option": "f32" } + }, + { + "name": "interestCurveScalingOpt", + "type": { + "option": "f32" + } + }, + { + "name": "interestTargetUtilizationOpt", + "type": { + "option": "f32" + } + }, + { + "name": "maintWeightShiftStartOpt", + "type": { + "option": "u64" + } + }, + { + "name": "maintWeightShiftEndOpt", + "type": { + "option": "u64" + } + }, + { + "name": "maintWeightShiftAssetTargetOpt", + "type": { + "option": "f32" + } + }, + { + "name": "maintWeightShiftLiabTargetOpt", + "type": { + "option": "f32" + } + }, + { + "name": "maintWeightShiftAbort", + "type": "bool" + }, + { + "name": "setFallbackOracle", + "type": "bool" + }, + { + "name": "depositLimitOpt", + "type": { + "option": "u64" + } } ] }, @@ -1817,8 +1883,7 @@ "isMut": true, "isSigner": false, "relations": [ - "group", - "owner" + "group" ] }, { @@ -2306,6 +2371,10 @@ { "name": "name", "type": "string" + }, + { + "name": "oraclePriceBand", + "type": "f32" } ] }, @@ -2349,6 +2418,12 @@ "type": { "option": "string" } + }, + { + "name": "oraclePriceBandOpt", + "type": { + "option": "f32" + } } ] }, @@ -2689,6 +2764,164 @@ } ] }, + { + "name": "serum3PlaceOrderV2", + "docs": [ + "requires the receiver_bank in the health account list to be writable" + ], + "accounts": [ + { + "name": "group", + "isMut": false, + "isSigner": false + }, + { + "name": "account", + "isMut": true, + "isSigner": false, + "relations": [ + "group" + ] + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "openOrders", + "isMut": true, + "isSigner": false + }, + { + "name": "serumMarket", + "isMut": false, + "isSigner": false, + "relations": [ + "group", + "serum_program", + "serum_market_external" + ] + }, + { + "name": "serumProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "serumMarketExternal", + "isMut": true, + "isSigner": false + }, + { + "name": "marketBids", + "isMut": true, + "isSigner": false + }, + { + "name": "marketAsks", + "isMut": true, + "isSigner": false + }, + { + "name": "marketEventQueue", + "isMut": true, + "isSigner": false + }, + { + "name": "marketRequestQueue", + "isMut": true, + "isSigner": false + }, + { + "name": "marketBaseVault", + "isMut": true, + "isSigner": false + }, + { + "name": "marketQuoteVault", + "isMut": true, + "isSigner": false + }, + { + "name": "marketVaultSigner", + "isMut": false, + "isSigner": false, + "docs": [ + "needed for the automatic settle_funds call" + ] + }, + { + "name": "payerBank", + "isMut": true, + "isSigner": false, + "docs": [ + "The bank that pays for the order, if necessary" + ], + "relations": [ + "group" + ] + }, + { + "name": "payerVault", + "isMut": true, + "isSigner": false, + "docs": [ + "The bank vault that pays for the order, if necessary" + ] + }, + { + "name": "payerOracle", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "side", + "type": { + "defined": "Serum3Side" + } + }, + { + "name": "limitPrice", + "type": "u64" + }, + { + "name": "maxBaseQty", + "type": "u64" + }, + { + "name": "maxNativeQuoteQtyIncludingFees", + "type": "u64" + }, + { + "name": "selfTradeBehavior", + "type": { + "defined": "Serum3SelfTradeBehavior" + } + }, + { + "name": "orderType", + "type": { + "defined": "Serum3OrderType" + } + }, + { + "name": "clientOrderId", + "type": "u64" + }, + { + "name": "limit", + "type": "u16" + } + ] + }, { "name": "serum3CancelOrder", "accounts": [ @@ -7057,15 +7290,56 @@ "type": "f64" }, { - "name": "depositsInSerum", - "type": "i64" + "name": "potentialSerumTokens", + "docs": [ + "Largest amount of tokens that might be added the the bank based on", + "serum open order execution." + ], + "type": "u64" + }, + { + "name": "maintWeightShiftStart", + "type": "u64" + }, + { + "name": "maintWeightShiftEnd", + "type": "u64" + }, + { + "name": "maintWeightShiftDurationInv", + "type": { + "defined": "I80F48" + } + }, + { + "name": "maintWeightShiftAssetTarget", + "type": { + "defined": "I80F48" + } + }, + { + "name": "maintWeightShiftLiabTarget", + "type": { + "defined": "I80F48" + } + }, + { + "name": "fallbackOracle", + "type": "publicKey" + }, + { + "name": "depositLimit", + "docs": [ + "zero means none, in token native" + ], + "type": "u64" }, { "name": "reserved", "type": { "array": [ "u8", - 2072 + 1968 ] } } @@ -7990,7 +8264,7 @@ "name": "settleFeeFlat", "docs": [ "In native units of settlement token, given to each settle call above the", - "settle_fee_amount_threshold." + "settle_fee_amount_threshold if settling at least 1% of perp base pos value." ], "type": "f32" }, @@ -8004,7 +8278,8 @@ { "name": "settleFeeFractionLowHealth", "docs": [ - "Fraction of pnl to pay out as fee if +pnl account has low health." + "Fraction of pnl to pay out as fee if +pnl account has low health.", + "(limited to 2x settle_fee_flat)" ], "type": "f32" }, @@ -8163,10 +8438,20 @@ "type": { "array": [ "u8", - 5 + 1 ] } }, + { + "name": "oraclePriceBand", + "docs": [ + "Limit orders must be <= oracle * (1+band) and >= oracle / (1+band)", + "", + "Zero value is the default due to migration and disables the limit,", + "same as f32::MAX." + ], + "type": "f32" + }, { "name": "registrationTime", "type": "u64" @@ -8602,26 +8887,41 @@ "type": "f64" }, { - "name": "baseDepositsReserved", + "name": "potentialBaseTokens", "docs": [ - "Tracks the amount of deposits that flowed into the serum open orders account.", + "An overestimate of the amount of tokens that might flow out of the open orders account.", "", - "The bank still considers these amounts user deposits (see deposits_in_serum)", - "and they need to be deducted from there when they flow back into the bank", - "as real tokens." + "The bank still considers these amounts user deposits (see Bank::potential_serum_tokens)", + "and that value needs to be updated in conjunction with these numbers.", + "", + "This estimation is based on the amount of tokens in the open orders account", + "(see update_bank_potential_tokens() in serum3_place_order and settle)" ], "type": "u64" }, { - "name": "quoteDepositsReserved", + "name": "potentialQuoteTokens", "type": "u64" }, + { + "name": "lowestPlacedBidInv", + "docs": [ + "Track lowest bid/highest ask, same way as for highest bid/lowest ask.", + "", + "0 is a special \"unset\" state." + ], + "type": "f64" + }, + { + "name": "highestPlacedAsk", + "type": "f64" + }, { "name": "reserved", "type": { "array": [ "u8", - 32 + 16 ] } } @@ -10284,6 +10584,9 @@ }, { "name": "TokenConditionalSwapCreateLinearAuction" + }, + { + "name": "Serum3PlaceOrderV2" } ] } @@ -11393,6 +11696,56 @@ } ] }, + { + "name": "UpdateRateLogV2", + "fields": [ + { + "name": "mangoGroup", + "type": "publicKey", + "index": false + }, + { + "name": "tokenIndex", + "type": "u16", + "index": false + }, + { + "name": "rate0", + "type": "i128", + "index": false + }, + { + "name": "util0", + "type": "i128", + "index": false + }, + { + "name": "rate1", + "type": "i128", + "index": false + }, + { + "name": "util1", + "type": "i128", + "index": false + }, + { + "name": "maxRate", + "type": "i128", + "index": false + }, + { + "name": "curveScaling", + "type": "f64", + "index": false + }, + { + "name": "targetUtilization", + "type": "f32", + "index": false + } + ] + }, { "name": "TokenLiqWithTokenLog", "fields": [ @@ -13155,6 +13508,21 @@ "code": 6059, "name": "TokenConditionalSwapTypeNotStartable", "msg": "token conditional swap type cannot be started" + }, + { + "code": 6060, + "name": "HealthAccountBankNotWritable", + "msg": "a bank in the health account list should be writable but is not" + }, + { + "code": 6061, + "name": "Serum3PriceBandExceeded", + "msg": "the market does not allow limit orders too far from the current oracle value" + }, + { + "code": 6062, + "name": "BankDepositLimit", + "msg": "deposit crosses the token's deposit limit" } ] } \ No newline at end of file diff --git a/programs/mango-v4/Cargo.toml b/programs/mango-v4/Cargo.toml index 2e8056146..543f262cd 100644 --- a/programs/mango-v4/Cargo.toml +++ b/programs/mango-v4/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mango-v4" -version = "0.20.0" +version = "0.21.0" description = "Created with Anchor" edition = "2021" diff --git a/programs/mango-v4/src/accounts_ix/serum3_place_order.rs b/programs/mango-v4/src/accounts_ix/serum3_place_order.rs index 975d937d4..eb7b62335 100644 --- a/programs/mango-v4/src/accounts_ix/serum3_place_order.rs +++ b/programs/mango-v4/src/accounts_ix/serum3_place_order.rs @@ -31,11 +31,10 @@ pub enum Serum3Side { Ask = 1, } +// Used for Serum3PlaceOrder v1 and v2 #[derive(Accounts)] pub struct Serum3PlaceOrder<'info> { - #[account( - constraint = group.load()?.is_ix_enabled(IxGate::Serum3PlaceOrder) @ MangoError::IxIsDisabled, - )] + // ix gate check is done at #4 pub group: AccountLoader<'info, Group>, #[account( diff --git a/programs/mango-v4/src/accounts_ix/token_add_bank.rs b/programs/mango-v4/src/accounts_ix/token_add_bank.rs index bf1bcb682..6ef6ff33f 100644 --- a/programs/mango-v4/src/accounts_ix/token_add_bank.rs +++ b/programs/mango-v4/src/accounts_ix/token_add_bank.rs @@ -10,7 +10,12 @@ pub struct TokenAddBank<'info> { #[account( has_one = admin, constraint = group.load()?.is_ix_enabled(IxGate::TokenAddBank) @ MangoError::IxIsDisabled, - constraint = group.load()?.multiple_banks_supported() + constraint = group.load()?.multiple_banks_supported(), + // Concerns are: + // - general reaudit + // - client support + // - potential_serum_tokens + constraint = group.load()?.is_testing(), )] pub group: AccountLoader<'info, Group>, pub admin: Signer<'info>, diff --git a/programs/mango-v4/src/error.rs b/programs/mango-v4/src/error.rs index 92ee2c907..00911084e 100644 --- a/programs/mango-v4/src/error.rs +++ b/programs/mango-v4/src/error.rs @@ -125,6 +125,12 @@ pub enum MangoError { TokenConditionalSwapTooSmallForStartIncentive, #[msg("token conditional swap type cannot be started")] TokenConditionalSwapTypeNotStartable, + #[msg("a bank in the health account list should be writable but is not")] + HealthAccountBankNotWritable, + #[msg("the market does not allow limit orders too far from the current oracle value")] + Serum3PriceBandExceeded, + #[msg("deposit crosses the token's deposit limit")] + BankDepositLimit, } impl MangoError { diff --git a/programs/mango-v4/src/health/cache.rs b/programs/mango-v4/src/health/cache.rs index 9f132cdba..677fb13c9 100644 --- a/programs/mango-v4/src/health/cache.rs +++ b/programs/mango-v4/src/health/cache.rs @@ -94,9 +94,10 @@ pub fn compute_health_from_fixed_accounts( account: &MangoAccountRef, health_type: HealthType, ais: &[AccountInfo], + now_ts: u64, ) -> Result { let retriever = new_fixed_order_account_retriever(ais, account)?; - Ok(new_health_cache(account, &retriever)?.health(health_type)) + Ok(new_health_cache(account, &retriever, now_ts)?.health(health_type)) } /// Compute health with an arbitrary AccountRetriever @@ -104,8 +105,9 @@ pub fn compute_health( account: &MangoAccountRef, health_type: HealthType, retriever: &impl AccountRetriever, + now_ts: u64, ) -> Result { - Ok(new_health_cache(account, retriever)?.health(health_type)) + Ok(new_health_cache(account, retriever, now_ts)?.health(health_type)) } /// How much of a token can be taken away before health decreases to zero? @@ -1221,8 +1223,9 @@ pub(crate) fn find_token_info_index(infos: &[TokenInfo], token_index: TokenIndex pub fn new_health_cache( account: &MangoAccountRef, retriever: &impl AccountRetriever, + now_ts: u64, ) -> Result { - new_health_cache_impl(account, retriever, false) + new_health_cache_impl(account, retriever, now_ts, false) } /// Generate a special HealthCache for an account and its health accounts @@ -1233,20 +1236,22 @@ pub fn new_health_cache( pub fn new_health_cache_skipping_bad_oracles( account: &MangoAccountRef, retriever: &impl AccountRetriever, + now_ts: u64, ) -> Result { - new_health_cache_impl(account, retriever, true) + new_health_cache_impl(account, retriever, now_ts, true) } fn new_health_cache_impl( account: &MangoAccountRef, retriever: &impl AccountRetriever, + now_ts: u64, // If an oracle is stale or inconfident and the health contribution would // not be negative, skip it. This decreases health, but maybe overall it's // still positive? skip_bad_oracles: bool, ) -> Result { // token contribution from token accounts - let mut token_infos = vec![]; + let mut token_infos = Vec::with_capacity(account.active_token_positions().count()); for (i, position) in account.active_token_positions().enumerate() { let bank_oracle_result = @@ -1268,12 +1273,15 @@ fn new_health_cache_impl( // Use the liab price for computing weight scaling, because it's pessimistic and // causes the most unfavorable scaling. let liab_price = prices.liab(HealthType::Init); + + let (maint_asset_weight, maint_liab_weight) = bank.maint_weights(now_ts); + token_infos.push(TokenInfo { token_index: bank.token_index, - maint_asset_weight: bank.maint_asset_weight, + maint_asset_weight, init_asset_weight: bank.init_asset_weight, init_scaled_asset_weight: bank.scaled_init_asset_weight(liab_price), - maint_liab_weight: bank.maint_liab_weight, + maint_liab_weight, init_liab_weight: bank.init_liab_weight, init_scaled_liab_weight: bank.scaled_init_liab_weight(liab_price), prices, @@ -1282,7 +1290,7 @@ fn new_health_cache_impl( } // Fill the TokenInfo balance with free funds in serum3 oo accounts and build Serum3Infos. - let mut serum3_infos = vec![]; + let mut serum3_infos = Vec::with_capacity(account.active_serum3_orders().count()); for (i, serum_account) in account.active_serum3_orders().enumerate() { let oo = retriever.serum_oo(i, &serum_account.open_orders)?; @@ -1443,7 +1451,7 @@ mod tests { // for bank2/oracle2 let health2 = (-10.0 + 3.0) * 5.0 * 1.5; assert!(health_eq( - compute_health(&account.borrow(), HealthType::Init, &retriever).unwrap(), + compute_health(&account.borrow(), HealthType::Init, &retriever, 0).unwrap(), health1 + health2 )); } @@ -1454,7 +1462,7 @@ mod tests { borrows: u64, deposit_weight_scale_start_quote: u64, borrow_weight_scale_start_quote: u64, - deposits_in_serum: i64, + potential_serum_tokens: u64, } #[derive(Default)] @@ -1510,7 +1518,7 @@ mod tests { let bank = bank.data(); bank.indexed_deposits = I80F48::from(settings.deposits) / bank.deposit_index; bank.indexed_borrows = I80F48::from(settings.borrows) / bank.borrow_index; - bank.deposits_in_serum = settings.deposits_in_serum; + bank.potential_serum_tokens = settings.potential_serum_tokens; if settings.deposit_weight_scale_start_quote > 0 { bank.deposit_weight_scale_start_quote = settings.deposit_weight_scale_start_quote as f64; @@ -1568,7 +1576,7 @@ mod tests { let retriever = ScanningAccountRetriever::new_with_staleness(&ais, &group, None).unwrap(); assert!(health_eq( - compute_health(&account.borrow(), HealthType::Init, &retriever).unwrap(), + compute_health(&account.borrow(), HealthType::Init, &retriever, 0).unwrap(), testcase.expected_health )); } @@ -1826,7 +1834,7 @@ mod tests { ..Default::default() }, TestHealth1Case { - // 17, deposits_in_serum counts for deposit weight scaling + // 17, potential_serum_tokens counts for deposit weight scaling token1: 100, token2: 100, token3: 100, @@ -1839,13 +1847,13 @@ mod tests { BankSettings { deposits: 100, deposit_weight_scale_start_quote: 100 * 5, - deposits_in_serum: 100, + potential_serum_tokens: 100, ..BankSettings::default() }, BankSettings { deposits: 600, deposit_weight_scale_start_quote: 500 * 10, - deposits_in_serum: 100, + potential_serum_tokens: 100, ..BankSettings::default() }, ], diff --git a/programs/mango-v4/src/health/client.rs b/programs/mango-v4/src/health/client.rs index 867c3e5af..73b003280 100644 --- a/programs/mango-v4/src/health/client.rs +++ b/programs/mango-v4/src/health/client.rs @@ -42,7 +42,6 @@ impl HealthCache { let mut source_bank = source_bank.clone(); source_bank.withdraw_with_fee(&mut source_position, amount, now_ts)?; - source_bank.check_net_borrows(source_oracle_price)?; let mut target_bank = target_bank.clone(); target_bank.deposit(&mut target_position, target_amount, now_ts)?; @@ -52,7 +51,40 @@ impl HealthCache { Ok(resulting_cache) } - pub fn max_swap_source_for_health_ratio( + fn apply_limits_to_swap( + account: &MangoAccountValue, + source_bank: &Bank, + source_oracle_price: I80F48, + target_bank: &Bank, + price: I80F48, + source_unlimited: I80F48, + ) -> Result { + let source_pos = account + .token_position(source_bank.token_index)? + .native(source_bank); + let target_pos = account + .token_position(target_bank.token_index)? + .native(target_bank); + + // net borrow limit on source + let available_net_borrows = source_bank + .remaining_net_borrows_quote(source_oracle_price) + .saturating_div(source_oracle_price); + let potential_source = source_unlimited + .min(available_net_borrows.saturating_add(source_pos.max(I80F48::ZERO))); + + // deposit limit on target + let available_deposits = target_bank.remaining_deposits_until_limit(); + let potential_target_unlimited = potential_source.saturating_mul(price); + let potential_target = potential_target_unlimited + .min(available_deposits.saturating_add(-target_pos.min(I80F48::ZERO))); + + let source = potential_source.min(potential_target.saturating_div(price)); + Ok(source) + } + + /// Verifies neither the net borrow or deposit limits + pub fn max_swap_source_for_health_ratio_ignoring_limits( &self, account: &MangoAccountValue, source_bank: &Bank, @@ -72,7 +104,7 @@ impl HealthCache { ) } - pub fn max_swap_source_for_health( + pub fn max_swap_source_for_health_ratio_with_limits( &self, account: &MangoAccountValue, source_bank: &Bank, @@ -81,14 +113,23 @@ impl HealthCache { price: I80F48, min_ratio: I80F48, ) -> Result { - self.max_swap_source_for_health_fn( + let source_unlimited = self.max_swap_source_for_health_fn( account, source_bank, source_oracle_price, target_bank, price, min_ratio, - |cache| cache.health(HealthType::Init), + |cache| cache.health_ratio(HealthType::Init), + )?; + + Self::apply_limits_to_swap( + account, + source_bank, + source_oracle_price, + target_bank, + price, + source_unlimited, ) } @@ -707,7 +748,7 @@ mod tests { assert_eq!(health_cache.health_ratio(HealthType::Init), I80F48::MAX); assert_eq!( health_cache - .max_swap_source_for_health_ratio( + .max_swap_source_for_health_ratio_with_limits( &account, &banks[0], I80F48::from(1), @@ -748,7 +789,7 @@ mod tests { let swap_price = I80F48::from_num(price_factor) * source_price.oracle / target_price.oracle; - let source_amount = c + let source_unlimited = c .max_swap_source_for_health_fn( &account, &source_bank, @@ -759,6 +800,15 @@ mod tests { max_swap_fn, ) .unwrap(); + let source_amount = HealthCache::apply_limits_to_swap( + &account, + &source_bank, + source_price.oracle, + &target_bank, + swap_price, + source_unlimited, + ) + .unwrap(); if source_amount == I80F48::MAX { return (f64::MAX, f64::MAX, f64::MAX, f64::MAX); } @@ -865,10 +915,7 @@ mod tests { } // At this unlikely price it's healthy to swap infinitely - assert_eq!( - find_max_swap(&health_cache, 0, 1, 50.0, 1.5, banks).0, - f64::MAX - ); + assert!(find_max_swap(&health_cache, 0, 1, 50.0, 1.5, banks).0 > 1e16); } { @@ -1264,7 +1311,7 @@ mod tests { let retriever = ScanningAccountRetriever::new_with_staleness(&ais, &group, None).unwrap(); assert!(health_eq( - compute_health(&account.borrow(), HealthType::Init, &retriever).unwrap(), + compute_health(&account.borrow(), HealthType::Init, &retriever, 0).unwrap(), // token 0.8 * (100.0 // perp base @@ -1353,27 +1400,27 @@ mod tests { let retriever = ScanningAccountRetriever::new_with_staleness(&ais, &group, None).unwrap(); assert!(health_eq( - compute_health(&account.borrow(), HealthType::Init, &retriever).unwrap(), + compute_health(&account.borrow(), HealthType::Init, &retriever, 0).unwrap(), 0.8 * 0.5 * 100.0 )); assert!(health_eq( - compute_health(&account.borrow(), HealthType::Maint, &retriever).unwrap(), + compute_health(&account.borrow(), HealthType::Maint, &retriever, 0).unwrap(), 0.9 * 1.0 * 100.0 )); assert!(health_eq( - compute_health(&account2.borrow(), HealthType::Init, &retriever).unwrap(), + compute_health(&account2.borrow(), HealthType::Init, &retriever, 0).unwrap(), -1.2 * 1.0 * 100.0 )); assert!(health_eq( - compute_health(&account2.borrow(), HealthType::Maint, &retriever).unwrap(), + compute_health(&account2.borrow(), HealthType::Maint, &retriever, 0).unwrap(), -1.1 * 1.0 * 100.0 )); assert!(health_eq( - compute_health(&account3.borrow(), HealthType::Init, &retriever).unwrap(), + compute_health(&account3.borrow(), HealthType::Init, &retriever, 0).unwrap(), 1.2 * (0.8 * 0.5 * 10.0 * 10.0 - 100.0) )); assert!(health_eq( - compute_health(&account3.borrow(), HealthType::Maint, &retriever).unwrap(), + compute_health(&account3.borrow(), HealthType::Maint, &retriever, 0).unwrap(), 1.1 * (0.9 * 1.0 * 10.0 * 10.0 - 100.0) )); } diff --git a/programs/mango-v4/src/instructions/account_buyback_fees_with_mngo.rs b/programs/mango-v4/src/instructions/account_buyback_fees_with_mngo.rs index b6ea14ab2..099ce455b 100644 --- a/programs/mango-v4/src/instructions/account_buyback_fees_with_mngo.rs +++ b/programs/mango-v4/src/instructions/account_buyback_fees_with_mngo.rs @@ -7,7 +7,7 @@ use crate::state::*; use crate::accounts_ix::*; -use crate::logs::{AccountBuybackFeesWithMngoLog, TokenBalanceLog}; +use crate::logs::{emit_stack, AccountBuybackFeesWithMngoLog, TokenBalanceLog}; pub fn account_buyback_fees_with_mngo( ctx: Context, @@ -105,7 +105,7 @@ pub fn account_buyback_fees_with_mngo( ); let in_use = mngo_bank.withdraw_without_fee(account_mngo_token_position, max_buyback_mngo, now_ts)?; - emit!(TokenBalanceLog { + emit_stack(TokenBalanceLog { mango_group: ctx.accounts.group.key(), mango_account: ctx.accounts.account.key(), token_index: mngo_bank.token_index, @@ -137,7 +137,7 @@ pub fn account_buyback_fees_with_mngo( ); } let in_use = fees_bank.deposit(account_fees_token_position, max_buyback_fees, now_ts)?; - emit!(TokenBalanceLog { + emit_stack(TokenBalanceLog { mango_group: ctx.accounts.group.key(), mango_account: ctx.accounts.account.key(), token_index: fees_bank.token_index, @@ -162,7 +162,7 @@ pub fn account_buyback_fees_with_mngo( max_buyback_fees, ); - emit!(AccountBuybackFeesWithMngoLog { + emit_stack(AccountBuybackFeesWithMngoLog { mango_group: ctx.accounts.group.key(), mango_account: ctx.accounts.account.key(), buyback_fees: max_buyback_fees.to_bits(), diff --git a/programs/mango-v4/src/instructions/compute_account_data.rs b/programs/mango-v4/src/instructions/compute_account_data.rs index 4d4583687..1db6e854b 100644 --- a/programs/mango-v4/src/instructions/compute_account_data.rs +++ b/programs/mango-v4/src/instructions/compute_account_data.rs @@ -13,12 +13,14 @@ pub fn compute_account_data(ctx: Context) -> Result<()> { let account_retriever = ScanningAccountRetriever::new(ctx.remaining_accounts, &group_pk)?; - let health_cache = new_health_cache(&account.borrow(), &account_retriever)?; + let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap(); + let health_cache = new_health_cache(&account.borrow(), &account_retriever, now_ts)?; let init_health = health_cache.health(HealthType::Init); let maint_health = health_cache.health(HealthType::Maint); let equity = compute_equity(&account.borrow(), &account_retriever)?; + // Potentially too big for the stack! emit!(MangoAccountData { init_health, maint_health, diff --git a/programs/mango-v4/src/instructions/flash_loan.rs b/programs/mango-v4/src/instructions/flash_loan.rs index 675cf4f2f..6f7403a58 100644 --- a/programs/mango-v4/src/instructions/flash_loan.rs +++ b/programs/mango-v4/src/instructions/flash_loan.rs @@ -3,7 +3,7 @@ use crate::accounts_zerocopy::*; use crate::error::*; use crate::group_seeds; use crate::health::{new_fixed_order_account_retriever, new_health_cache, AccountRetriever}; -use crate::logs::{FlashLoanLogV3, FlashLoanTokenDetailV3, TokenBalanceLog}; +use crate::logs::{emit_stack, FlashLoanLogV3, FlashLoanTokenDetailV3, TokenBalanceLog}; use crate::state::*; use anchor_lang::prelude::*; @@ -388,7 +388,8 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>( // Check health before balance adjustments let retriever = new_fixed_order_account_retriever(health_ais, &account.borrow())?; - let health_cache = new_health_cache(&account.borrow(), &retriever)?; + let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap(); + let health_cache = new_health_cache(&account.borrow(), &retriever, now_ts)?; let pre_init_health = account.check_health_pre(&health_cache)?; // Prices for logging and net borrow checks @@ -466,6 +467,10 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>( bank.check_net_borrows(*oracle_price)?; } + if change_amount > 0 && native_after_change > 0 { + bank.check_deposit_and_oo_limit()?; + } + bank.flash_loan_approved_amount = 0; bank.flash_loan_token_account_initial = u64::MAX; @@ -481,7 +486,7 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>( approved_amount: approved_amount_u64, }); - emit!(TokenBalanceLog { + emit_stack(TokenBalanceLog { mango_group: group.key(), mango_account: ctx.accounts.account.key(), token_index: bank.token_index as u16, @@ -491,16 +496,16 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>( }); } - emit!(FlashLoanLogV3 { + emit_stack(FlashLoanLogV3 { mango_group: group.key(), mango_account: ctx.accounts.account.key(), flash_loan_type, - token_loan_details + token_loan_details, }); // Check health after account position changes let retriever = new_fixed_order_account_retriever(health_ais, &account.borrow())?; - let health_cache = new_health_cache(&account.borrow(), &retriever)?; + let health_cache = new_health_cache(&account.borrow(), &retriever, now_ts)?; account.check_health_post(&health_cache, pre_init_health)?; // Deactivate inactive token accounts after health check diff --git a/programs/mango-v4/src/instructions/health_region.rs b/programs/mango-v4/src/instructions/health_region.rs index b6b25ee00..7b25cdac5 100644 --- a/programs/mango-v4/src/instructions/health_region.rs +++ b/programs/mango-v4/src/instructions/health_region.rs @@ -23,6 +23,7 @@ pub fn health_region_begin<'key, 'accounts, 'remaining, 'info>( crate::instruction::Serum3CancelAllOrders::discriminator(), crate::instruction::Serum3CancelOrder::discriminator(), crate::instruction::Serum3PlaceOrder::discriminator(), + crate::instruction::Serum3PlaceOrderV2::discriminator(), crate::instruction::Serum3SettleFunds::discriminator(), crate::instruction::Serum3SettleFundsV2::discriminator(), ]; @@ -87,7 +88,8 @@ pub fn health_region_begin<'key, 'accounts, 'remaining, 'info>( .context("create account retriever")?; // Compute pre-health and store it on the account - let health_cache = new_health_cache(&account.borrow(), &account_retriever)?; + let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap(); + let health_cache = new_health_cache(&account.borrow(), &account_retriever, now_ts)?; let pre_init_health = account.check_health_pre(&health_cache)?; account.fixed.health_region_begin_init_health = pre_init_health.ceil().to_num(); @@ -107,7 +109,8 @@ pub fn health_region_end<'key, 'accounts, 'remaining, 'info>( let group = account.fixed.group; let account_retriever = ScanningAccountRetriever::new(ctx.remaining_accounts, &group) .context("create account retriever")?; - let health_cache = new_health_cache(&account.borrow(), &account_retriever)?; + let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap(); + let health_cache = new_health_cache(&account.borrow(), &account_retriever, now_ts)?; let pre_init_health = I80F48::from(account.fixed.health_region_begin_init_health); account.check_health_post(&health_cache, pre_init_health)?; diff --git a/programs/mango-v4/src/instructions/ix_gate_set.rs b/programs/mango-v4/src/instructions/ix_gate_set.rs index 4a8c0b325..22c8255de 100644 --- a/programs/mango-v4/src/instructions/ix_gate_set.rs +++ b/programs/mango-v4/src/instructions/ix_gate_set.rs @@ -94,6 +94,7 @@ pub fn ix_gate_set(ctx: Context, ix_gate: u128) -> Result<()> { ix_gate, IxGate::TokenConditionalSwapCreateLinearAuction, ); + log_if_changed(&group, ix_gate, IxGate::Serum3PlaceOrderV2); group.ix_gate = ix_gate; diff --git a/programs/mango-v4/src/instructions/perp_consume_events.rs b/programs/mango-v4/src/instructions/perp_consume_events.rs index 80eb1082e..b232749c6 100644 --- a/programs/mango-v4/src/instructions/perp_consume_events.rs +++ b/programs/mango-v4/src/instructions/perp_consume_events.rs @@ -5,7 +5,7 @@ use crate::error::MangoError; use crate::state::*; use crate::accounts_ix::*; -use crate::logs::{emit_perp_balances, FillLogV3}; +use crate::logs::{emit_perp_balances, emit_stack, FillLogV3}; /// Load a mango account by key from the list of account infos. /// @@ -131,7 +131,7 @@ pub fn perp_consume_events(ctx: Context, limit: usize) -> Res let taker_closed_pnl = taker_after_pnl - taker_before_pnl; (maker_closed_pnl, taker_closed_pnl) }; - emit!(FillLogV3 { + emit_stack(FillLogV3 { mango_group: group_key, market_index: perp_market_index, taker_side: fill.taker_side as u8, @@ -149,7 +149,7 @@ pub fn perp_consume_events(ctx: Context, limit: usize) -> Res price: fill.price, quantity: fill.quantity, maker_closed_pnl: maker_closed_pnl.to_num(), - taker_closed_pnl: taker_closed_pnl.to_num() + taker_closed_pnl: taker_closed_pnl.to_num(), }); } EventType::Out => { diff --git a/programs/mango-v4/src/instructions/perp_create_market.rs b/programs/mango-v4/src/instructions/perp_create_market.rs index cf1133b01..c27a48d72 100644 --- a/programs/mango-v4/src/instructions/perp_create_market.rs +++ b/programs/mango-v4/src/instructions/perp_create_market.rs @@ -7,7 +7,7 @@ use crate::state::*; use crate::util::fill_from_str; use crate::accounts_ix::*; -use crate::logs::PerpMarketMetaDataLog; +use crate::logs::{emit_stack, PerpMarketMetaDataLog}; #[allow(clippy::too_many_arguments)] pub fn perp_create_market( @@ -111,7 +111,7 @@ pub fn perp_create_market( }; orderbook.init(); - emit!(PerpMarketMetaDataLog { + emit_stack(PerpMarketMetaDataLog { mango_group: ctx.accounts.group.key(), perp_market: ctx.accounts.perp_market.key(), perp_market_index, diff --git a/programs/mango-v4/src/instructions/perp_edit_market.rs b/programs/mango-v4/src/instructions/perp_edit_market.rs index 09e3a62c7..42564b4f9 100644 --- a/programs/mango-v4/src/instructions/perp_edit_market.rs +++ b/programs/mango-v4/src/instructions/perp_edit_market.rs @@ -4,7 +4,7 @@ use anchor_lang::prelude::*; use fixed::types::I80F48; use crate::accounts_ix::*; -use crate::logs::PerpMarketMetaDataLog; +use crate::logs::{emit_stack, PerpMarketMetaDataLog}; #[allow(clippy::too_many_arguments)] pub fn perp_edit_market( @@ -358,7 +358,7 @@ pub fn perp_edit_market( ); } - emit!(PerpMarketMetaDataLog { + emit_stack(PerpMarketMetaDataLog { mango_group: ctx.accounts.group.key(), perp_market: ctx.accounts.perp_market.key(), perp_market_index: perp_market.perp_market_index, diff --git a/programs/mango-v4/src/instructions/perp_force_close_position.rs b/programs/mango-v4/src/instructions/perp_force_close_position.rs index 9e3b04d4f..27df21f3b 100644 --- a/programs/mango-v4/src/instructions/perp_force_close_position.rs +++ b/programs/mango-v4/src/instructions/perp_force_close_position.rs @@ -4,7 +4,7 @@ use crate::accounts_ix::*; use crate::accounts_zerocopy::AccountInfoRef; use crate::error::MangoError; -use crate::logs::{emit_perp_balances, PerpForceClosePositionLog}; +use crate::logs::{emit_perp_balances, emit_stack, PerpForceClosePositionLog}; use crate::state::*; use fixed::types::I80F48; @@ -56,7 +56,7 @@ pub fn perp_force_close_position(ctx: Context) -> Result &perp_market, ); - emit!(PerpForceClosePositionLog { + emit_stack(PerpForceClosePositionLog { mango_group: ctx.accounts.group.key(), perp_market_index: perp_market.perp_market_index, account_a: ctx.accounts.account_a.key(), diff --git a/programs/mango-v4/src/instructions/perp_liq_base_or_positive_pnl.rs b/programs/mango-v4/src/instructions/perp_liq_base_or_positive_pnl.rs index 64084a348..68b14b18e 100644 --- a/programs/mango-v4/src/instructions/perp_liq_base_or_positive_pnl.rs +++ b/programs/mango-v4/src/instructions/perp_liq_base_or_positive_pnl.rs @@ -8,7 +8,7 @@ use crate::health::*; use crate::state::*; use crate::accounts_ix::*; -use crate::logs::{emit_perp_balances, PerpLiqBaseOrPositivePnlLog, TokenBalanceLog}; +use crate::logs::{emit_perp_balances, emit_stack, PerpLiqBaseOrPositivePnlLog, TokenBalanceLog}; /// This instruction deals with increasing health by: /// - reducing the liqee's base position @@ -29,6 +29,7 @@ pub fn perp_liq_base_or_positive_pnl( max_base_transfer = max_base_transfer.max(i64::MIN + 1); let group_pk = &ctx.accounts.group.key(); + let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap(); require_keys_neq!(ctx.accounts.liqor.key(), ctx.accounts.liqee.key()); let mut liqor = ctx.accounts.liqor.load_full_mut()?; @@ -51,7 +52,7 @@ pub fn perp_liq_base_or_positive_pnl( let mut liqee_health_cache = { let account_retriever = ScanningAccountRetriever::new(ctx.remaining_accounts, group_pk) .context("create account retriever")?; - new_health_cache(&liqee.borrow(), &account_retriever) + new_health_cache(&liqee.borrow(), &account_retriever, now_ts) .context("create liqee health cache")? }; let liqee_liq_end_health = liqee_health_cache.health(HealthType::LiquidationEnd); @@ -87,7 +88,6 @@ pub fn perp_liq_base_or_positive_pnl( // Settle funding, update limit liqee_perp_position.settle_funding(&perp_market); liqor_perp_position.settle_funding(&perp_market); - let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap(); liqee_perp_position.update_settle_limit(&perp_market, now_ts); // @@ -131,7 +131,7 @@ pub fn perp_liq_base_or_positive_pnl( let liqee_token_position = liqee.token_position(settle_token_index)?; let liqor_token_position = liqor.token_position(settle_token_index)?; - emit!(TokenBalanceLog { + emit_stack(TokenBalanceLog { mango_group: ctx.accounts.group.key(), mango_account: ctx.accounts.liqee.key(), token_index: settle_token_index, @@ -140,7 +140,7 @@ pub fn perp_liq_base_or_positive_pnl( borrow_index: settle_bank.borrow_index.to_bits(), }); - emit!(TokenBalanceLog { + emit_stack(TokenBalanceLog { mango_group: ctx.accounts.group.key(), mango_account: ctx.accounts.liqor.key(), token_index: settle_token_index, @@ -151,7 +151,7 @@ pub fn perp_liq_base_or_positive_pnl( } if base_transfer != 0 || pnl_transfer != 0 { - emit!(PerpLiqBaseOrPositivePnlLog { + emit_stack(PerpLiqBaseOrPositivePnlLog { mango_group: ctx.accounts.group.key(), perp_market_index: perp_market.perp_market_index, liqor: ctx.accounts.liqor.key(), @@ -183,8 +183,13 @@ pub fn perp_liq_base_or_positive_pnl( if !liqor.fixed.is_in_health_region() { let account_retriever = ScanningAccountRetriever::new(ctx.remaining_accounts, group_pk) .context("create account retriever end")?; - let liqor_health = compute_health(&liqor.borrow(), HealthType::Init, &account_retriever) - .context("compute liqor health")?; + let liqor_health = compute_health( + &liqor.borrow(), + HealthType::Init, + &account_retriever, + now_ts, + ) + .context("compute liqor health")?; require!(liqor_health >= 0, MangoError::HealthMustBePositive); } @@ -675,7 +680,7 @@ mod tests { let retriever = ScanningAccountRetriever::new_with_staleness(&ais, &setup.group, None).unwrap(); - health::new_health_cache(&setup.liqee.borrow(), &retriever).unwrap() + health::new_health_cache(&setup.liqee.borrow(), &retriever, 0).unwrap() } fn run(&self, max_base: i64, max_pnl: u64) -> Result { diff --git a/programs/mango-v4/src/instructions/perp_liq_force_cancel_orders.rs b/programs/mango-v4/src/instructions/perp_liq_force_cancel_orders.rs index 04b74b7dd..e9d18d955 100644 --- a/programs/mango-v4/src/instructions/perp_liq_force_cancel_orders.rs +++ b/programs/mango-v4/src/instructions/perp_liq_force_cancel_orders.rs @@ -11,10 +11,11 @@ pub fn perp_liq_force_cancel_orders( ) -> Result<()> { let mut account = ctx.accounts.account.load_full_mut()?; + let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap(); let mut health_cache = { let retriever = new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?; - new_health_cache(&account.borrow(), &retriever).context("create health cache")? + new_health_cache(&account.borrow(), &retriever, now_ts).context("create health cache")? }; let mut perp_market = ctx.accounts.perp_market.load_mut()?; diff --git a/programs/mango-v4/src/instructions/perp_liq_negative_pnl_or_bankruptcy.rs b/programs/mango-v4/src/instructions/perp_liq_negative_pnl_or_bankruptcy.rs index 12d7b53be..5581cca3b 100644 --- a/programs/mango-v4/src/instructions/perp_liq_negative_pnl_or_bankruptcy.rs +++ b/programs/mango-v4/src/instructions/perp_liq_negative_pnl_or_bankruptcy.rs @@ -10,7 +10,8 @@ use crate::accounts_zerocopy::AccountInfoRef; use crate::error::*; use crate::health::*; use crate::logs::{ - emit_perp_balances, PerpLiqBankruptcyLog, PerpLiqNegativePnlOrBankruptcyLog, TokenBalanceLog, + emit_perp_balances, emit_stack, PerpLiqBankruptcyLog, PerpLiqNegativePnlOrBankruptcyLog, + TokenBalanceLog, }; use crate::state::*; @@ -71,7 +72,7 @@ pub fn perp_liq_negative_pnl_or_bankruptcy( let retriever = ScanningAccountRetriever::new(ctx.remaining_accounts, &mango_group) .context("create account retriever")?; - let mut liqee_health_cache = new_health_cache(&liqee.borrow(), &retriever)?; + let mut liqee_health_cache = new_health_cache(&liqee.borrow(), &retriever, now_ts)?; drop(retriever); let liqee_liq_end_health = liqee_health_cache.health(HealthType::LiquidationEnd); @@ -136,7 +137,7 @@ pub fn perp_liq_negative_pnl_or_bankruptcy( if settlement > 0 { let settle_bank = ctx.accounts.settle_bank.load()?; let liqor_token_position = liqor.token_position(settle_token_index)?; - emit!(TokenBalanceLog { + emit_stack(TokenBalanceLog { mango_group, mango_account: ctx.accounts.liqor.key(), token_index: settle_token_index, @@ -146,7 +147,7 @@ pub fn perp_liq_negative_pnl_or_bankruptcy( }); let liqee_token_position = liqee.token_position(settle_token_index)?; - emit!(TokenBalanceLog { + emit_stack(TokenBalanceLog { mango_group, mango_account: ctx.accounts.liqee.key(), token_index: settle_token_index, @@ -159,7 +160,7 @@ pub fn perp_liq_negative_pnl_or_bankruptcy( if insurance_transfer > 0 { let insurance_bank = ctx.accounts.insurance_bank.load()?; let liqor_token_position = liqor.token_position(insurance_bank.token_index)?; - emit!(TokenBalanceLog { + emit_stack(TokenBalanceLog { mango_group, mango_account: ctx.accounts.liqor.key(), token_index: insurance_bank.token_index, @@ -197,8 +198,13 @@ pub fn perp_liq_negative_pnl_or_bankruptcy( if !liqor.fixed.is_in_health_region() { let account_retriever = ScanningAccountRetriever::new(ctx.remaining_accounts, &mango_group)?; - let liqor_health = compute_health(&liqor.borrow(), HealthType::Init, &account_retriever) - .context("compute liqor health")?; + let liqor_health = compute_health( + &liqor.borrow(), + HealthType::Init, + &account_retriever, + now_ts, + ) + .context("compute liqor health")?; require!(liqor_health >= 0, MangoError::HealthMustBePositive); } @@ -276,7 +282,7 @@ pub(crate) fn liquidation_action( settle_bank.withdraw_without_fee(liqee_token_position, settlement, now_ts)?; liqee_health_cache.adjust_token_balance(&settle_bank, -settlement)?; - emit!(PerpLiqNegativePnlOrBankruptcyLog { + emit_stack(PerpLiqNegativePnlOrBankruptcyLog { mango_group: group_key, liqee: liqee_key, liqor: liqor_key, @@ -397,7 +403,7 @@ pub(crate) fn liquidation_action( msg!("socialized loss: {}", socialized_loss); } - emit!(PerpLiqBankruptcyLog { + emit_stack(PerpLiqBankruptcyLog { mango_group: group_key, liqee: liqee_key, liqor: liqor_key, @@ -509,7 +515,7 @@ mod tests { ScanningAccountRetriever::new_with_staleness(&ais, &setup.group, None).unwrap(); liqee_health_cache = - health::new_health_cache(&setup.liqee.borrow(), &retriever).unwrap(); + health::new_health_cache(&setup.liqee.borrow(), &retriever, 0).unwrap(); liqee_liq_end_health = liqee_health_cache.health(HealthType::LiquidationEnd); } diff --git a/programs/mango-v4/src/instructions/perp_place_order.rs b/programs/mango-v4/src/instructions/perp_place_order.rs index ecb661c82..0ad00aab4 100644 --- a/programs/mango-v4/src/instructions/perp_place_order.rs +++ b/programs/mango-v4/src/instructions/perp_place_order.rs @@ -67,8 +67,8 @@ pub fn perp_place_order( let pre_health_opt = if !account.fixed.is_in_health_region() { let retriever = new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?; - let health_cache = - new_health_cache(&account.borrow(), &retriever).context("pre-withdraw init health")?; + let health_cache = new_health_cache(&account.borrow(), &retriever, now_ts) + .context("pre-withdraw init health")?; let pre_init_health = account.check_health_pre(&health_cache)?; Some((health_cache, pre_init_health)) } else { diff --git a/programs/mango-v4/src/instructions/perp_settle_fees.rs b/programs/mango-v4/src/instructions/perp_settle_fees.rs index 30a2beb06..4fd94cb39 100644 --- a/programs/mango-v4/src/instructions/perp_settle_fees.rs +++ b/programs/mango-v4/src/instructions/perp_settle_fees.rs @@ -8,7 +8,7 @@ use crate::health::{compute_health, new_fixed_order_account_retriever, HealthTyp use crate::state::*; use crate::accounts_ix::*; -use crate::logs::{emit_perp_balances, PerpSettleFeesLog, TokenBalanceLog}; +use crate::logs::{emit_perp_balances, emit_stack, PerpSettleFeesLog, TokenBalanceLog}; pub fn perp_settle_fees(ctx: Context, max_settle_amount: u64) -> Result<()> { // max_settle_amount must greater than zero @@ -100,7 +100,7 @@ pub fn perp_settle_fees(ctx: Context, max_settle_amount: u64) -> // Update the settled balance on the market itself perp_market.fees_settled += settlement; - emit!(TokenBalanceLog { + emit_stack(TokenBalanceLog { mango_group: ctx.accounts.group.key(), mango_account: ctx.accounts.account.key(), token_index: perp_market.settle_token_index, @@ -109,7 +109,7 @@ pub fn perp_settle_fees(ctx: Context, max_settle_amount: u64) -> borrow_index: settle_bank.borrow_index.to_bits(), }); - emit!(PerpSettleFeesLog { + emit_stack(PerpSettleFeesLog { mango_group: ctx.accounts.group.key(), mango_account: ctx.accounts.account.key(), perp_market_index: perp_market.perp_market_index, @@ -122,7 +122,8 @@ pub fn perp_settle_fees(ctx: Context, max_settle_amount: u64) -> // Verify that the result of settling did not violate the health of the account that lost money let retriever = new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?; - let health = compute_health(&account.borrow(), HealthType::Init, &retriever)?; + let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap(); + let health = compute_health(&account.borrow(), HealthType::Init, &retriever, now_ts)?; require!(health >= 0, MangoError::HealthMustBePositive); msg!("settled fees = {}", settlement); diff --git a/programs/mango-v4/src/instructions/perp_settle_pnl.rs b/programs/mango-v4/src/instructions/perp_settle_pnl.rs index 13693b93c..0dfc190e8 100644 --- a/programs/mango-v4/src/instructions/perp_settle_pnl.rs +++ b/programs/mango-v4/src/instructions/perp_settle_pnl.rs @@ -6,7 +6,7 @@ use crate::accounts_ix::*; use crate::accounts_zerocopy::*; use crate::error::*; use crate::health::{new_health_cache, HealthType, ScanningAccountRetriever}; -use crate::logs::{emit_perp_balances, PerpSettlePnlLog, TokenBalanceLog}; +use crate::logs::{emit_perp_balances, emit_stack, PerpSettlePnlLog, TokenBalanceLog}; use crate::state::*; pub fn perp_settle_pnl(ctx: Context) -> Result<()> { @@ -36,6 +36,8 @@ pub fn perp_settle_pnl(ctx: Context) -> Result<()> { account_b.token_position(settle_token_index)?; } + let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap(); + let a_liq_end_health; let a_maint_health; let b_max_settle; @@ -43,9 +45,9 @@ pub fn perp_settle_pnl(ctx: Context) -> Result<()> { let retriever = ScanningAccountRetriever::new(ctx.remaining_accounts, &ctx.accounts.group.key()) .context("create account retriever")?; - b_max_settle = new_health_cache(&account_b.borrow(), &retriever)? + b_max_settle = new_health_cache(&account_b.borrow(), &retriever, now_ts)? .perp_max_settle(settle_token_index)?; - let a_cache = new_health_cache(&account_a.borrow(), &retriever)?; + let a_cache = new_health_cache(&account_a.borrow(), &retriever, now_ts)?; a_liq_end_health = a_cache.health(HealthType::LiquidationEnd); a_maint_health = a_cache.health(HealthType::Maint); }; @@ -93,7 +95,6 @@ pub fn perp_settle_pnl(ctx: Context) -> Result<()> { ); // Apply pnl settle limits - let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap(); a_perp_position.update_settle_limit(&perp_market, now_ts); let a_settleable_pnl = a_perp_position.apply_pnl_settle_limit(&perp_market, a_pnl); b_perp_position.update_settle_limit(&perp_market, now_ts); @@ -188,7 +189,7 @@ pub fn perp_settle_pnl(ctx: Context) -> Result<()> { // settled back and forth repeatedly. settle_bank.withdraw_without_fee(b_token_position, settlement, now_ts)?; - emit!(TokenBalanceLog { + emit_stack(TokenBalanceLog { mango_group: ctx.accounts.group.key(), mango_account: ctx.accounts.account_a.key(), token_index: settle_token_index, @@ -197,7 +198,7 @@ pub fn perp_settle_pnl(ctx: Context) -> Result<()> { borrow_index: settle_bank.borrow_index.to_bits(), }); - emit!(TokenBalanceLog { + emit_stack(TokenBalanceLog { mango_group: ctx.accounts.group.key(), mango_account: ctx.accounts.account_b.key(), token_index: settle_token_index, @@ -223,7 +224,7 @@ pub fn perp_settle_pnl(ctx: Context) -> Result<()> { settler.ensure_token_position(settle_token_index)?; let settler_token_position_active = settle_bank.deposit(settler_token_position, fee, now_ts)?; - emit!(TokenBalanceLog { + emit_stack(TokenBalanceLog { mango_group: ctx.accounts.group.key(), mango_account: ctx.accounts.settler.key(), token_index: settler_token_position.token_index, @@ -237,7 +238,7 @@ pub fn perp_settle_pnl(ctx: Context) -> Result<()> { .deactivate_token_position_and_log(settler_token_raw_index, ctx.accounts.settler.key()); } - emit!(PerpSettlePnlLog { + emit_stack(PerpSettlePnlLog { mango_group: ctx.accounts.group.key(), mango_account_a: ctx.accounts.account_a.key(), mango_account_b: ctx.accounts.account_b.key(), diff --git a/programs/mango-v4/src/instructions/serum3_cancel_all_orders.rs b/programs/mango-v4/src/instructions/serum3_cancel_all_orders.rs index 39c304666..a1a2d4c59 100644 --- a/programs/mango-v4/src/instructions/serum3_cancel_all_orders.rs +++ b/programs/mango-v4/src/instructions/serum3_cancel_all_orders.rs @@ -2,7 +2,7 @@ use anchor_lang::prelude::*; use crate::accounts_ix::*; use crate::error::*; -use crate::logs::Serum3OpenOrdersBalanceLogV2; +use crate::logs::{emit_stack, Serum3OpenOrdersBalanceLogV2}; use crate::serum3_cpi::{load_open_orders_ref, OpenOrdersAmounts, OpenOrdersSlim}; use crate::state::*; @@ -39,7 +39,7 @@ pub fn serum3_cancel_all_orders(ctx: Context, limit: u8) let oo_ai = &ctx.accounts.open_orders.as_ref(); let open_orders = load_open_orders_ref(oo_ai)?; let after_oo = OpenOrdersSlim::from_oo(&open_orders); - emit!(Serum3OpenOrdersBalanceLogV2 { + emit_stack(Serum3OpenOrdersBalanceLogV2 { mango_group: ctx.accounts.group.key(), mango_account: ctx.accounts.account.key(), market_index: serum_market.market_index, diff --git a/programs/mango-v4/src/instructions/serum3_cancel_order.rs b/programs/mango-v4/src/instructions/serum3_cancel_order.rs index ae6ac591b..edc8926d5 100644 --- a/programs/mango-v4/src/instructions/serum3_cancel_order.rs +++ b/programs/mango-v4/src/instructions/serum3_cancel_order.rs @@ -6,7 +6,7 @@ use crate::error::*; use crate::state::*; use crate::accounts_ix::*; -use crate::logs::Serum3OpenOrdersBalanceLogV2; +use crate::logs::{emit_stack, Serum3OpenOrdersBalanceLogV2}; use crate::serum3_cpi::{load_open_orders_ref, OpenOrdersAmounts, OpenOrdersSlim}; pub fn serum3_cancel_order( @@ -49,7 +49,7 @@ pub fn serum3_cancel_order( let oo_ai = &ctx.accounts.open_orders.as_ref(); let open_orders = load_open_orders_ref(oo_ai)?; let after_oo = OpenOrdersSlim::from_oo(&open_orders); - emit!(Serum3OpenOrdersBalanceLogV2 { + emit_stack(Serum3OpenOrdersBalanceLogV2 { mango_group: ctx.accounts.group.key(), mango_account: ctx.accounts.account.key(), market_index: serum_market.market_index, diff --git a/programs/mango-v4/src/instructions/serum3_edit_market.rs b/programs/mango-v4/src/instructions/serum3_edit_market.rs index 74493ef60..a6e81f741 100644 --- a/programs/mango-v4/src/instructions/serum3_edit_market.rs +++ b/programs/mango-v4/src/instructions/serum3_edit_market.rs @@ -7,6 +7,7 @@ pub fn serum3_edit_market( reduce_only_opt: Option, force_close_opt: Option, name_opt: Option, + oracle_price_band_opt: Option, ) -> Result<()> { let mut serum3_market = ctx.accounts.market.load_mut()?; @@ -46,6 +47,16 @@ pub fn serum3_edit_market( require_group_admin = true; }; + if let Some(oracle_price_band) = oracle_price_band_opt { + msg!( + "Oracle price band: old - {:?}, new - {:?}", + serum3_market.oracle_price_band, + oracle_price_band + ); + serum3_market.oracle_price_band = oracle_price_band; + require_group_admin = true; + }; + if require_group_admin { require!( group.admin == ctx.accounts.admin.key(), diff --git a/programs/mango-v4/src/instructions/serum3_liq_force_cancel_orders.rs b/programs/mango-v4/src/instructions/serum3_liq_force_cancel_orders.rs index 229e65bde..ed395d091 100644 --- a/programs/mango-v4/src/instructions/serum3_liq_force_cancel_orders.rs +++ b/programs/mango-v4/src/instructions/serum3_liq_force_cancel_orders.rs @@ -5,7 +5,7 @@ use crate::error::*; use crate::health::*; use crate::instructions::apply_settle_changes; use crate::instructions::charge_loan_origination_fees; -use crate::logs::Serum3OpenOrdersBalanceLogV2; +use crate::logs::{emit_stack, Serum3OpenOrdersBalanceLogV2}; use crate::serum3_cpi::{load_open_orders_ref, OpenOrdersAmounts, OpenOrdersSlim}; use crate::state::*; @@ -57,8 +57,9 @@ pub fn serum3_liq_force_cancel_orders( let mut account = ctx.accounts.account.load_full_mut()?; let retriever = new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?; - let health_cache = - new_health_cache(&account.borrow(), &retriever).context("create health cache")?; + let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap(); + let health_cache = new_health_cache(&account.borrow(), &retriever, now_ts) + .context("create health cache")?; let liquidatable = account.check_liquidatable(&health_cache)?; let can_force_cancel = !account.fixed.is_operational() @@ -116,7 +117,7 @@ pub fn serum3_liq_force_cancel_orders( let open_orders = load_open_orders_ref(oo_ai)?; after_oo = OpenOrdersSlim::from_oo(&open_orders); - emit!(Serum3OpenOrdersBalanceLogV2 { + emit_stack(Serum3OpenOrdersBalanceLogV2 { mango_group: ctx.accounts.group.key(), mango_account: ctx.accounts.account.key(), market_index: serum_market.market_index, diff --git a/programs/mango-v4/src/instructions/serum3_place_order.rs b/programs/mango-v4/src/instructions/serum3_place_order.rs index fbadf0927..3c2d1cbe9 100644 --- a/programs/mango-v4/src/instructions/serum3_place_order.rs +++ b/programs/mango-v4/src/instructions/serum3_place_order.rs @@ -1,11 +1,11 @@ -use crate::accounts_zerocopy::AccountInfoRef; +use crate::accounts_zerocopy::*; use crate::error::*; use crate::health::*; use crate::i80f48::ClampToInt; use crate::state::*; use crate::accounts_ix::*; -use crate::logs::{Serum3OpenOrdersBalanceLogV2, TokenBalanceLog}; +use crate::logs::{emit_stack, Serum3OpenOrdersBalanceLogV2, TokenBalanceLog}; use crate::serum3_cpi::{ load_market_state, load_open_orders_ref, OpenOrdersAmounts, OpenOrdersSlim, }; @@ -25,6 +25,7 @@ pub fn serum3_place_order( order_type: Serum3OrderType, client_order_id: u64, limit: u16, + require_v2: bool, ) -> Result<()> { // Also required by serum3's place order require_gt!(limit_price_lots, 0); @@ -76,8 +77,9 @@ pub fn serum3_place_order( // let mut account = ctx.accounts.account.load_full_mut()?; let retriever = new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?; - let mut health_cache = - new_health_cache(&account.borrow(), &retriever).context("pre-withdraw init health")?; + let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap(); + let mut health_cache = new_health_cache(&account.borrow(), &retriever, now_ts) + .context("pre-withdraw init health")?; let pre_health_opt = if !account.fixed.is_in_health_region() { let pre_init_health = account.check_health_pre(&health_cache)?; Some(pre_init_health) @@ -86,18 +88,52 @@ pub fn serum3_place_order( }; // Check if the bank for the token whose balance is increased is in reduce-only mode - let receiver_bank_reduce_only = { + let receiver_bank_ai; + let receiver_bank_oracle; + let receiver_bank_reduce_only; + { // The token position already exists, but we need the active_index. let (_, _, active_index) = account.ensure_token_position(receiver_token_index)?; let group_key = ctx.accounts.group.key(); - let receiver_bank = retriever - .bank_and_oracle(&group_key, active_index, receiver_token_index)? - .0; - receiver_bank.are_deposits_reduce_only() - }; + let (receiver_bank, oracle) = + retriever.bank_and_oracle(&group_key, active_index, receiver_token_index)?; + receiver_bank_oracle = oracle; + receiver_bank_reduce_only = receiver_bank.are_deposits_reduce_only(); + + // The fixed_order account retriever can't give us mut references, so use the above + // call to .bank_and_oracle() as validation and then copy out the matching AccountInfo. + receiver_bank_ai = ctx.remaining_accounts[active_index].clone(); + // Double-check that we got the right account + let receiver_bank2 = receiver_bank_ai.load::()?; + assert_eq!(receiver_bank2.group, group_key); + assert_eq!(receiver_bank2.token_index, receiver_token_index); + } drop(retriever); + // + // Instruction version checking #4 + // + let is_v2_instruction; + { + let group = ctx.accounts.group.load()?; + let v1_available = group.is_ix_enabled(IxGate::Serum3PlaceOrder); + let v2_available = group.is_ix_enabled(IxGate::Serum3PlaceOrderV2); + is_v2_instruction = + require_v2 || !v1_available || (receiver_bank_ai.is_writable && v2_available); + if is_v2_instruction { + require!(v2_available, MangoError::IxIsDisabled); + require_msg_typed!( + receiver_bank_ai.is_writable, + MangoError::HealthAccountBankNotWritable, + "the receiver bank (token index {}) in the health account list must be writable", + receiver_token_index + ); + } else { + require!(v1_available, MangoError::IxIsDisabled); + } + } + // // Before-order tracking // @@ -184,38 +220,50 @@ pub fn serum3_place_order( if !before_had_bids { // The 0 state means uninitialized/no value serum.highest_placed_bid_inv = 0.0; + serum.lowest_placed_bid_inv = 0.0; } if !before_had_asks { serum.lowest_placed_ask = 0.0; + serum.highest_placed_ask = 0.0; } + // in the normal quote per base units + let limit_price = limit_price_lots as f64 * quote_lot_size as f64 / base_lot_size as f64; + let new_order_on_book = after_oo_free_slots != before_oo_free_slots; if new_order_on_book { match side { Serum3Side::Ask => { - // in the normal quote per base units - let limit_price = - limit_price_lots as f64 * quote_lot_size as f64 / base_lot_size as f64; serum.lowest_placed_ask = if serum.lowest_placed_ask == 0.0 { limit_price } else { serum.lowest_placed_ask.min(limit_price) }; + serum.highest_placed_ask = if serum.highest_placed_ask == 0.0 { + limit_price + } else { + serum.highest_placed_ask.max(limit_price) + } } Serum3Side::Bid => { // in base per quote units, to avoid a division in health - let limit_price_inv = - base_lot_size as f64 / (limit_price_lots as f64 * quote_lot_size as f64); + let limit_price_inv = 1.0 / limit_price; serum.highest_placed_bid_inv = if serum.highest_placed_bid_inv == 0.0 { limit_price_inv } else { // the highest bid has the lowest _inv value serum.highest_placed_bid_inv.min(limit_price_inv) }; + serum.lowest_placed_bid_inv = if serum.lowest_placed_bid_inv == 0.0 { + limit_price_inv + } else { + // lowest bid has max _inv value + serum.lowest_placed_bid_inv.max(limit_price_inv) + } } } } - emit!(Serum3OpenOrdersBalanceLogV2 { + emit_stack(Serum3OpenOrdersBalanceLogV2 { mango_group: ctx.accounts.group.key(), mango_account: ctx.accounts.account.key(), market_index: serum_market.market_index, @@ -236,14 +284,27 @@ pub fn serum3_place_order( let mut payer_bank = ctx.accounts.payer_bank.load_mut()?; - // Enforce min vault to deposits ratio - let withdrawn_from_vault = I80F48::from(before_vault - after_vault); - let position_native = account + // Update the potential token tracking in banks + // (for init weight scaling, deposit limit checks) + if is_v2_instruction { + let mut receiver_bank = receiver_bank_ai.load_mut::()?; + let (base_bank, quote_bank) = match side { + Serum3Side::Bid => (&mut receiver_bank, &mut payer_bank), + Serum3Side::Ask => (&mut payer_bank, &mut receiver_bank), + }; + update_bank_potential_tokens(serum, base_bank, quote_bank, &after_oo); + } else { + update_bank_potential_tokens_payer_only(serum, &mut payer_bank, &after_oo); + } + + // Track position before withdraw happens + let before_position_native = account .token_position_mut(payer_bank.token_index)? .0 .native(&payer_bank); // Charge the difference in vault balance to the user's account + // (must be done before limit checks like deposit limit) let vault_difference = { apply_vault_difference( ctx.accounts.account.key(), @@ -255,20 +316,80 @@ pub fn serum3_place_order( )? }; - if withdrawn_from_vault > position_native { + // Deposit limit check: Placing an order can increase deposit limit use on both + // the payer and receiver bank. Imagine placing a bid for 500 base @ 0.5: it would + // use up 1000 quote and 500 base because either could be deposit on cancel/fill. + // This is why this must happen after update_bank_potential_tokens() and any withdraws. + { + let receiver_bank = receiver_bank_ai.load::()?; + receiver_bank + .check_deposit_and_oo_limit() + .with_context(|| std::format!("on {}", receiver_bank.name()))?; + payer_bank + .check_deposit_and_oo_limit() + .with_context(|| std::format!("on {}", payer_bank.name()))?; + } + + // Payer bank safety checks like reduce-only, net borrows, vault-to-deposits ratio + let payer_bank_oracle = + payer_bank.oracle_price(&AccountInfoRef::borrow(&ctx.accounts.payer_oracle)?, None)?; + let withdrawn_from_vault = I80F48::from(before_vault - after_vault); + if withdrawn_from_vault > before_position_native { require_msg_typed!( !payer_bank.are_borrows_reduce_only(), MangoError::TokenInReduceOnlyMode, "the payer tokens cannot be borrowed" ); - let oracle_price = - payer_bank.oracle_price(&AccountInfoRef::borrow(&ctx.accounts.payer_oracle)?, None)?; payer_bank.enforce_min_vault_to_deposits_ratio((*ctx.accounts.payer_vault).as_ref())?; - payer_bank.check_net_borrows(oracle_price)?; + payer_bank.check_net_borrows(payer_bank_oracle)?; } - vault_difference.adjust_health_cache_token_balance(&mut health_cache, &payer_bank)?; + // Limit order price bands: If the order ends up on the book, ensure + // - a bid isn't too far below oracle + // - an ask isn't too far above oracle + // because placing orders that are guaranteed to never be hit can be bothersome: + // For example placing a very large bid near zero would make the potential_base_tokens + // value go through the roof, reducing available init margin for other users. + let band_threshold = serum_market.oracle_price_band(); + if new_order_on_book && band_threshold != f32::MAX { + let (base_oracle, quote_oracle) = match side { + Serum3Side::Bid => (&receiver_bank_oracle, &payer_bank_oracle), + Serum3Side::Ask => (&payer_bank_oracle, &receiver_bank_oracle), + }; + let base_oracle_f64 = base_oracle.to_num::(); + let quote_oracle_f64 = quote_oracle.to_num::(); + // this has the same units as base_oracle: USD per BASE; limit_price is in QUOTE per BASE + let limit_price_in_dollar = limit_price * quote_oracle_f64; + let band_factor = 1.0 + band_threshold as f64; + match side { + Serum3Side::Bid => { + require_msg_typed!( + limit_price_in_dollar * band_factor >= base_oracle_f64, + MangoError::Serum3PriceBandExceeded, + "bid price {} must be larger than {} ({}% of oracle)", + limit_price, + base_oracle_f64 / (quote_oracle_f64 * band_factor), + (100.0 / band_factor) as u64, + ); + } + Serum3Side::Ask => { + require_msg_typed!( + limit_price_in_dollar <= base_oracle_f64 * band_factor, + MangoError::Serum3PriceBandExceeded, + "ask price {} must be smaller than {} ({}% of oracle)", + limit_price, + base_oracle_f64 * band_factor / quote_oracle_f64, + (100.0 * band_factor) as u64, + ); + } + } + } + // Health cache updates for the changed account state + let receiver_bank = receiver_bank_ai.load::()?; + // update scaled weights for receiver bank + health_cache.adjust_token_balance(&receiver_bank, I80F48::ZERO)?; + vault_difference.adjust_health_cache_token_balance(&mut health_cache, &payer_bank)?; let serum_account = account.serum3_orders(serum_market.market_index)?; oo_difference.recompute_health_cache_serum3_state( &mut health_cache, @@ -378,40 +499,29 @@ fn apply_vault_difference( .min(I80F48::ZERO) .abs() .to_num::(); - // amount of tokens transferred to serum3 reserved that were taken from deposits - let used_deposits = native_before - .min(-needed_change) - .max(I80F48::ZERO) - .to_num::(); let indexed_position = position.indexed_position; let market = account.serum3_orders_mut(serum_market_index).unwrap(); let borrows_without_fee; - let deposits_reserved; if bank.token_index == market.base_token_index { borrows_without_fee = &mut market.base_borrows_without_fee; - deposits_reserved = &mut market.base_deposits_reserved; } else if bank.token_index == market.quote_token_index { borrows_without_fee = &mut market.quote_borrows_without_fee; - deposits_reserved = &mut market.quote_deposits_reserved; } else { return Err(error_msg!( "assert failed: apply_vault_difference called with bad token index" )); }; - // Only for place: Add to potential borrow amount and reserved deposits + // Only for place: Add to potential borrow amount *borrows_without_fee += new_borrows; - *deposits_reserved += used_deposits; - let used_deposits_signed: i64 = used_deposits.try_into().unwrap(); - bank.deposits_in_serum += used_deposits_signed; // Only for settle/liq_force_cancel: Reduce the potential borrow amounts if needed_change > 0 { *borrows_without_fee = (*borrows_without_fee).saturating_sub(needed_change.to_num::()); } - emit!(TokenBalanceLog { + emit_stack(TokenBalanceLog { mango_group: bank.group, mango_account: account_pk, token_index: bank.token_index, @@ -498,25 +608,10 @@ pub fn apply_settle_changes( )?; // Tokens were moved from open orders into banks again: also update the tracking - // for deposits_in_serum on the banks. + // for potential_serum_tokens on the banks. { let serum_orders = account.serum3_orders_mut(serum_market.market_index)?; - - let after_base_reserved = after_oo.native_base_reserved(); - if after_base_reserved < serum_orders.base_deposits_reserved { - let diff = serum_orders.base_deposits_reserved - after_base_reserved; - serum_orders.base_deposits_reserved = after_base_reserved; - let diff_signed: i64 = diff.try_into().unwrap(); - base_bank.deposits_in_serum -= diff_signed; - } - - let after_quote_reserved = after_oo.native_quote_reserved(); - if after_quote_reserved < serum_orders.quote_deposits_reserved { - let diff = serum_orders.quote_deposits_reserved - after_quote_reserved; - serum_orders.quote_deposits_reserved = after_quote_reserved; - let diff_signed: i64 = diff.try_into().unwrap(); - quote_bank.deposits_in_serum -= diff_signed; - } + update_bank_potential_tokens(serum_orders, base_bank, quote_bank, after_oo); } if let Some(health_cache) = health_cache { @@ -534,6 +629,58 @@ pub fn apply_settle_changes( Ok(()) } +fn update_bank_potential_tokens_payer_only( + serum_orders: &mut Serum3Orders, + payer_bank: &mut Bank, + oo: &OpenOrdersSlim, +) { + // Do the tracking for the avaliable bank + if serum_orders.base_token_index == payer_bank.token_index { + let new_base = oo.native_base_total() + + (oo.native_quote_reserved() as f64 * serum_orders.lowest_placed_bid_inv) as u64; + let old_base = serum_orders.potential_base_tokens; + + payer_bank.update_potential_serum_tokens(old_base, new_base); + serum_orders.potential_base_tokens = new_base; + } else { + assert_eq!(serum_orders.quote_token_index, payer_bank.token_index); + + let new_quote = oo.native_quote_total() + + (oo.native_base_reserved() as f64 * serum_orders.highest_placed_ask) as u64; + let old_quote = serum_orders.potential_quote_tokens; + + payer_bank.update_potential_serum_tokens(old_quote, new_quote); + serum_orders.potential_quote_tokens = new_quote; + } +} + +fn update_bank_potential_tokens( + serum_orders: &mut Serum3Orders, + base_bank: &mut Bank, + quote_bank: &mut Bank, + oo: &OpenOrdersSlim, +) { + assert_eq!(serum_orders.base_token_index, base_bank.token_index); + assert_eq!(serum_orders.quote_token_index, quote_bank.token_index); + + // Potential tokens are all tokens on the side, plus reserved on the other side + // converted at favorable price. This creates an overestimation of the potential + // base and quote tokens flowing out of this open orders account. + let new_base = oo.native_base_total() + + (oo.native_quote_reserved() as f64 * serum_orders.lowest_placed_bid_inv) as u64; + let new_quote = oo.native_quote_total() + + (oo.native_base_reserved() as f64 * serum_orders.highest_placed_ask) as u64; + + let old_base = serum_orders.potential_base_tokens; + let old_quote = serum_orders.potential_quote_tokens; + + base_bank.update_potential_serum_tokens(old_base, new_base); + quote_bank.update_potential_serum_tokens(old_quote, new_quote); + + serum_orders.potential_base_tokens = new_base; + serum_orders.potential_quote_tokens = new_quote; +} + fn cpi_place_order(ctx: &Serum3PlaceOrder, order: NewOrderInstructionV3) -> Result<()> { use crate::serum3_cpi; diff --git a/programs/mango-v4/src/instructions/serum3_register_market.rs b/programs/mango-v4/src/instructions/serum3_register_market.rs index fe01d489b..532060392 100644 --- a/programs/mango-v4/src/instructions/serum3_register_market.rs +++ b/programs/mango-v4/src/instructions/serum3_register_market.rs @@ -6,12 +6,13 @@ use crate::state::*; use crate::util::fill_from_str; use crate::accounts_ix::*; -use crate::logs::Serum3RegisterMarketLog; +use crate::logs::{emit_stack, Serum3RegisterMarketLog}; pub fn serum3_register_market( ctx: Context, market_index: Serum3MarketIndex, name: String, + oracle_price_band: f32, ) -> Result<()> { // TODO: must guard against accidentally using the same market_index twice! @@ -44,6 +45,7 @@ pub fn serum3_register_market( market_index, bump: *ctx.bumps.get("serum_market").ok_or(MangoError::SomeError)?, padding2: Default::default(), + oracle_price_band, registration_time: Clock::get()?.unix_timestamp.try_into().unwrap(), reserved: [0; 128], }; @@ -55,7 +57,7 @@ pub fn serum3_register_market( reserved: [0; 38], }; - emit!(Serum3RegisterMarketLog { + emit_stack(Serum3RegisterMarketLog { mango_group: ctx.accounts.group.key(), serum_market: ctx.accounts.serum_market.key(), market_index, diff --git a/programs/mango-v4/src/instructions/serum3_settle_funds.rs b/programs/mango-v4/src/instructions/serum3_settle_funds.rs index 77bb2e2b4..e1d61fcf6 100644 --- a/programs/mango-v4/src/instructions/serum3_settle_funds.rs +++ b/programs/mango-v4/src/instructions/serum3_settle_funds.rs @@ -7,8 +7,9 @@ use crate::state::*; use super::apply_settle_changes; use crate::accounts_ix::*; -use crate::logs::Serum3OpenOrdersBalanceLogV2; -use crate::logs::{LoanOriginationFeeInstruction, WithdrawLoanLog}; +use crate::logs::{ + emit_stack, LoanOriginationFeeInstruction, Serum3OpenOrdersBalanceLogV2, WithdrawLoanLog, +}; use crate::accounts_zerocopy::AccountInfoRef; @@ -132,7 +133,7 @@ pub fn serum3_settle_funds<'info>( v2.map(|d| d.quote_oracle.as_ref()), )?; - emit!(Serum3OpenOrdersBalanceLogV2 { + emit_stack(Serum3OpenOrdersBalanceLogV2 { mango_group: accounts.group.key(), mango_account: accounts.account.key(), market_index: serum_market.market_index, @@ -188,14 +189,14 @@ pub fn charge_loan_origination_fees( }) .transpose()?; - emit!(WithdrawLoanLog { + emit_stack(WithdrawLoanLog { mango_group: *group_pubkey, mango_account: *account_pubkey, token_index: base_bank.token_index, loan_amount: withdraw_result.loan_amount.to_bits(), loan_origination_fee: withdraw_result.loan_origination_fee.to_bits(), instruction: LoanOriginationFeeInstruction::Serum3SettleFunds, - price: base_oracle_price.map(|p| p.to_bits()) + price: base_oracle_price.map(|p| p.to_bits()), }); } @@ -224,14 +225,14 @@ pub fn charge_loan_origination_fees( }) .transpose()?; - emit!(WithdrawLoanLog { + emit_stack(WithdrawLoanLog { mango_group: *group_pubkey, mango_account: *account_pubkey, token_index: quote_bank.token_index, loan_amount: withdraw_result.loan_amount.to_bits(), loan_origination_fee: withdraw_result.loan_origination_fee.to_bits(), instruction: LoanOriginationFeeInstruction::Serum3SettleFunds, - price: quote_oracle_price.map(|p| p.to_bits()) + price: quote_oracle_price.map(|p| p.to_bits()), }); } diff --git a/programs/mango-v4/src/instructions/token_conditional_swap_cancel.rs b/programs/mango-v4/src/instructions/token_conditional_swap_cancel.rs index 58ced0cfd..e95c697b1 100644 --- a/programs/mango-v4/src/instructions/token_conditional_swap_cancel.rs +++ b/programs/mango-v4/src/instructions/token_conditional_swap_cancel.rs @@ -2,7 +2,7 @@ use anchor_lang::prelude::*; use crate::accounts_ix::*; use crate::error::MangoError; -use crate::logs::TokenConditionalSwapCancelLog; +use crate::logs::{emit_stack, TokenConditionalSwapCancelLog}; use crate::state::*; #[allow(clippy::too_many_arguments)] @@ -31,7 +31,7 @@ pub fn token_conditional_swap_cancel( ); *tcs = TokenConditionalSwap::default(); - emit!(TokenConditionalSwapCancelLog { + emit_stack(TokenConditionalSwapCancelLog { mango_group: ctx.accounts.group.key(), mango_account: ctx.accounts.account.key(), id: token_conditional_swap_id, diff --git a/programs/mango-v4/src/instructions/token_conditional_swap_create.rs b/programs/mango-v4/src/instructions/token_conditional_swap_create.rs index 1cd730d61..454c4a232 100644 --- a/programs/mango-v4/src/instructions/token_conditional_swap_create.rs +++ b/programs/mango-v4/src/instructions/token_conditional_swap_create.rs @@ -1,7 +1,7 @@ use anchor_lang::prelude::*; use crate::accounts_ix::*; -use crate::logs::TokenConditionalSwapCreateLogV3; +use crate::logs::{emit_stack, TokenConditionalSwapCreateLogV3}; use crate::state::*; #[allow(clippy::too_many_arguments)] @@ -56,7 +56,7 @@ pub fn token_conditional_swap_create( require_gte!(tcs.price_lower_limit, 0.0); require_gte!(tcs.price_upper_limit, 0.0); - emit!(TokenConditionalSwapCreateLogV3 { + emit_stack(TokenConditionalSwapCreateLogV3 { mango_group: ctx.accounts.group.key(), mango_account: ctx.accounts.account.key(), id, diff --git a/programs/mango-v4/src/instructions/token_conditional_swap_start.rs b/programs/mango-v4/src/instructions/token_conditional_swap_start.rs index 7f4ad67d8..4d3624ac0 100644 --- a/programs/mango-v4/src/instructions/token_conditional_swap_start.rs +++ b/programs/mango-v4/src/instructions/token_conditional_swap_start.rs @@ -5,8 +5,7 @@ use crate::accounts_ix::*; use crate::error::*; use crate::health::*; use crate::i80f48::ClampToInt; -use crate::logs::TokenBalanceLog; -use crate::logs::TokenConditionalSwapStartLog; +use crate::logs::{emit_stack, TokenBalanceLog, TokenConditionalSwapStartLog}; use crate::state::*; #[allow(clippy::too_many_arguments)] @@ -44,7 +43,7 @@ pub fn token_conditional_swap_start( MangoError::TokenConditionalSwapTypeNotStartable ); - let mut health_cache = new_health_cache(&liqee.borrow(), &account_retriever) + let mut health_cache = new_health_cache(&liqee.borrow(), &account_retriever, now_ts) .context("create liqee health cache")?; let pre_init_health = liqee.check_health_pre(&health_cache)?; @@ -65,8 +64,13 @@ pub fn token_conditional_swap_start( // We allow the incentive to be < 1 native token because of tokens like BTC, where 1 native token // far exceeds the incentive value. let incentive = (I80F48::from(TCS_START_INCENTIVE) / sell_oracle_price) - .min(I80F48::from(tcs.remaining_sell())); - // However, the tcs tracking is in u64 units. We need to live with the fact of + .min(I80F48::from(tcs.remaining_sell())) + // Limiting to remaining deposits is too strict, since this could be a deposit + // to deposit transfer, but this is good enough to make the incentive deposit + // guaranteed to not exceed the limit. + .min(sell_bank.remaining_deposits_until_limit()) + .max(I80F48::ZERO); + // The tcs tracking is in u64 units. We need to live with the fact of // not accounting the incentive fee perfectly. let incentive_native = incentive.clamp_to_u64(); @@ -74,28 +78,27 @@ pub fn token_conditional_swap_start( let (liqor_sell_token, liqor_sell_raw_index, _) = liqor.ensure_token_position(sell_token_index)?; - sell_bank.deposit(liqor_sell_token, incentive, now_ts)?; - - // This withdraw might be a borrow, so can fail due to net borrows or reduce-only let liqee_sell_pre_balance = liqee_sell_token.native(sell_bank); - sell_bank.withdraw_with_fee(liqee_sell_token, incentive, now_ts)?; + sell_bank.checked_transfer_with_fee( + liqee_sell_token, + incentive, + liqor_sell_token, + incentive, + now_ts, + sell_oracle_price, + )?; let liqee_sell_post_balance = liqee_sell_token.native(sell_bank); if liqee_sell_post_balance < 0 { require!( tcs.allow_creating_borrows(), MangoError::TokenConditionalSwapCantPayIncentive ); - require!( - !sell_bank.are_borrows_reduce_only(), - MangoError::TokenInReduceOnlyMode - ); - sell_bank.check_net_borrows(sell_oracle_price)?; } health_cache .adjust_token_balance(sell_bank, liqee_sell_post_balance - liqee_sell_pre_balance)?; - emit!(TokenBalanceLog { + emit_stack(TokenBalanceLog { mango_group: *group_pk, mango_account: liqee_key, token_index: sell_token_index, @@ -103,7 +106,7 @@ pub fn token_conditional_swap_start( deposit_index: sell_bank.deposit_index.to_bits(), borrow_index: sell_bank.borrow_index.to_bits(), }); - emit!(TokenBalanceLog { + emit_stack(TokenBalanceLog { mango_group: *group_pk, mango_account: liqor_key, token_index: sell_token_index, @@ -111,7 +114,7 @@ pub fn token_conditional_swap_start( deposit_index: sell_bank.deposit_index.to_bits(), borrow_index: sell_bank.borrow_index.to_bits(), }); - emit!(TokenConditionalSwapStartLog { + emit_stack(TokenConditionalSwapStartLog { mango_group: *group_pk, mango_account: liqee_key, caller: liqor_key, diff --git a/programs/mango-v4/src/instructions/token_conditional_swap_trigger.rs b/programs/mango-v4/src/instructions/token_conditional_swap_trigger.rs index 837ec514a..1d25846b4 100644 --- a/programs/mango-v4/src/instructions/token_conditional_swap_trigger.rs +++ b/programs/mango-v4/src/instructions/token_conditional_swap_trigger.rs @@ -5,10 +5,9 @@ use crate::accounts_ix::*; use crate::error::*; use crate::health::*; use crate::i80f48::ClampToInt; -use crate::logs::TokenConditionalSwapCancelLog; use crate::logs::{ - LoanOriginationFeeInstruction, TokenBalanceLog, TokenConditionalSwapTriggerLogV3, - WithdrawLoanLog, + emit_stack, LoanOriginationFeeInstruction, TokenBalanceLog, TokenConditionalSwapCancelLog, + TokenConditionalSwapTriggerLogV3, WithdrawLoanLog, }; use crate::state::*; @@ -73,7 +72,7 @@ pub fn token_conditional_swap_trigger( liqee.token_decrement_dust_deactivate(sell_bank, now_ts, liqee_key)?; msg!("TokenConditionalSwap is expired, removing"); - emit!(TokenConditionalSwapCancelLog { + emit_stack(TokenConditionalSwapCancelLog { mango_group: ctx.accounts.group.key(), mango_account: ctx.accounts.liqee.key(), id: token_conditional_swap_id, @@ -87,7 +86,7 @@ pub fn token_conditional_swap_trigger( // changes when the tcs was created. liqee.ensure_token_position(buy_token_index)?; liqee.ensure_token_position(sell_token_index)?; - let mut liqee_health_cache = new_health_cache(&liqee.borrow(), &account_retriever) + let mut liqee_health_cache = new_health_cache(&liqee.borrow(), &account_retriever, now_ts) .context("create liqee health cache")?; let (buy_bank, buy_token_price, sell_bank_and_oracle_opt) = @@ -118,8 +117,13 @@ pub fn token_conditional_swap_trigger( ); // Check liqor health, liqee health is checked inside (has to be, since tcs closure depends on it) - let liqor_health = compute_health(&liqor.borrow(), HealthType::Init, &account_retriever) - .context("compute liqor health")?; + let liqor_health = compute_health( + &liqor.borrow(), + HealthType::Init, + &account_retriever, + now_ts, + ) + .context("compute liqor health")?; require!(liqor_health >= 0, MangoError::HealthMustBePositive); Ok(()) @@ -290,9 +294,15 @@ fn action( let (liqee_buy_token, liqee_buy_raw_index) = liqee.token_position_mut(tcs.buy_token_index)?; let (liqor_buy_token, liqor_buy_raw_index) = liqor.token_position_mut(tcs.buy_token_index)?; - buy_bank.deposit(liqee_buy_token, buy_token_amount_i80f48, now_ts)?; - let liqor_buy_withdraw = - buy_bank.withdraw_with_fee(liqor_buy_token, buy_token_amount_i80f48, now_ts)?; + let buy_transfer = buy_bank.checked_transfer_with_fee( + liqor_buy_token, + buy_token_amount_i80f48, + liqee_buy_token, + buy_token_amount_i80f48, + now_ts, + buy_token_price, + )?; + let liqor_buy_active = buy_transfer.source_is_active; let post_liqee_buy_token = liqee_buy_token.native(&buy_bank); let post_liqor_buy_token = liqor_buy_token.native(&buy_bank); @@ -303,28 +313,18 @@ fn action( liqee.token_position_mut(tcs.sell_token_index)?; let (liqor_sell_token, liqor_sell_raw_index) = liqor.token_position_mut(tcs.sell_token_index)?; - let liqor_sell_active = sell_bank.deposit( + let sell_transfer = sell_bank.checked_transfer_with_fee( + liqee_sell_token, + I80F48::from(sell_token_amount_from_liqee), liqor_sell_token, I80F48::from(sell_token_amount_to_liqor), now_ts, + sell_token_price, )?; - let liqee_sell_withdraw = sell_bank.withdraw_with_fee( - liqee_sell_token, - I80F48::from(sell_token_amount_from_liqee), - now_ts, - )?; + let liqor_sell_active = sell_transfer.target_is_active; sell_bank.collected_fees_native += I80F48::from(maker_fee + taker_fee); - // Check net borrows on both banks. - // - // While tcs triggering doesn't cause actual tokens to leave the platform, it can increase the amount - // of borrows. For instance, if someone with USDC has a tcs to buy SOL and sell BTC, execution would - // create BTC borrows (unless the executor had BTC borrows that get repaid by the execution, but - // most executors will work on margin) - buy_bank.check_net_borrows(buy_token_price)?; - sell_bank.check_net_borrows(sell_token_price)?; - let post_liqee_sell_token = liqee_sell_token.native(&sell_bank); let post_liqor_sell_token = liqor_sell_token.native(&sell_bank); let liqee_sell_indexed_position = liqee_sell_token.indexed_position; @@ -332,7 +332,7 @@ fn action( // With a scanning account retriever, it's safe to deactivate inactive token positions immediately. // Liqee positions can only be deactivated if the tcs is closed (see below). - if !liqor_buy_withdraw.position_is_active { + if !liqor_buy_active { liqor.deactivate_token_position_and_log(liqor_buy_raw_index, liqor_key); } if !liqor_sell_active { @@ -342,7 +342,7 @@ fn action( // Log info // liqee buy token - emit!(TokenBalanceLog { + emit_stack(TokenBalanceLog { mango_group: liqee.fixed.group, mango_account: liqee_key, token_index: tcs.buy_token_index, @@ -351,7 +351,7 @@ fn action( borrow_index: buy_bank.borrow_index.to_bits(), }); // liqee sell token - emit!(TokenBalanceLog { + emit_stack(TokenBalanceLog { mango_group: liqee.fixed.group, mango_account: liqee_key, token_index: tcs.sell_token_index, @@ -360,7 +360,7 @@ fn action( borrow_index: sell_bank.borrow_index.to_bits(), }); // liqor buy token - emit!(TokenBalanceLog { + emit_stack(TokenBalanceLog { mango_group: liqee.fixed.group, mango_account: liqor_key, token_index: tcs.buy_token_index, @@ -369,7 +369,7 @@ fn action( borrow_index: buy_bank.borrow_index.to_bits(), }); // liqor sell token - emit!(TokenBalanceLog { + emit_stack(TokenBalanceLog { mango_group: liqee.fixed.group, mango_account: liqor_key, token_index: tcs.sell_token_index, @@ -378,24 +378,24 @@ fn action( borrow_index: sell_bank.borrow_index.to_bits(), }); - if liqor_buy_withdraw.has_loan() { - emit!(WithdrawLoanLog { + if buy_transfer.has_loan() { + emit_stack(WithdrawLoanLog { mango_group: liqee.fixed.group, mango_account: liqor_key, token_index: tcs.buy_token_index, - loan_amount: liqor_buy_withdraw.loan_amount.to_bits(), - loan_origination_fee: liqor_buy_withdraw.loan_origination_fee.to_bits(), + loan_amount: buy_transfer.loan_amount.to_bits(), + loan_origination_fee: buy_transfer.loan_origination_fee.to_bits(), instruction: LoanOriginationFeeInstruction::TokenConditionalSwapTrigger, price: Some(buy_token_price.to_bits()), }); } - if liqee_sell_withdraw.has_loan() { - emit!(WithdrawLoanLog { + if sell_transfer.has_loan() { + emit_stack(WithdrawLoanLog { mango_group: liqee.fixed.group, mango_account: liqee_key, token_index: tcs.sell_token_index, - loan_amount: liqee_sell_withdraw.loan_amount.to_bits(), - loan_origination_fee: liqee_sell_withdraw.loan_origination_fee.to_bits(), + loan_amount: sell_transfer.loan_amount.to_bits(), + loan_origination_fee: sell_transfer.loan_origination_fee.to_bits(), instruction: LoanOriginationFeeInstruction::TokenConditionalSwapTrigger, price: Some(sell_token_price.to_bits()), }); @@ -483,7 +483,7 @@ fn action( liqee.token_decrement_dust_deactivate(sell_bank, now_ts, liqee_key)?; } - emit!(TokenConditionalSwapTriggerLogV3 { + emit_stack(TokenConditionalSwapTriggerLogV3 { mango_group: liqee.fixed.group, liqee: liqee_key, liqor: liqor_key, @@ -797,7 +797,7 @@ mod tests { let retriever = ScanningAccountRetriever::new_with_staleness(&ais, &setup.group, None).unwrap(); let mut liqee_health_cache = - crate::health::new_health_cache(&setup.liqee.borrow(), &retriever).unwrap(); + crate::health::new_health_cache(&setup.liqee.borrow(), &retriever, 0).unwrap(); action( &mut self.liqor.borrow_mut(), diff --git a/programs/mango-v4/src/instructions/token_deposit.rs b/programs/mango-v4/src/instructions/token_deposit.rs index f87974980..33fa8c6d6 100644 --- a/programs/mango-v4/src/instructions/token_deposit.rs +++ b/programs/mango-v4/src/instructions/token_deposit.rs @@ -10,7 +10,7 @@ use crate::health::*; use crate::state::*; use crate::accounts_ix::*; -use crate::logs::{DepositLog, TokenBalanceLog}; +use crate::logs::*; struct DepositCommon<'a, 'info> { pub group: &'a AccountLoader<'info, Group>, @@ -90,17 +90,22 @@ impl<'a, 'info> DepositCommon<'a, 'info> { // Get the oracle price, even if stale or unconfident: We want to allow users // to deposit to close borrows or do other fixes even if the oracle is bad. - let unsafe_oracle_price = oracle_state_unchecked( + let unsafe_oracle_state = oracle_state_unchecked( &AccountInfoRef::borrow(self.oracle.as_ref())?, bank.mint_decimals, - )? - .price; + )?; + let unsafe_oracle_price = unsafe_oracle_state.price; + + // If increasing total deposits, check deposit limits + if indexed_position > 0 { + bank.check_deposit_and_oo_limit()?; + } // Update the net deposits - adjust by price so different tokens are on the same basis (in USD terms) let amount_usd = (amount_i80f48 * unsafe_oracle_price).to_num::(); account.fixed.net_deposits += amount_usd; - emit!(TokenBalanceLog { + emit_stack(TokenBalanceLog { mango_group: self.group.key(), mango_account: self.account.key(), token_index, @@ -114,11 +119,12 @@ impl<'a, 'info> DepositCommon<'a, 'info> { // Health computation // let retriever = new_fixed_order_account_retriever(remaining_accounts, &account.borrow())?; + let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap(); // We only compute health to check if the account leaves the being_liquidated state. // So it's ok to possibly skip token positions for bad oracles and compute a health // value that is too low. - let cache = new_health_cache_skipping_bad_oracles(&account.borrow(), &retriever)?; + let cache = new_health_cache_skipping_bad_oracles(&account.borrow(), &retriever, now_ts)?; // Since depositing can only increase health, we can skip the usual pre-health computation. // Also, TokenDeposit is one of the rare instructions that is allowed even during being_liquidated. @@ -162,7 +168,20 @@ impl<'a, 'info> DepositCommon<'a, 'info> { account.deactivate_token_position_and_log(raw_token_index, self.account.key()); } - emit!(DepositLog { + unsafe { + const POS_PTR: *mut usize = 0x300000000 as usize as *mut usize; + msg!("heap {}", *POS_PTR); + } + + // emit_stack(DepositLog { + // mango_group: self.group.key(), + // mango_account: self.account.key(), + // signer: self.token_authority.key(), + // token_index, + // quantity: amount_i80f48.to_num::(), + // price: unsafe_oracle_price.to_bits(), + // }); + emit_stack(DepositLog { mango_group: self.group.key(), mango_account: self.account.key(), signer: self.token_authority.key(), @@ -171,6 +190,11 @@ impl<'a, 'info> DepositCommon<'a, 'info> { price: unsafe_oracle_price.to_bits(), }); + unsafe { + const POS_PTR: *mut usize = 0x300000000 as usize as *mut usize; + msg!("heap {}", *POS_PTR); + } + Ok(()) } } diff --git a/programs/mango-v4/src/instructions/token_edit.rs b/programs/mango-v4/src/instructions/token_edit.rs index db645f3ff..948f7a3d5 100644 --- a/programs/mango-v4/src/instructions/token_edit.rs +++ b/programs/mango-v4/src/instructions/token_edit.rs @@ -8,7 +8,7 @@ use crate::error::MangoError; use crate::state::*; use crate::accounts_ix::*; -use crate::logs::TokenMetaDataLog; +use crate::logs::{emit_stack, TokenMetaDataLog}; use crate::util::fill_from_str; #[allow(unused_variables)] @@ -42,6 +42,15 @@ pub fn token_edit( token_conditional_swap_taker_fee_rate_opt: Option, token_conditional_swap_maker_fee_rate_opt: Option, flash_loan_swap_fee_rate_opt: Option, + interest_curve_scaling_opt: Option, + interest_target_utilization_opt: Option, + maint_weight_shift_start_opt: Option, + maint_weight_shift_end_opt: Option, + maint_weight_shift_asset_target_opt: Option, + maint_weight_shift_liab_target_opt: Option, + maint_weight_shift_abort: bool, + set_fallback_oracle: bool, // unused, introduced in v0.22 + deposit_limit_opt: Option, ) -> Result<()> { let group = ctx.accounts.group.load()?; @@ -339,6 +348,106 @@ pub fn token_edit( bank.flash_loan_swap_fee_rate = fee_rate; require_group_admin = true; } + + if let Some(interest_curve_scaling) = interest_curve_scaling_opt { + msg!( + "Interest curve scaling old {:?}, new {:?}", + bank.interest_curve_scaling, + interest_curve_scaling + ); + require_gte!(interest_curve_scaling, 1.0); + bank.interest_curve_scaling = interest_curve_scaling.into(); + require_group_admin = true; + } + if let Some(interest_target_utilization) = interest_target_utilization_opt { + msg!( + "Interest target utilization old {:?}, new {:?}", + bank.interest_target_utilization, + interest_target_utilization + ); + require_gte!(interest_target_utilization, 0.0); + bank.interest_target_utilization = interest_target_utilization; + require_group_admin = true; + } + + if maint_weight_shift_abort { + let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap(); + let (maint_asset_weight, maint_liab_weight) = bank.maint_weights(now_ts); + bank.maint_asset_weight = maint_asset_weight; + bank.maint_liab_weight = maint_liab_weight; + bank.maint_weight_shift_start = 0; + bank.maint_weight_shift_end = 0; + bank.maint_weight_shift_duration_inv = I80F48::ZERO; + bank.maint_weight_shift_asset_target = I80F48::ZERO; + bank.maint_weight_shift_liab_target = I80F48::ZERO; + msg!( + "Maint weight shift aborted, current maint weights asset {} liab {}", + maint_asset_weight, + maint_liab_weight, + ); + // Allow execution by group admin or security admin + } + if let Some(maint_weight_shift_start) = maint_weight_shift_start_opt { + msg!( + "Maint weight shift start old {:?}, new {:?}", + bank.maint_weight_shift_start, + maint_weight_shift_start + ); + bank.maint_weight_shift_start = maint_weight_shift_start; + require_group_admin = true; + } + if let Some(maint_weight_shift_end) = maint_weight_shift_end_opt { + msg!( + "Maint weight shift end old {:?}, new {:?}", + bank.maint_weight_shift_end, + maint_weight_shift_end + ); + bank.maint_weight_shift_end = maint_weight_shift_end; + require_group_admin = true; + } + if let Some(maint_weight_shift_asset_target) = maint_weight_shift_asset_target_opt { + msg!( + "Maint weight shift asset target old {:?}, new {:?}", + bank.maint_weight_shift_asset_target, + maint_weight_shift_asset_target + ); + bank.maint_weight_shift_asset_target = + I80F48::from_num(maint_weight_shift_asset_target); + require_group_admin = true; + } + if let Some(maint_weight_shift_liab_target) = maint_weight_shift_liab_target_opt { + msg!( + "Maint weight shift liab target old {:?}, new {:?}", + bank.maint_weight_shift_liab_target, + maint_weight_shift_liab_target + ); + bank.maint_weight_shift_liab_target = I80F48::from_num(maint_weight_shift_liab_target); + require_group_admin = true; + } + if maint_weight_shift_start_opt.is_some() || maint_weight_shift_end_opt.is_some() { + let was_enabled = bank.maint_weight_shift_duration_inv.is_positive(); + if bank.maint_weight_shift_end <= bank.maint_weight_shift_start { + bank.maint_weight_shift_duration_inv = I80F48::ZERO; + } else { + bank.maint_weight_shift_duration_inv = I80F48::ONE + / I80F48::from(bank.maint_weight_shift_end - bank.maint_weight_shift_start); + } + msg!( + "Maint weight shift enabled old {}, new {}", + was_enabled, + bank.maint_weight_shift_duration_inv.is_positive(), + ); + } + + if let Some(deposit_limit) = deposit_limit_opt { + msg!( + "Deposit limit old {:?}, new {:?}", + bank.deposit_limit, + deposit_limit + ); + bank.deposit_limit = deposit_limit; + require_group_admin = true; + } } // account constraint #1 @@ -359,7 +468,7 @@ pub fn token_edit( let bank = ctx.remaining_accounts.first().unwrap().load_mut::()?; bank.verify()?; - emit!(TokenMetaDataLog { + emit_stack(TokenMetaDataLog { mango_group: ctx.accounts.group.key(), mint: mint_info.mint.key(), token_index: bank.token_index, diff --git a/programs/mango-v4/src/instructions/token_force_close_borrows_with_token.rs b/programs/mango-v4/src/instructions/token_force_close_borrows_with_token.rs index f32d5b6b1..da38fe112 100644 --- a/programs/mango-v4/src/instructions/token_force_close_borrows_with_token.rs +++ b/programs/mango-v4/src/instructions/token_force_close_borrows_with_token.rs @@ -1,7 +1,7 @@ use crate::accounts_ix::*; use crate::error::*; use crate::health::*; -use crate::logs::{TokenBalanceLog, TokenForceCloseBorrowsWithTokenLog}; +use crate::logs::{emit_stack, TokenBalanceLog, TokenForceCloseBorrowsWithTokenLog}; use crate::state::*; use anchor_lang::prelude::*; use fixed::types::I80F48; @@ -131,7 +131,7 @@ pub fn token_force_close_borrows_with_token( ); // liqee asset - emit!(TokenBalanceLog { + emit_stack(TokenBalanceLog { mango_group: liqee.fixed.group, mango_account: liqee_key, token_index: asset_token_index, @@ -140,7 +140,7 @@ pub fn token_force_close_borrows_with_token( borrow_index: asset_bank.borrow_index.to_bits(), }); // liqee liab - emit!(TokenBalanceLog { + emit_stack(TokenBalanceLog { mango_group: liqee.fixed.group, mango_account: liqee_key, token_index: liab_token_index, @@ -149,7 +149,7 @@ pub fn token_force_close_borrows_with_token( borrow_index: liab_bank.borrow_index.to_bits(), }); // liqor asset - emit!(TokenBalanceLog { + emit_stack(TokenBalanceLog { mango_group: liqee.fixed.group, mango_account: liqor_key, token_index: asset_token_index, @@ -158,7 +158,7 @@ pub fn token_force_close_borrows_with_token( borrow_index: asset_bank.borrow_index.to_bits(), }); // liqor liab - emit!(TokenBalanceLog { + emit_stack(TokenBalanceLog { mango_group: liqee.fixed.group, mango_account: liqor_key, token_index: liab_token_index, @@ -167,7 +167,7 @@ pub fn token_force_close_borrows_with_token( borrow_index: liab_bank.borrow_index.to_bits(), }); - emit!(TokenForceCloseBorrowsWithTokenLog { + emit_stack(TokenForceCloseBorrowsWithTokenLog { mango_group: liqee.fixed.group, liqee: liqee_key, liqor: liqor_key, @@ -186,7 +186,8 @@ pub fn token_force_close_borrows_with_token( MangoError::SomeError ); - let liqee_health_cache = new_health_cache(&liqee.borrow(), &mut account_retriever) + let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap(); + let liqee_health_cache = new_health_cache(&liqee.borrow(), &mut account_retriever, now_ts) .context("create liqee health cache")?; let liqee_liq_end_health = liqee_health_cache.health(HealthType::LiquidationEnd); liqee @@ -211,12 +212,17 @@ pub fn token_force_close_borrows_with_token( // Check liqor's health // This should always improve liqor health, since we decrease the zero-asset-weight // liab token and gain some asset token, this check is just for denfensive measure - let liqor_health = compute_health(&liqor.borrow(), HealthType::Init, &mut account_retriever) - .context("compute liqor health")?; + let liqor_health = compute_health( + &liqor.borrow(), + HealthType::Init, + &mut account_retriever, + now_ts, + ) + .context("compute liqor health")?; require!(liqor_health >= 0, MangoError::HealthMustBePositive); // TODO log - // emit!(TokenForceCloseBorrowWithToken + // emit_stack(TokenForceCloseBorrowWithToken Ok(()) } diff --git a/programs/mango-v4/src/instructions/token_liq_bankruptcy.rs b/programs/mango-v4/src/instructions/token_liq_bankruptcy.rs index 37a0ff3a6..8cc5e8638 100644 --- a/programs/mango-v4/src/instructions/token_liq_bankruptcy.rs +++ b/programs/mango-v4/src/instructions/token_liq_bankruptcy.rs @@ -9,7 +9,8 @@ use crate::state::*; use crate::accounts_ix::*; use crate::logs::{ - LoanOriginationFeeInstruction, TokenBalanceLog, TokenLiqBankruptcyLog, WithdrawLoanLog, + emit_stack, LoanOriginationFeeInstruction, TokenBalanceLog, TokenLiqBankruptcyLog, + WithdrawLoanLog, }; pub fn token_liq_bankruptcy( @@ -42,9 +43,10 @@ pub fn token_liq_bankruptcy( ); let mut account_retriever = ScanningAccountRetriever::new(health_ais, group_pk)?; + let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap(); let mut liqee = ctx.accounts.liqee.load_full_mut()?; - let mut liqee_health_cache = new_health_cache(&liqee.borrow(), &account_retriever) + let mut liqee_health_cache = new_health_cache(&liqee.borrow(), &account_retriever, now_ts) .context("create liqee health cache")?; liqee_health_cache.require_after_phase2_liquidation()?; liqee.fixed.set_being_liquidated(true); @@ -105,8 +107,6 @@ pub fn token_liq_bankruptcy( // liquidators to exploit the insurance fund for 1 native token each call. let liab_transfer = insurance_transfer_i80f48 / liab_to_quote_with_fee; - 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 @@ -138,7 +138,7 @@ pub fn token_liq_bankruptcy( quote_bank.deposit(liqor_quote, insurance_transfer_i80f48, now_ts)?; // liqor quote - emit!(TokenBalanceLog { + emit_stack(TokenBalanceLog { mango_group: ctx.accounts.group.key(), mango_account: ctx.accounts.liqor.key(), token_index: INSURANCE_TOKEN_INDEX, @@ -154,7 +154,7 @@ pub fn token_liq_bankruptcy( liab_bank.withdraw_with_fee(liqor_liab, liab_transfer, now_ts)?; // liqor liab - emit!(TokenBalanceLog { + emit_stack(TokenBalanceLog { mango_group: ctx.accounts.group.key(), mango_account: ctx.accounts.liqor.key(), token_index: liab_token_index, @@ -165,8 +165,12 @@ pub fn token_liq_bankruptcy( // Check liqor's health if !liqor.fixed.is_in_health_region() { - let liqor_health = - compute_health(&liqor.borrow(), HealthType::Init, &account_retriever)?; + let liqor_health = compute_health( + &liqor.borrow(), + HealthType::Init, + &account_retriever, + now_ts, + )?; require!(liqor_health >= 0, MangoError::HealthMustBePositive); } @@ -174,14 +178,14 @@ pub fn token_liq_bankruptcy( .loan_origination_fee .is_positive() { - emit!(WithdrawLoanLog { + emit_stack(WithdrawLoanLog { mango_group: ctx.accounts.group.key(), mango_account: ctx.accounts.liqor.key(), token_index: liab_token_index, loan_amount: liqor_liab_withdraw_result.loan_amount.to_bits(), loan_origination_fee: liqor_liab_withdraw_result.loan_origination_fee.to_bits(), instruction: LoanOriginationFeeInstruction::LiqTokenBankruptcy, - price: Some(liab_oracle_price.to_bits()) + price: Some(liab_oracle_price.to_bits()), }); } @@ -253,7 +257,7 @@ pub fn token_liq_bankruptcy( } // liqee liab - emit!(TokenBalanceLog { + emit_stack(TokenBalanceLog { mango_group: ctx.accounts.group.key(), mango_account: ctx.accounts.liqee.key(), token_index: liab_token_index, @@ -276,7 +280,7 @@ pub fn token_liq_bankruptcy( liqee.deactivate_token_position_and_log(liqee_raw_token_index, ctx.accounts.liqee.key()); } - emit!(TokenLiqBankruptcyLog { + emit_stack(TokenLiqBankruptcyLog { mango_group: ctx.accounts.group.key(), liqee: ctx.accounts.liqee.key(), liqor: ctx.accounts.liqor.key(), @@ -287,7 +291,7 @@ pub fn token_liq_bankruptcy( insurance_transfer: insurance_transfer_i80f48.to_bits(), socialized_loss: socialized_loss.to_bits(), starting_liab_deposit_index: starting_deposit_index.to_bits(), - ending_liab_deposit_index: liab_deposit_index.to_bits() + ending_liab_deposit_index: liab_deposit_index.to_bits(), }); Ok(()) 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 5f4059e8c..551bc8611 100644 --- a/programs/mango-v4/src/instructions/token_liq_with_token.rs +++ b/programs/mango-v4/src/instructions/token_liq_with_token.rs @@ -5,7 +5,8 @@ use crate::accounts_ix::*; use crate::error::*; use crate::health::*; use crate::logs::{ - LoanOriginationFeeInstruction, TokenBalanceLog, TokenLiqWithTokenLog, WithdrawLoanLog, + emit_stack, LoanOriginationFeeInstruction, TokenBalanceLog, TokenLiqWithTokenLog, + WithdrawLoanLog, }; use crate::state::*; @@ -20,6 +21,7 @@ pub fn token_liq_with_token( require!(asset_token_index != liab_token_index, MangoError::SomeError); let mut account_retriever = ScanningAccountRetriever::new(ctx.remaining_accounts, group_pk) .context("create account retriever")?; + let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap(); require_keys_neq!(ctx.accounts.liqor.key(), ctx.accounts.liqee.key()); let mut liqor = ctx.accounts.liqor.load_full_mut()?; @@ -39,7 +41,7 @@ pub fn token_liq_with_token( let mut liqee = ctx.accounts.liqee.load_full_mut()?; // Initial liqee health check - let mut liqee_health_cache = new_health_cache(&liqee.borrow(), &account_retriever) + let mut liqee_health_cache = new_health_cache(&liqee.borrow(), &account_retriever, now_ts) .context("create liqee health cache")?; let liqee_liq_end_health = liqee_health_cache.health(HealthType::LiquidationEnd); liqee_health_cache.require_after_phase1_liquidation()?; @@ -52,7 +54,6 @@ pub fn token_liq_with_token( // Transfer some liab_token from liqor to liqee and // transfer some asset_token from liqee to liqor. // - let now_ts = Clock::get()?.unix_timestamp.try_into().unwrap(); liquidation_action( &mut account_retriever, liab_token_index, @@ -69,8 +70,13 @@ pub fn token_liq_with_token( // Check liqor's health if !liqor.fixed.is_in_health_region() { - let liqor_health = compute_health(&liqor.borrow(), HealthType::Init, &account_retriever) - .context("compute liqor health")?; + let liqor_health = compute_health( + &liqor.borrow(), + HealthType::Init, + &account_retriever, + now_ts, + ) + .context("compute liqor health")?; require!(liqor_health >= 0, MangoError::HealthMustBePositive); } @@ -252,7 +258,7 @@ pub(crate) fn liquidation_action( ); // liqee asset - emit!(TokenBalanceLog { + emit_stack(TokenBalanceLog { mango_group: liqee.fixed.group, mango_account: liqee_key, token_index: asset_token_index, @@ -261,7 +267,7 @@ pub(crate) fn liquidation_action( borrow_index: asset_bank.borrow_index.to_bits(), }); // liqee liab - emit!(TokenBalanceLog { + emit_stack(TokenBalanceLog { mango_group: liqee.fixed.group, mango_account: liqee_key, token_index: liab_token_index, @@ -270,7 +276,7 @@ pub(crate) fn liquidation_action( borrow_index: liab_bank.borrow_index.to_bits(), }); // liqor asset - emit!(TokenBalanceLog { + emit_stack(TokenBalanceLog { mango_group: liqee.fixed.group, mango_account: liqor_key, token_index: asset_token_index, @@ -279,7 +285,7 @@ pub(crate) fn liquidation_action( borrow_index: asset_bank.borrow_index.to_bits(), }); // liqor liab - emit!(TokenBalanceLog { + emit_stack(TokenBalanceLog { mango_group: liqee.fixed.group, mango_account: liqor_key, token_index: liab_token_index, @@ -292,14 +298,14 @@ pub(crate) fn liquidation_action( .loan_origination_fee .is_positive() { - emit!(WithdrawLoanLog { + emit_stack(WithdrawLoanLog { mango_group: liqee.fixed.group, mango_account: liqor_key, token_index: liab_token_index, loan_amount: liqor_liab_withdraw_result.loan_amount.to_bits(), loan_origination_fee: liqor_liab_withdraw_result.loan_origination_fee.to_bits(), instruction: LoanOriginationFeeInstruction::LiqTokenWithToken, - price: Some(liab_oracle_price.to_bits()) + price: Some(liab_oracle_price.to_bits()), }); } @@ -323,7 +329,7 @@ pub(crate) fn liquidation_action( .fixed .maybe_recover_from_being_liquidated(liqee_liq_end_health); - emit!(TokenLiqWithTokenLog { + emit_stack(TokenLiqWithTokenLog { mango_group: liqee.fixed.group, liqee: liqee_key, liqor: liqor_key, @@ -334,7 +340,7 @@ pub(crate) fn liquidation_action( asset_price: asset_oracle_price.to_bits(), liab_price: liab_oracle_price.to_bits(), bankruptcy: !liqee_health_cache.has_phase2_liquidatable() - & liqee_liq_end_health.is_negative() + & liqee_liq_end_health.is_negative(), }); Ok(()) @@ -446,7 +452,7 @@ mod tests { let retriever = ScanningAccountRetriever::new_with_staleness(&ais, &setup.group, None).unwrap(); - health::new_health_cache(&setup.liqee.borrow(), &retriever).unwrap() + health::new_health_cache(&setup.liqee.borrow(), &retriever, 0).unwrap() } fn run(&self, max_liab_transfer: I80F48) -> Result { @@ -468,7 +474,7 @@ mod tests { ScanningAccountRetriever::new_with_staleness(&ais, &setup.group, None).unwrap(); let mut liqee_health_cache = - health::new_health_cache(&setup.liqee.borrow(), &retriever).unwrap(); + health::new_health_cache(&setup.liqee.borrow(), &retriever, 0).unwrap(); let liqee_liq_end_health = liqee_health_cache.health(HealthType::LiquidationEnd); liquidation_action( diff --git a/programs/mango-v4/src/instructions/token_register.rs b/programs/mango-v4/src/instructions/token_register.rs index 88d1a68ff..7b4110363 100644 --- a/programs/mango-v4/src/instructions/token_register.rs +++ b/programs/mango-v4/src/instructions/token_register.rs @@ -6,7 +6,7 @@ use crate::error::*; use crate::state::*; use crate::util::fill_from_str; -use crate::logs::TokenMetaDataLog; +use crate::logs::{emit_stack, TokenMetaDataLog}; pub const INDEX_START: I80F48 = I80F48::from_bits(1_000_000 * I80F48::ONE.to_bits()); @@ -38,6 +38,10 @@ pub fn token_register( token_conditional_swap_taker_fee_rate: f32, token_conditional_swap_maker_fee_rate: f32, flash_loan_swap_fee_rate: f32, + interest_curve_scaling: f32, + interest_target_utilization: f32, + group_insurance_fund: bool, + deposit_limit: u64, ) -> Result<()> { // Require token 0 to be in the insurance token if token_index == INSURANCE_TOKEN_INDEX { @@ -107,11 +111,18 @@ pub fn token_register( fees_withdrawn: 0, token_conditional_swap_taker_fee_rate, token_conditional_swap_maker_fee_rate, - flash_loan_swap_fee_rate, - interest_target_utilization: 0.0, // unused in v0.20.0 - interest_curve_scaling: 0.0, // unused in v0.20.0 - deposits_in_serum: 0, - reserved: [0; 2072], + flash_loan_swap_fee_rate: flash_loan_swap_fee_rate, + interest_target_utilization, + interest_curve_scaling: interest_curve_scaling.into(), + potential_serum_tokens: 0, + maint_weight_shift_start: 0, + maint_weight_shift_end: 0, + maint_weight_shift_duration_inv: I80F48::ZERO, + maint_weight_shift_asset_target: I80F48::ZERO, + maint_weight_shift_liab_target: I80F48::ZERO, + fallback_oracle: Pubkey::default(), // unused, introduced in v0.22 + deposit_limit, + reserved: [0; 1968], }; if let Ok(oracle_price) = @@ -129,7 +140,7 @@ pub fn token_register( *mint_info = MintInfo { group: ctx.accounts.group.key(), token_index, - group_insurance_fund: 1, + group_insurance_fund: if group_insurance_fund { 1 } else { 0 }, padding1: Default::default(), mint: ctx.accounts.mint.key(), banks: Default::default(), @@ -142,7 +153,7 @@ pub fn token_register( mint_info.banks[0] = ctx.accounts.bank.key(); mint_info.vaults[0] = ctx.accounts.vault.key(); - emit!(TokenMetaDataLog { + emit_stack(TokenMetaDataLog { mango_group: ctx.accounts.group.key(), mint: ctx.accounts.mint.key(), token_index, diff --git a/programs/mango-v4/src/instructions/token_register_trustless.rs b/programs/mango-v4/src/instructions/token_register_trustless.rs index 99ba833b9..fe9fe6e58 100644 --- a/programs/mango-v4/src/instructions/token_register_trustless.rs +++ b/programs/mango-v4/src/instructions/token_register_trustless.rs @@ -7,7 +7,7 @@ use crate::instructions::INDEX_START; use crate::state::*; use crate::util::fill_from_str; -use crate::logs::TokenMetaDataLog; +use crate::logs::{emit_stack, TokenMetaDataLog}; use crate::accounts_ix::*; @@ -45,8 +45,8 @@ pub fn token_register_trustless( vault: ctx.accounts.vault.key(), oracle: ctx.accounts.oracle.key(), oracle_config: OracleConfig { - conf_filter: I80F48::from_num(0.10), - max_staleness_slots: 10000, + conf_filter: I80F48::from_num(1000.0), // effectively disabled + max_staleness_slots: -1, reserved: [0; 72], }, stable_price_model: StablePriceModel::default(), @@ -67,11 +67,11 @@ pub fn token_register_trustless( collected_fees_native: I80F48::ZERO, loan_origination_fee_rate: I80F48::from_num(0.0020), loan_fee_rate: I80F48::from_num(0.005), - maint_asset_weight: I80F48::from_num(0.75), // 4x leverage - init_asset_weight: I80F48::from_num(0.5), // 2x leverage - maint_liab_weight: I80F48::from_num(1.25), // 4x leverage - init_liab_weight: I80F48::from_num(1.5), // 2x leverage - liquidation_fee: I80F48::from_num(0.125), + maint_asset_weight: I80F48::from_num(0), + init_asset_weight: I80F48::from_num(0), + maint_liab_weight: I80F48::from_num(1.4), // 2.5x + init_liab_weight: I80F48::from_num(1.8), // 1.25x + liquidation_fee: I80F48::from_num(0.2), dust: I80F48::ZERO, flash_loan_token_account_initial: u64::MAX, flash_loan_approved_amount: 0, @@ -87,17 +87,24 @@ pub fn token_register_trustless( net_borrows_in_window: 0, borrow_weight_scale_start_quote: 5_000_000_000.0, // $5k deposit_weight_scale_start_quote: 5_000_000_000.0, // $5k - reduce_only: 0, // allow both deposits and borrows + reduce_only: 2, // deposit-only force_close: 0, padding: Default::default(), fees_withdrawn: 0, - token_conditional_swap_taker_fee_rate: 0.0005, - token_conditional_swap_maker_fee_rate: 0.0005, + token_conditional_swap_taker_fee_rate: 0.0, + token_conditional_swap_maker_fee_rate: 0.0, flash_loan_swap_fee_rate: 0.0, - interest_target_utilization: 0.0, // unused in v0.20.0 - interest_curve_scaling: 0.0, // unused in v0.20.0 - deposits_in_serum: 0, - reserved: [0; 2072], + interest_target_utilization: 0.5, + interest_curve_scaling: 4.0, + potential_serum_tokens: 0, + maint_weight_shift_start: 0, + maint_weight_shift_end: 0, + maint_weight_shift_duration_inv: I80F48::ZERO, + maint_weight_shift_asset_target: I80F48::ZERO, + maint_weight_shift_liab_target: I80F48::ZERO, + fallback_oracle: Pubkey::default(), // unused, introduced in v0.22 + deposit_limit: 0, + reserved: [0; 1968], }; if let Ok(oracle_price) = @@ -128,7 +135,7 @@ pub fn token_register_trustless( mint_info.banks[0] = ctx.accounts.bank.key(); mint_info.vaults[0] = ctx.accounts.vault.key(); - emit!(TokenMetaDataLog { + emit_stack(TokenMetaDataLog { mango_group: ctx.accounts.group.key(), mint: ctx.accounts.mint.key(), token_index, 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 15cb1ec57..f42fde41b 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 @@ -2,7 +2,7 @@ use anchor_lang::prelude::*; use crate::accounts_ix::*; use crate::error::MangoError; -use crate::logs::{UpdateIndexLog, UpdateRateLog}; +use crate::logs::{emit_stack, UpdateIndexLog, UpdateRateLogV2}; use crate::state::HOUR; use crate::{ accounts_zerocopy::{AccountInfoRef, LoadMutZeroCopyRef, LoadZeroCopyRef}, @@ -99,7 +99,12 @@ pub fn token_update_index_and_rate(ctx: Context) -> Res .update(now_ts as u64, price.to_num()); let stable_price_model = some_bank.stable_price_model; - emit!(UpdateIndexLog { + // If a maint weight shift is done, copy the target into the normal values + // and clear the transition parameters. + let maint_shift_done = some_bank.maint_weight_shift_duration_inv.is_positive() + && now_ts >= some_bank.maint_weight_shift_end; + + emit_stack(UpdateIndexLog { mango_group: mint_info.group.key(), token_index: mint_info.token_index, deposit_index: deposit_index.to_bits(), @@ -135,37 +140,69 @@ pub fn token_update_index_and_rate(ctx: Context) -> Res bank.avg_utilization = new_avg_utilization; bank.stable_price_model = stable_price_model; + + if maint_shift_done { + bank.maint_asset_weight = bank.maint_weight_shift_asset_target; + bank.maint_liab_weight = bank.maint_weight_shift_liab_target; + bank.maint_weight_shift_duration_inv = I80F48::ZERO; + bank.maint_weight_shift_asset_target = I80F48::ZERO; + bank.maint_weight_shift_liab_target = I80F48::ZERO; + bank.maint_weight_shift_start = 0; + bank.maint_weight_shift_end = 0; + } } } // compute optimal rates, and max rate and set them on the bank { - let some_bank = ctx.remaining_accounts[0].load::()?; + let mut some_bank = ctx.remaining_accounts[0].load_mut::()?; let diff_ts = I80F48::from_num(now_ts - some_bank.bank_rate_last_updated); // update each hour if diff_ts > HOUR { - let (rate0, rate1, max_rate) = some_bank.compute_rates(); + // First setup when new parameters are introduced + if some_bank.interest_curve_scaling == 0.0 { + let old_max_rate = 0.5; + some_bank.interest_curve_scaling = + some_bank.max_rate.to_num::() / old_max_rate; + some_bank.interest_target_utilization = some_bank.util0.to_num(); - emit!(UpdateRateLog { + let descale_factor = I80F48::from_num(1.0 / some_bank.interest_curve_scaling); + some_bank.rate0 *= descale_factor; + some_bank.rate1 *= descale_factor; + some_bank.max_rate *= descale_factor; + } + + some_bank.update_interest_rate_scaling(); + + let rate0 = some_bank.rate0; + let rate1 = some_bank.rate1; + let max_rate = some_bank.max_rate; + let scaling = some_bank.interest_curve_scaling; + let target_util = some_bank.interest_target_utilization; + + emit_stack(UpdateRateLogV2 { mango_group: mint_info.group.key(), token_index: mint_info.token_index, rate0: rate0.to_bits(), + util0: some_bank.util0.to_bits(), rate1: rate1.to_bits(), + util1: some_bank.util1.to_bits(), max_rate: max_rate.to_bits(), + curve_scaling: some_bank.interest_curve_scaling, + target_utilization: some_bank.interest_target_utilization, }); drop(some_bank); - msg!("rate0 {}", rate0); - msg!("rate1 {}", rate1); - msg!("max_rate {}", max_rate); - + // Apply the new parameters to all banks for ai in ctx.remaining_accounts.iter() { let mut bank = ai.load_mut::()?; bank.bank_rate_last_updated = now_ts; + bank.interest_curve_scaling = scaling; + bank.interest_target_utilization = target_util; bank.rate0 = rate0; bank.rate1 = rate1; bank.max_rate = max_rate; diff --git a/programs/mango-v4/src/instructions/token_withdraw.rs b/programs/mango-v4/src/instructions/token_withdraw.rs index 196dba333..7ed0a2bc4 100644 --- a/programs/mango-v4/src/instructions/token_withdraw.rs +++ b/programs/mango-v4/src/instructions/token_withdraw.rs @@ -7,13 +7,16 @@ use anchor_spl::token; use fixed::types::I80F48; use crate::accounts_ix::*; -use crate::logs::{LoanOriginationFeeInstruction, TokenBalanceLog, WithdrawLoanLog, WithdrawLog}; +use crate::logs::{ + emit_stack, LoanOriginationFeeInstruction, TokenBalanceLog, WithdrawLoanLog, WithdrawLog, +}; pub fn token_withdraw(ctx: Context, amount: u64, allow_borrow: bool) -> Result<()> { require_msg!(amount > 0, "withdraw amount must be positive"); let group = ctx.accounts.group.load()?; let token_index = ctx.accounts.bank.load()?.token_index; + let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap(); // Create the account's position for that token index let mut account = ctx.accounts.account.load_full_mut()?; @@ -23,8 +26,8 @@ pub fn token_withdraw(ctx: Context, amount: u64, allow_borrow: bo let pre_health_opt = if !account.fixed.is_in_health_region() { let retriever = new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?; - let hc_result = - new_health_cache(&account.borrow(), &retriever).context("pre-withdraw health cache"); + let hc_result = new_health_cache(&account.borrow(), &retriever, now_ts) + .context("pre-withdraw health cache"); if hc_result.is_oracle_error() { // We allow NOT checking the pre init health. That means later on the health // check will be stricter (post_init > 0, without the post_init >= pre_init option) @@ -102,7 +105,7 @@ pub fn token_withdraw(ctx: Context, amount: u64, allow_borrow: bo let native_position_after = position.native(&bank); - emit!(TokenBalanceLog { + emit_stack(TokenBalanceLog { mango_group: ctx.accounts.group.key(), mango_account: ctx.accounts.account.key(), token_index, @@ -132,8 +135,9 @@ pub fn token_withdraw(ctx: Context, amount: u64, allow_borrow: bo // Note that this must include the normal pre and post health checks. let retriever = new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?; - let health_cache = new_health_cache_skipping_bad_oracles(&account.borrow(), &retriever) - .context("special post-withdraw health-cache")?; + let health_cache = + new_health_cache_skipping_bad_oracles(&account.borrow(), &retriever, now_ts) + .context("special post-withdraw health-cache")?; let post_init_health = health_cache.health(HealthType::Init); account.check_health_pre_checks(&health_cache, post_init_health)?; account.check_health_post_checks(I80F48::MAX, post_init_health)?; @@ -149,7 +153,7 @@ pub fn token_withdraw(ctx: Context, amount: u64, allow_borrow: bo account.deactivate_token_position_and_log(raw_token_index, ctx.accounts.account.key()); } - emit!(WithdrawLog { + emit_stack(WithdrawLog { mango_group: ctx.accounts.group.key(), mango_account: ctx.accounts.account.key(), signer: ctx.accounts.owner.key(), @@ -159,7 +163,7 @@ pub fn token_withdraw(ctx: Context, amount: u64, allow_borrow: bo }); if withdraw_result.loan_origination_fee.is_positive() { - emit!(WithdrawLoanLog { + emit_stack(WithdrawLoanLog { mango_group: ctx.accounts.group.key(), mango_account: ctx.accounts.account.key(), token_index, diff --git a/programs/mango-v4/src/lib.rs b/programs/mango-v4/src/lib.rs index df2716181..4bbe558dc 100644 --- a/programs/mango-v4/src/lib.rs +++ b/programs/mango-v4/src/lib.rs @@ -150,6 +150,10 @@ pub mod mango_v4 { token_conditional_swap_taker_fee_rate: f32, token_conditional_swap_maker_fee_rate: f32, flash_loan_swap_fee_rate: f32, + interest_curve_scaling: f32, + interest_target_utilization: f32, + group_insurance_fund: bool, + deposit_limit: u64, ) -> Result<()> { #[cfg(feature = "enable-gpl")] instructions::token_register( @@ -177,6 +181,10 @@ pub mod mango_v4 { token_conditional_swap_taker_fee_rate, token_conditional_swap_maker_fee_rate, flash_loan_swap_fee_rate, + interest_curve_scaling, + interest_target_utilization, + group_insurance_fund, + deposit_limit, )?; Ok(()) } @@ -221,6 +229,15 @@ pub mod mango_v4 { token_conditional_swap_taker_fee_rate_opt: Option, token_conditional_swap_maker_fee_rate_opt: Option, flash_loan_swap_fee_rate_opt: Option, + interest_curve_scaling_opt: Option, + interest_target_utilization_opt: Option, + maint_weight_shift_start_opt: Option, + maint_weight_shift_end_opt: Option, + maint_weight_shift_asset_target_opt: Option, + maint_weight_shift_liab_target_opt: Option, + maint_weight_shift_abort: bool, + set_fallback_oracle: bool, // unused, introduced in v0.22 + deposit_limit_opt: Option, ) -> Result<()> { #[cfg(feature = "enable-gpl")] instructions::token_edit( @@ -252,6 +269,15 @@ pub mod mango_v4 { token_conditional_swap_taker_fee_rate_opt, token_conditional_swap_maker_fee_rate_opt, flash_loan_swap_fee_rate_opt, + interest_curve_scaling_opt, + interest_target_utilization_opt, + maint_weight_shift_start_opt, + maint_weight_shift_end_opt, + maint_weight_shift_asset_target_opt, + maint_weight_shift_liab_target_opt, + maint_weight_shift_abort, + set_fallback_oracle, + deposit_limit_opt, )?; Ok(()) } @@ -540,9 +566,10 @@ pub mod mango_v4 { ctx: Context, market_index: Serum3MarketIndex, name: String, + oracle_price_band: f32, ) -> Result<()> { #[cfg(feature = "enable-gpl")] - instructions::serum3_register_market(ctx, market_index, name)?; + instructions::serum3_register_market(ctx, market_index, name, oracle_price_band)?; Ok(()) } @@ -551,9 +578,16 @@ pub mod mango_v4 { reduce_only_opt: Option, force_close_opt: Option, name_opt: Option, + oracle_price_band_opt: Option, ) -> Result<()> { #[cfg(feature = "enable-gpl")] - instructions::serum3_edit_market(ctx, reduce_only_opt, force_close_opt, name_opt)?; + instructions::serum3_edit_market( + ctx, + reduce_only_opt, + force_close_opt, + name_opt, + oracle_price_band_opt, + )?; Ok(()) } @@ -603,6 +637,36 @@ pub mod mango_v4 { order_type, client_order_id, limit, + false, + )?; + Ok(()) + } + + /// requires the receiver_bank in the health account list to be writable + #[allow(clippy::too_many_arguments)] + pub fn serum3_place_order_v2( + ctx: Context, + side: Serum3Side, + limit_price: u64, + max_base_qty: u64, + max_native_quote_qty_including_fees: u64, + self_trade_behavior: Serum3SelfTradeBehavior, + order_type: Serum3OrderType, + client_order_id: u64, + limit: u16, + ) -> Result<()> { + #[cfg(feature = "enable-gpl")] + instructions::serum3_place_order( + ctx, + side, + limit_price, + max_base_qty, + max_native_quote_qty_including_fees, + self_trade_behavior, + order_type, + client_order_id, + limit, + true, )?; Ok(()) } diff --git a/programs/mango-v4/src/logs.rs b/programs/mango-v4/src/logs.rs index 51b897248..215da9bba 100644 --- a/programs/mango-v4/src/logs.rs +++ b/programs/mango-v4/src/logs.rs @@ -5,13 +5,29 @@ use crate::{ use anchor_lang::prelude::*; use borsh::BorshSerialize; +#[inline(never)] // ensure fresh stack frame +pub fn emit_stack(e: T) { + use std::io::{Cursor, Write}; + + // stack buffer, stack frames are 4kb + let mut buffer = [0u8; 3000]; + + let mut cursor = Cursor::new(&mut buffer[..]); + cursor.write_all(&T::DISCRIMINATOR).unwrap(); + e.serialize(&mut cursor) + .expect("event must fit into stack buffer"); + + let pos = cursor.position() as usize; + anchor_lang::solana_program::log::sol_log_data(&[&buffer[..pos]]); +} + pub fn emit_perp_balances( mango_group: Pubkey, mango_account: Pubkey, pp: &PerpPosition, pm: &PerpMarket, ) { - emit!(PerpBalanceLog { + emit_stack(PerpBalanceLog { mango_group, mango_account, market_index: pm.perp_market_index, @@ -300,6 +316,20 @@ pub struct UpdateRateLog { pub max_rate: i128, // I80F48 } +#[event] +pub struct UpdateRateLogV2 { + pub mango_group: Pubkey, + pub token_index: u16, + // contrary to v1 these do not have curve_scaling factored in! + pub rate0: i128, // I80F48 + pub util0: i128, // I80F48 + pub rate1: i128, // I80F48 + pub util1: i128, // I80F48 + pub max_rate: i128, // I80F48 + pub curve_scaling: f64, + pub target_utilization: f32, +} + #[event] pub struct TokenLiqWithTokenLog { pub mango_group: Pubkey, diff --git a/programs/mango-v4/src/state/bank.rs b/programs/mango-v4/src/state/bank.rs index b3dbc6c56..7a397224f 100644 --- a/programs/mango-v4/src/state/bank.rs +++ b/programs/mango-v4/src/state/bank.rs @@ -18,7 +18,6 @@ pub const DAY: i64 = 86400; pub const DAY_I80F48: I80F48 = I80F48::from_bits(86_400 * I80F48::ONE.to_bits()); pub const ONE_BPS: I80F48 = I80F48::from_bits(28147497671); pub const YEAR_I80F48: I80F48 = I80F48::from_bits(31_536_000 * I80F48::ONE.to_bits()); -pub const MINIMUM_MAX_RATE: I80F48 = I80F48::from_bits(I80F48::ONE.to_bits() / 2); #[derive(Derivative)] #[derivative(Debug)] @@ -151,19 +150,29 @@ pub struct Bank { /// Target utilization: If actual utilization is higher, scale up interest. /// If it's lower, scale down interest (if possible) - pub interest_target_utilization: f32, // unused in v0.20.0 + pub interest_target_utilization: f32, /// Current interest curve scaling, always >= 1.0 /// /// Except when first migrating to having this field, then 0.0 - pub interest_curve_scaling: f64, // unused in v0.20.0 + pub interest_curve_scaling: f64, - // user deposits that were moved into serum open orders - // can be negative due to multibank, then it'd need to be balanced in the keeper - pub deposits_in_serum: i64, + /// Largest amount of tokens that might be added the the bank based on + /// serum open order execution. + pub potential_serum_tokens: u64, - #[derivative(Debug = "ignore")] - pub reserved: [u8; 2072], + pub maint_weight_shift_start: u64, + pub maint_weight_shift_end: u64, + pub maint_weight_shift_duration_inv: I80F48, + pub maint_weight_shift_asset_target: I80F48, + pub maint_weight_shift_liab_target: I80F48, + + pub fallback_oracle: Pubkey, // unused, introduced in v0.22 + + /// zero means none, in token native + pub deposit_limit: u64, + + pub reserved: [u8; 1968], } const_assert_eq!( size_of::(), @@ -196,7 +205,11 @@ const_assert_eq!( + 8 + 4 * 4 + 8 * 2 - + 2072 + + 8 * 2 + + 16 * 3 + + 32 + + 8 + + 1968 ); const_assert_eq!(size_of::(), 3064); const_assert_eq!(size_of::() % 8, 0); @@ -213,6 +226,19 @@ impl WithdrawResult { } } +pub struct TransferResult { + pub source_is_active: bool, + pub target_is_active: bool, + pub loan_origination_fee: I80F48, + pub loan_amount: I80F48, +} + +impl TransferResult { + pub fn has_loan(&self) -> bool { + self.loan_amount.is_positive() + } +} + impl Bank { pub fn from_existing_bank( existing_bank: &Bank, @@ -231,7 +257,7 @@ impl Bank { flash_loan_approved_amount: 0, flash_loan_token_account_initial: u64::MAX, net_borrows_in_window: 0, - deposits_in_serum: 0, + potential_serum_tokens: 0, bump, bank_num, @@ -263,15 +289,15 @@ impl Bank { token_index: existing_bank.token_index, mint_decimals: existing_bank.mint_decimals, oracle_config: existing_bank.oracle_config, - stable_price_model: StablePriceModel::default(), + stable_price_model: existing_bank.stable_price_model, min_vault_to_deposits_ratio: existing_bank.min_vault_to_deposits_ratio, net_borrow_limit_per_window_quote: existing_bank.net_borrow_limit_per_window_quote, net_borrow_limit_window_size_ts: existing_bank.net_borrow_limit_window_size_ts, last_net_borrows_window_start_ts: existing_bank.last_net_borrows_window_start_ts, - borrow_weight_scale_start_quote: f64::MAX, - deposit_weight_scale_start_quote: f64::MAX, - reduce_only: 0, - force_close: 0, + borrow_weight_scale_start_quote: existing_bank.borrow_weight_scale_start_quote, + deposit_weight_scale_start_quote: existing_bank.deposit_weight_scale_start_quote, + reduce_only: existing_bank.reduce_only, + force_close: existing_bank.force_close, padding: [0; 6], token_conditional_swap_taker_fee_rate: existing_bank .token_conditional_swap_taker_fee_rate, @@ -280,7 +306,14 @@ impl Bank { flash_loan_swap_fee_rate: existing_bank.flash_loan_swap_fee_rate, interest_target_utilization: existing_bank.interest_target_utilization, interest_curve_scaling: existing_bank.interest_curve_scaling, - reserved: [0; 2072], + maint_weight_shift_start: existing_bank.maint_weight_shift_start, + maint_weight_shift_end: existing_bank.maint_weight_shift_end, + maint_weight_shift_duration_inv: existing_bank.maint_weight_shift_duration_inv, + maint_weight_shift_asset_target: existing_bank.maint_weight_shift_asset_target, + maint_weight_shift_liab_target: existing_bank.maint_weight_shift_liab_target, + fallback_oracle: existing_bank.oracle, + deposit_limit: existing_bank.deposit_limit, + reserved: [0; 1968], } } @@ -290,7 +323,7 @@ impl Bank { require_gte!(self.rate0, I80F48::ZERO); require_gte!(self.util1, I80F48::ZERO); require_gte!(self.rate1, I80F48::ZERO); - require_gte!(self.max_rate, MINIMUM_MAX_RATE); + require_gte!(self.max_rate, I80F48::ZERO); require_gte!(self.loan_fee_rate, 0.0); require_gte!(self.loan_origination_fee_rate, 0.0); require_gte!(self.maint_asset_weight, 0.0); @@ -306,6 +339,11 @@ impl Bank { require_gte!(self.token_conditional_swap_taker_fee_rate, 0.0); require_gte!(self.token_conditional_swap_maker_fee_rate, 0.0); require_gte!(self.flash_loan_swap_fee_rate, 0.0); + require_gte!(self.interest_curve_scaling, 1.0); + require_gte!(self.interest_target_utilization, 0.0); + require_gte!(self.maint_weight_shift_duration_inv, 0.0); + require_gte!(self.maint_weight_shift_asset_target, 0.0); + require_gte!(self.maint_weight_shift_liab_target, 0.0); Ok(()) } @@ -337,6 +375,26 @@ impl Bank { self.deposit_index * self.indexed_deposits } + pub fn maint_weights(&self, now_ts: u64) -> (I80F48, I80F48) { + if self.maint_weight_shift_duration_inv.is_zero() || now_ts <= self.maint_weight_shift_start + { + (self.maint_asset_weight, self.maint_liab_weight) + } else if now_ts >= self.maint_weight_shift_end { + ( + self.maint_weight_shift_asset_target, + self.maint_weight_shift_liab_target, + ) + } else { + let scale = I80F48::from(now_ts - self.maint_weight_shift_start) + * self.maint_weight_shift_duration_inv; + let asset = self.maint_asset_weight + + scale * (self.maint_weight_shift_asset_target - self.maint_asset_weight); + let liab = self.maint_liab_weight + + scale * (self.maint_weight_shift_liab_target - self.maint_liab_weight); + (asset, liab) + } + } + /// Prevent borrowing away the full bank vault. /// Keep some in reserve to satisfy non-borrow withdraws. pub fn enforce_min_vault_to_deposits_ratio(&self, vault_ai: &AccountInfo) -> Result<()> { @@ -557,7 +615,7 @@ impl Bank { require_gte!(native_amount, 0); let native_position = position.native(self); - if native_position.is_positive() { + if !native_position.is_negative() { let new_native_position = native_position - native_amount; if !new_native_position.is_negative() { // withdraw deposits only @@ -687,6 +745,64 @@ impl Bank { } } + /// Generic "transfer" from source to target. + /// + /// Amounts for source and target can differ and can be zero. + /// Checks reduce-only, net borrow limits and deposit limits. + pub fn checked_transfer_with_fee( + &mut self, + source: &mut TokenPosition, + source_amount: I80F48, + target: &mut TokenPosition, + target_amount: I80F48, + now_ts: u64, + oracle_price: I80F48, + ) -> Result { + let before_borrows = self.indexed_borrows; + let before_deposits = self.indexed_deposits; + + let withdraw_result = if !source_amount.is_zero() { + let withdraw_result = self.withdraw_with_fee(source, source_amount, now_ts)?; + require!( + source.indexed_position >= 0 || !self.are_borrows_reduce_only(), + MangoError::TokenInReduceOnlyMode + ); + withdraw_result + } else { + WithdrawResult { + position_is_active: true, + loan_amount: I80F48::ZERO, + loan_origination_fee: I80F48::ZERO, + } + }; + + let target_is_active = if !target_amount.is_zero() { + let active = self.deposit(target, target_amount, now_ts)?; + require!( + target.indexed_position <= 0 || !self.are_deposits_reduce_only(), + MangoError::TokenInReduceOnlyMode + ); + active + } else { + true + }; + + // Adding DELTA here covers the case where we add slightly more than we withdraw + if self.indexed_borrows > before_borrows + I80F48::DELTA { + self.check_net_borrows(oracle_price)?; + } + if self.indexed_deposits > before_deposits + I80F48::DELTA { + self.check_deposit_and_oo_limit()?; + } + + Ok(TransferResult { + source_is_active: withdraw_result.position_is_active, + target_is_active, + loan_origination_fee: withdraw_result.loan_origination_fee, + loan_amount: withdraw_result.loan_amount, + }) + } + /// 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. @@ -732,6 +848,49 @@ impl Bank { Ok(()) } + pub fn remaining_deposits_until_limit(&self) -> I80F48 { + if self.deposit_limit == 0 { + return I80F48::MAX; + } + + // Assuming slightly higher deposits than true allows the returned value + // to be deposit()ed safely into this bank without triggering limits. + // (because deposit() will round up in favor of the user) + let deposits = self.deposit_index * (self.indexed_deposits + I80F48::DELTA); + + let serum = I80F48::from(self.potential_serum_tokens); + let total = deposits + serum; + + I80F48::from(self.deposit_limit) - total + } + + pub fn check_deposit_and_oo_limit(&self) -> Result<()> { + if self.deposit_limit == 0 { + return Ok(()); + } + + // Intentionally does not use remaining_deposits_until_limit(): That function + // returns slightly less than the true limit to make sure depositing that amount + // will not cause a limit overrun. + let deposits = self.native_deposits(); + let serum = I80F48::from(self.potential_serum_tokens); + let total = deposits + serum; + let remaining = I80F48::from(self.deposit_limit) - total; + if remaining < 0 { + return Err(error_msg_typed!( + MangoError::BankDepositLimit, + "deposit limit exceeded: remaining: {}, total: {}, limit: {}, deposits: {}, serum: {}", + remaining, + total, + self.deposit_limit, + deposits, + serum, + )); + } + + Ok(()) + } + pub fn update_cumulative_interest( &self, position: &mut TokenPosition, @@ -815,6 +974,7 @@ impl Bank { self.util1, self.rate1, self.max_rate, + self.interest_curve_scaling, ) } @@ -828,8 +988,9 @@ impl Bank { util1: I80F48, rate1: I80F48, max_rate: I80F48, + scaling: f64, ) -> I80F48 { - if utilization <= util0 { + let v = if utilization <= util0 { let slope = rate0 / util0; slope * utilization } else if utilization <= util1 { @@ -840,6 +1001,13 @@ impl Bank { let extra_util = utilization - util1; let slope = (max_rate - rate1) / (I80F48::ONE - util1); rate1 + slope * extra_util + }; + + // scaling will be 0 when it's introduced + if scaling == 0.0 { + v + } else { + v * I80F48::from_num(scaling) } } @@ -875,34 +1043,23 @@ impl Bank { } // computes new optimal rates and max rate - pub fn compute_rates(&self) -> (I80F48, I80F48, I80F48) { - // interest rate legs 2 and 3 are seen as punitive legs, encouraging utilization to move towards optimal utilization - // lets choose util0 as optimal utilization and 0 to utli0 as the leg where we want the utlization to preferably be - let optimal_util = self.util0; + pub fn update_interest_rate_scaling(&mut self) { + // Interest increases above target_util, decreases below + let target_util = self.interest_target_utilization as f64; + // use avg_utilization and not instantaneous_utilization so that rates cannot be manipulated easily - let avg_util = self.avg_utilization; + let avg_util = self.avg_utilization.to_num::(); + // move rates up when utilization is above optimal utilization, and vice versa // util factor is between -1 (avg util = 0) and +1 (avg util = 100%) - let util_factor = if avg_util > optimal_util { - (avg_util - optimal_util) / (I80F48::ONE - optimal_util) + let util_factor = if avg_util > target_util { + (avg_util - target_util) / (1.0 - target_util) } else { - (avg_util - optimal_util) / optimal_util + (avg_util - target_util) / target_util }; - let adjustment = I80F48::ONE + self.adjustment_factor * util_factor; + let adjustment = 1.0 + self.adjustment_factor.to_num::() * util_factor; - // 1. irrespective of which leg current utilization is in, update all rates - // 2. only update rates as long as new adjusted rates are above MINIMUM_MAX_RATE, - // since we don't want to fall to such low rates that it would take a long time to - // recover to high rates if utilization suddently increases to a high value - if (self.max_rate * adjustment) > MINIMUM_MAX_RATE { - ( - (self.rate0 * adjustment), - (self.rate1 * adjustment), - (self.max_rate * adjustment), - ) - } else { - (self.rate0, self.rate1, self.max_rate) - } + self.interest_curve_scaling = (self.interest_curve_scaling * adjustment).max(1.0) } pub fn oracle_price( @@ -934,7 +1091,8 @@ impl Bank { if self.deposit_weight_scale_start_quote == f64::MAX { return self.init_asset_weight; } - let all_deposits = self.native_deposits().to_num::() + self.deposits_in_serum as f64; + let all_deposits = + self.native_deposits().to_num::() + self.potential_serum_tokens as f64; let deposits_quote = all_deposits * price.to_num::(); if deposits_quote <= self.deposit_weight_scale_start_quote { self.init_asset_weight @@ -963,6 +1121,16 @@ impl Bank { self.init_liab_weight * I80F48::from_num(scale) } } + + /// Grows potential_serum_tokens if new > old, shrinks it otherwise + #[inline(always)] + pub fn update_potential_serum_tokens(&mut self, old: u64, new: u64) { + if new >= old { + self.potential_serum_tokens += new - old; + } else { + self.potential_serum_tokens = self.potential_serum_tokens.saturating_sub(old - new); + } + } } #[macro_export] @@ -987,9 +1155,108 @@ mod tests { use super::*; - #[test] - pub fn change() -> Result<()> { + fn bank_change_runner(start: f64, change: i32, is_in_use: bool, use_withdraw: bool) { + println!( + "testing: in use: {is_in_use}, start: {start}, change: {change}, use_withdraw: {use_withdraw}", + ); + let epsilon = I80F48::from_bits(1); + + // + // SETUP + // + + let mut bank = Bank::zeroed(); + bank.net_borrow_limit_window_size_ts = 1; // dummy + bank.net_borrow_limit_per_window_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); + let indexed = |v: I80F48, b: &Bank| { + if v > 0 { + let i = v / b.deposit_index; + if i * b.deposit_index < v { + i + I80F48::DELTA + } else { + i + } + } else { + v / b.borrow_index + } + }; + + let mut account = TokenPosition { + indexed_position: I80F48::ZERO, + token_index: 0, + in_use_count: u16::from(is_in_use), + cumulative_deposit_interest: 0.0, + cumulative_borrow_interest: 0.0, + previous_index: I80F48::ZERO, + padding: Default::default(), + reserved: [0; 128], + }; + + account.indexed_position = indexed(I80F48::from_num(start), &bank); + if start >= 0.0 { + bank.indexed_deposits = account.indexed_position; + } else { + bank.indexed_borrows = -account.indexed_position; + } + + // get the rounded start value + let start_native = account.native(&bank); + + // + // TEST + // + + let change = I80F48::from(change); + let dummy_now_ts = 1 as u64; + let dummy_price = I80F48::ZERO; + let is_active = if use_withdraw { + bank.withdraw_with_fee(&mut account, change, dummy_now_ts) + .unwrap() + .position_is_active + } else { + bank.change_with_fee(&mut account, change, dummy_now_ts) + .unwrap() + .position_is_active + }; + + let mut expected_native = start_native + change; + let is_deposit_into_nonnegative = start >= 0.0 && change >= 0 && !use_withdraw; + if expected_native >= 0.0 + && expected_native < 1.0 + && !is_in_use + && !is_deposit_into_nonnegative + { + assert!(!is_active); + assert_eq!(bank.dust, expected_native); + expected_native = I80F48::ZERO; + } else { + assert!(is_active); + assert_eq!(bank.dust, I80F48::ZERO); + } + if change < 0 && expected_native < 0 { + let new_borrow = -(expected_native - min(start_native, I80F48::ZERO)); + expected_native -= new_borrow * bank.loan_origination_fee_rate; + } + let expected_indexed = indexed(expected_native, &bank); + + // at most one epsilon error in the resulting indexed value + assert!((account.indexed_position - expected_indexed).abs() <= epsilon); + + if account.indexed_position.is_positive() { + assert_eq!(bank.indexed_deposits, account.indexed_position); + assert_eq!(bank.indexed_borrows, I80F48::ZERO); + } else { + assert_eq!(bank.indexed_deposits, I80F48::ZERO); + assert_eq!(bank.indexed_borrows, -account.indexed_position); + } + } + + #[test] + pub fn bank_change() -> Result<()> { let cases = [ (-10.1, 1), (-10.1, 10), @@ -1013,95 +1280,167 @@ mod tests { (0.0, -1), (-0.1, -1), (-1.1, -10), + (10.0, 0), + (1.0, 0), + (0.1, 0), + (0.0, 0), + (-0.1, 0), ]; for is_in_use in [false, true] { for (start, change) in cases { - println!( - "testing: in use: {}, start: {}, change: {}", - is_in_use, start, change - ); - - // - // SETUP - // - - let mut bank = Bank::zeroed(); - bank.net_borrow_limit_window_size_ts = 1; // dummy - bank.net_borrow_limit_per_window_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); - let indexed = |v: I80F48, b: &Bank| { - if v > 0 { - v / b.deposit_index - } else { - v / b.borrow_index - } - }; - - let mut account = TokenPosition { - indexed_position: I80F48::ZERO, - token_index: 0, - in_use_count: u16::from(is_in_use), - cumulative_deposit_interest: 0.0, - cumulative_borrow_interest: 0.0, - previous_index: I80F48::ZERO, - padding: Default::default(), - reserved: [0; 128], - }; - - account.indexed_position = indexed(I80F48::from_num(start), &bank); - if start >= 0.0 { - bank.indexed_deposits = account.indexed_position; - } else { - bank.indexed_borrows = -account.indexed_position; - } - - // get the rounded start value - let start_native = account.native(&bank); - - // - // TEST - // - - let change = I80F48::from(change); - let dummy_now_ts = 1 as u64; - let dummy_price = I80F48::ZERO; - let is_active = bank - .change_with_fee(&mut account, change, dummy_now_ts)? - .position_is_active; - - let mut expected_native = start_native + change; - if expected_native >= 0.0 && expected_native < 1.0 && !is_in_use { - assert!(!is_active); - assert_eq!(bank.dust, expected_native); - expected_native = I80F48::ZERO; - } else { - assert!(is_active); - assert_eq!(bank.dust, I80F48::ZERO); - } - if change < 0 && expected_native < 0 { - let new_borrow = -(expected_native - min(start_native, I80F48::ZERO)); - expected_native -= new_borrow * bank.loan_origination_fee_rate; - } - let expected_indexed = indexed(expected_native, &bank); - - // at most one epsilon error in the resulting indexed value - assert!((account.indexed_position - expected_indexed).abs() <= epsilon); - - if account.indexed_position.is_positive() { - assert_eq!(bank.indexed_deposits, account.indexed_position); - assert_eq!(bank.indexed_borrows, I80F48::ZERO); - } else { - assert_eq!(bank.indexed_deposits, I80F48::ZERO); - assert_eq!(bank.indexed_borrows, -account.indexed_position); + bank_change_runner(start, change, is_in_use, false); + if change == 0 { + // check withdrawing 0 + bank_change_runner(start, change, is_in_use, true); } } } Ok(()) } + #[test] + fn bank_transfer() { + // + // SETUP + // + + let mut bank_proto = Bank::zeroed(); + bank_proto.net_borrow_limit_window_size_ts = 1; // dummy + bank_proto.net_borrow_limit_per_window_quote = i64::MAX; // max since we don't want this to interfere + bank_proto.deposit_index = I80F48::from(1_234_567); + bank_proto.borrow_index = I80F48::from(1_234_567); + bank_proto.loan_origination_fee_rate = I80F48::from_num(0.1); + + let account_proto = TokenPosition { + indexed_position: I80F48::ZERO, + token_index: 0, + in_use_count: 1, + cumulative_deposit_interest: 0.0, + cumulative_borrow_interest: 0.0, + previous_index: I80F48::ZERO, + padding: Default::default(), + reserved: [0; 128], + }; + + // + // TESTS + // + + // simple transfer + { + let mut bank = bank_proto.clone(); + let mut a1 = account_proto.clone(); + let mut a2 = account_proto.clone(); + + let amount = I80F48::from(100); + bank.deposit(&mut a1, amount, 0).unwrap(); + let damount = a1.native(&bank); + let r = bank + .checked_transfer_with_fee(&mut a1, amount, &mut a2, amount, 0, I80F48::ONE) + .unwrap(); + assert_eq!(a2.native(&bank), damount); + assert!(r.source_is_active); + assert!(r.target_is_active); + } + + // borrow limits + { + let mut bank = bank_proto.clone(); + bank.net_borrow_limit_per_window_quote = 100; + bank.loan_origination_fee_rate = I80F48::ZERO; + let mut a1 = account_proto.clone(); + let mut a2 = account_proto.clone(); + + { + let mut b = bank.clone(); + let amount = I80F48::from(101); + assert!(b + .checked_transfer_with_fee(&mut a1, amount, &mut a2, amount, 0, I80F48::ONE) + .is_err()); + } + + { + let mut b = bank.clone(); + let amount = I80F48::from(100); + b.checked_transfer_with_fee(&mut a1, amount, &mut a2, amount, 0, I80F48::ONE) + .unwrap(); + } + + { + let mut b = bank.clone(); + let amount = b.remaining_net_borrows_quote(I80F48::ONE); + b.checked_transfer_with_fee(&mut a1, amount, &mut a2, amount, 0, I80F48::ONE) + .unwrap(); + } + } + + // deposit limits + { + let mut bank = bank_proto.clone(); + bank.deposit_limit = 100; + let mut a1 = account_proto.clone(); + let mut a2 = account_proto.clone(); + + { + let mut b = bank.clone(); + let amount = I80F48::from(101); + assert!(b + .checked_transfer_with_fee(&mut a1, amount, &mut a2, amount, 0, I80F48::ONE) + .is_err()); + } + + { + // still bad because deposit() adds DELTA more than requested + let mut b = bank.clone(); + let amount = I80F48::from(100); + assert!(b + .checked_transfer_with_fee(&mut a1, amount, &mut a2, amount, 0, I80F48::ONE) + .is_err()); + } + + { + let mut b = bank.clone(); + let amount = I80F48::from_num(99.999); + b.checked_transfer_with_fee(&mut a1, amount, &mut a2, amount, 0, I80F48::ONE) + .unwrap(); + } + + { + let mut b = bank.clone(); + let amount = b.remaining_deposits_until_limit(); + b.checked_transfer_with_fee(&mut a1, amount, &mut a2, amount, 0, I80F48::ONE) + .unwrap(); + } + } + + // reducing transfer while limits exceeded + { + let mut bank = bank_proto.clone(); + bank.loan_origination_fee_rate = I80F48::ZERO; + + let amount = I80F48::from(100); + let mut a1 = account_proto.clone(); + bank.deposit(&mut a1, amount, 0).unwrap(); + let mut a2 = account_proto.clone(); + bank.withdraw_with_fee(&mut a2, amount, 0).unwrap(); + + bank.net_borrow_limit_per_window_quote = 100; + bank.net_borrows_in_window = 200; + bank.deposit_limit = 100; + bank.potential_serum_tokens = 200; + + let half = I80F48::from(50); + bank.checked_transfer_with_fee(&mut a1, half, &mut a2, half, 0, I80F48::ONE) + .unwrap(); + bank.checked_transfer_with_fee(&mut a1, half, &mut a2, half, 0, I80F48::ONE) + .unwrap(); + assert!(bank + .checked_transfer_with_fee(&mut a1, half, &mut a2, half, 0, I80F48::ONE) + .is_err()); + } + } + #[test] fn test_compute_new_avg_utilization() { let mut bank = Bank::zeroed(); @@ -1185,4 +1524,48 @@ mod tests { Ok(()) } + + #[test] + pub fn test_bank_maint_weight_shift() -> Result<()> { + let mut bank = Bank::zeroed(); + bank.maint_asset_weight = I80F48::ONE; + bank.maint_liab_weight = I80F48::ZERO; + bank.maint_weight_shift_start = 100; + bank.maint_weight_shift_end = 1100; + bank.maint_weight_shift_duration_inv = I80F48::ONE / I80F48::from(1000); + bank.maint_weight_shift_asset_target = I80F48::from(2); + bank.maint_weight_shift_liab_target = I80F48::from(10); + + let (a, l) = bank.maint_weights(0); + assert_eq!(a, 1.0); + assert_eq!(l, 0.0); + + let (a, l) = bank.maint_weights(100); + assert_eq!(a, 1.0); + assert_eq!(l, 0.0); + + let (a, l) = bank.maint_weights(1100); + assert_eq!(a, 2.0); + assert_eq!(l, 10.0); + + let (a, l) = bank.maint_weights(2000); + assert_eq!(a, 2.0); + assert_eq!(l, 10.0); + + let abs_diff = |x: I80F48, y: f64| (x.to_num::() - y).abs(); + + let (a, l) = bank.maint_weights(600); + assert!(abs_diff(a, 1.5) < 1e-8); + assert!(abs_diff(l, 5.0) < 1e-8); + + let (a, l) = bank.maint_weights(200); + assert!(abs_diff(a, 1.1) < 1e-8); + assert!(abs_diff(l, 1.0) < 1e-8); + + let (a, l) = bank.maint_weights(1000); + assert!(abs_diff(a, 1.9) < 1e-8); + assert!(abs_diff(l, 9.0) < 1e-8); + + Ok(()) + } } diff --git a/programs/mango-v4/src/state/group.rs b/programs/mango-v4/src/state/group.rs index 47d7c2664..89ca7c36c 100644 --- a/programs/mango-v4/src/state/group.rs +++ b/programs/mango-v4/src/state/group.rs @@ -220,6 +220,7 @@ pub enum IxGate { TokenConditionalSwapStart = 68, TokenConditionalSwapCreatePremiumAuction = 69, TokenConditionalSwapCreateLinearAuction = 70, + Serum3PlaceOrderV2 = 71, // NOTE: Adding new variants requires matching changes in ts and the ix_gate_set instruction. } diff --git a/programs/mango-v4/src/state/mango_account.rs b/programs/mango-v4/src/state/mango_account.rs index 2403a4d1f..40e703795 100644 --- a/programs/mango-v4/src/state/mango_account.rs +++ b/programs/mango-v4/src/state/mango_account.rs @@ -13,7 +13,7 @@ use static_assertions::const_assert_eq; use crate::error::*; use crate::health::{HealthCache, HealthType}; -use crate::logs::{DeactivatePerpPositionLog, DeactivateTokenPositionLog}; +use crate::logs::{emit_stack, DeactivatePerpPositionLog, DeactivateTokenPositionLog}; use crate::util; use super::BookSideOrderTree; @@ -1014,7 +1014,7 @@ impl< let mango_group = self.fixed().group; let token_position = self.token_position_mut_by_raw_index(raw_index); assert!(token_position.in_use_count == 0); - emit!(DeactivateTokenPositionLog { + emit_stack(DeactivateTokenPositionLog { mango_group, mango_account: mango_account_pubkey, token_index: token_position.token_index, @@ -1168,7 +1168,7 @@ impl< let mango_group = self.fixed().group; let perp_position = self.perp_position_mut(perp_market_index)?; - emit!(DeactivatePerpPositionLog { + emit_stack(DeactivatePerpPositionLog { mango_group, mango_account: mango_account_pubkey, market_index: perp_market_index, diff --git a/programs/mango-v4/src/state/mango_account_components.rs b/programs/mango-v4/src/state/mango_account_components.rs index 0bcbb466d..f811df391 100644 --- a/programs/mango-v4/src/state/mango_account_components.rs +++ b/programs/mango-v4/src/state/mango_account_components.rs @@ -145,20 +145,28 @@ pub struct Serum3Orders { pub highest_placed_bid_inv: f64, pub lowest_placed_ask: f64, - /// Tracks the amount of deposits that flowed into the serum open orders account. + /// An overestimate of the amount of tokens that might flow out of the open orders account. /// - /// The bank still considers these amounts user deposits (see deposits_in_serum) - /// and they need to be deducted from there when they flow back into the bank - /// as real tokens. - pub base_deposits_reserved: u64, - pub quote_deposits_reserved: u64, + /// The bank still considers these amounts user deposits (see Bank::potential_serum_tokens) + /// and that value needs to be updated in conjunction with these numbers. + /// + /// This estimation is based on the amount of tokens in the open orders account + /// (see update_bank_potential_tokens() in serum3_place_order and settle) + pub potential_base_tokens: u64, + pub potential_quote_tokens: u64, + + /// Track lowest bid/highest ask, same way as for highest bid/lowest ask. + /// + /// 0 is a special "unset" state. + pub lowest_placed_bid_inv: f64, + pub highest_placed_ask: f64, #[derivative(Debug = "ignore")] - pub reserved: [u8; 32], + pub reserved: [u8; 16], } const_assert_eq!( size_of::(), - 32 + 8 * 2 + 2 * 3 + 2 + 4 * 8 + 32 + 32 + 8 * 2 + 2 * 3 + 2 + 6 * 8 + 16 ); const_assert_eq!(size_of::(), 120); const_assert_eq!(size_of::() % 8, 0); @@ -185,9 +193,11 @@ impl Default for Serum3Orders { quote_borrows_without_fee: 0, highest_placed_bid_inv: 0.0, lowest_placed_ask: 0.0, - base_deposits_reserved: 0, - quote_deposits_reserved: 0, - reserved: [0; 32], + potential_base_tokens: 0, + potential_quote_tokens: 0, + lowest_placed_bid_inv: 0.0, + highest_placed_ask: 0.0, + reserved: [0; 16], } } } diff --git a/programs/mango-v4/src/state/orderbook/book.rs b/programs/mango-v4/src/state/orderbook/book.rs index 57782ba3c..279f533cc 100644 --- a/programs/mango-v4/src/state/orderbook/book.rs +++ b/programs/mango-v4/src/state/orderbook/book.rs @@ -1,9 +1,6 @@ -use crate::logs::{FilledPerpOrderLog, PerpTakerTradeLog}; -use crate::state::MangoAccountRefMut; -use crate::{ - error::*, - state::{orderbook::bookside::*, EventQueue, PerpMarket}, -}; +use crate::error::*; +use crate::logs::{emit_stack, FilledPerpOrderLog, PerpTakerTradeLog}; +use crate::state::{orderbook::bookside::*, EventQueue, MangoAccountRefMut, PerpMarket}; use anchor_lang::prelude::*; use bytemuck::cast; use fixed::types::I80F48; @@ -205,7 +202,7 @@ impl<'a> Orderbook<'a> { event_queue.push_back(cast(fill)).unwrap(); limit -= 1; - emit!(FilledPerpOrderLog { + emit_stack(FilledPerpOrderLog { mango_group: market.group.key(), perp_market_index: market.perp_market_index, seq_num, @@ -226,7 +223,7 @@ impl<'a> Orderbook<'a> { mango_account, total_quote_lots_taken - decremented_quote_lots, )?; - emit!(PerpTakerTradeLog { + emit_stack(PerpTakerTradeLog { mango_group: market.group.key(), mango_account: *mango_account_pk, perp_market_index: market.perp_market_index, diff --git a/programs/mango-v4/src/state/perp_market.rs b/programs/mango-v4/src/state/perp_market.rs index 8aa550254..1bbf48270 100644 --- a/programs/mango-v4/src/state/perp_market.rs +++ b/programs/mango-v4/src/state/perp_market.rs @@ -8,7 +8,7 @@ use static_assertions::const_assert_eq; use crate::accounts_zerocopy::KeyedAccountReader; use crate::error::MangoError; -use crate::logs::PerpUpdateFundingLogV2; +use crate::logs::{emit_stack, PerpUpdateFundingLogV2}; use crate::state::orderbook::Side; use crate::state::{oracle, TokenIndex}; use crate::util; @@ -343,7 +343,7 @@ impl PerpMarket { self.stable_price_model .update(now_ts, oracle_price.to_num()); - emit!(PerpUpdateFundingLogV2 { + emit_stack(PerpUpdateFundingLogV2 { mango_group: self.group, market_index: self.perp_market_index, long_funding: self.long_funding.to_bits(), diff --git a/programs/mango-v4/src/state/serum3_market.rs b/programs/mango-v4/src/state/serum3_market.rs index f72ca7554..053bf0794 100644 --- a/programs/mango-v4/src/state/serum3_market.rs +++ b/programs/mango-v4/src/state/serum3_market.rs @@ -26,7 +26,13 @@ pub struct Serum3Market { pub bump: u8, - pub padding2: [u8; 5], + pub padding2: [u8; 1], + + /// Limit orders must be <= oracle * (1+band) and >= oracle / (1+band) + /// + /// Zero value is the default due to migration and disables the limit, + /// same as f32::MAX. + pub oracle_price_band: f32, pub registration_time: u64, @@ -34,7 +40,7 @@ pub struct Serum3Market { } const_assert_eq!( size_of::(), - 32 + 2 + 2 + 1 + 3 + 16 + 2 * 32 + 2 + 1 + 5 + 8 + 128 + 32 + 2 + 2 + 1 + 3 + 16 + 2 * 32 + 2 + 1 + 1 + 4 + 8 + 128 ); const_assert_eq!(size_of::(), 264); const_assert_eq!(size_of::() % 8, 0); @@ -53,6 +59,14 @@ impl Serum3Market { pub fn is_force_close(&self) -> bool { self.force_close == 1 } + + pub fn oracle_price_band(&self) -> f32 { + if self.oracle_price_band == 0.0 { + f32::MAX // default disabled + } else { + self.oracle_price_band + } + } } #[account(zero_copy)] diff --git a/programs/mango-v4/tests/cases/test_basic.rs b/programs/mango-v4/tests/cases/test_basic.rs index 0f58ce0c8..b02cf1291 100644 --- a/programs/mango-v4/tests/cases/test_basic.rs +++ b/programs/mango-v4/tests/cases/test_basic.rs @@ -414,3 +414,270 @@ async fn test_account_size_migration() -> Result<(), TransportError> { Ok(()) } + +#[tokio::test] +async fn test_bank_maint_weight_shift() -> 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..1]; + + let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig { + admin, + payer, + mints: mints.to_vec(), + zero_token_is_quote: true, + ..mango_setup::GroupWithTokensConfig::default() + } + .create(solana) + .await; + + let funding_amount = 1000; + let account = create_funded_account( + &solana, + group, + owner, + 1, + &context.users[1], + mints, + funding_amount, + 0, + ) + .await; + + let maint_health = account_maint_health(solana, account).await; + assert!(assert_equal_f64_f64(maint_health, 1000.0, 1e-2)); + + let start_time = solana.clock_timestamp().await; + + send_tx( + solana, + TokenEdit { + group, + admin, + mint: mints[0].pubkey, + options: mango_v4::instruction::TokenEdit { + maint_weight_shift_start_opt: Some(start_time + 1000), + maint_weight_shift_end_opt: Some(start_time + 2000), + maint_weight_shift_asset_target_opt: Some(0.5), + maint_weight_shift_liab_target_opt: Some(1.5), + ..token_edit_instruction_default() + }, + }, + ) + .await + .unwrap(); + + let maint_health = account_maint_health(solana, account).await; + assert!(assert_equal_f64_f64(maint_health, 1000.0, 1e-2)); + + solana.set_clock_timestamp(start_time + 1500).await; + + let maint_health = account_maint_health(solana, account).await; + assert!(assert_equal_f64_f64(maint_health, 750.0, 1e-2)); + + solana.set_clock_timestamp(start_time + 3000).await; + + let maint_health = account_maint_health(solana, account).await; + assert!(assert_equal_f64_f64(maint_health, 500.0, 1e-2)); + + solana.set_clock_timestamp(start_time + 1600).await; + + send_tx( + solana, + TokenEdit { + group, + admin, + mint: mints[0].pubkey, + options: mango_v4::instruction::TokenEdit { + maint_weight_shift_abort: true, + ..token_edit_instruction_default() + }, + }, + ) + .await + .unwrap(); + + let maint_health = account_maint_health(solana, account).await; + assert!(assert_equal_f64_f64(maint_health, 700.0, 1e-2)); + + let bank: Bank = solana.get_account(tokens[0].bank).await; + assert!(assert_equal_fixed_f64(bank.maint_asset_weight, 0.7, 1e-4)); + assert!(assert_equal_fixed_f64(bank.maint_liab_weight, 1.3, 1e-4)); + assert_eq!(bank.maint_weight_shift_duration_inv, I80F48::ZERO); + + Ok(()) +} + +#[tokio::test] +async fn test_bank_deposit_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 payer_token_account = context.users[1].token_accounts[0]; + let mints = &context.mints[0..1]; + + let mango_setup::GroupWithTokens { group, .. } = mango_setup::GroupWithTokensConfig { + admin, + payer, + mints: mints.to_vec(), + zero_token_is_quote: true, + ..mango_setup::GroupWithTokensConfig::default() + } + .create(solana) + .await; + + let funding_amount = 0; + let account1 = create_funded_account( + &solana, + group, + owner, + 1, + &context.users[1], + &mints[0..0], + funding_amount, + 0, + ) + .await; + let account2 = create_funded_account( + &solana, + group, + owner, + 2, + &context.users[1], + &mints[0..0], + funding_amount, + 0, + ) + .await; + + send_tx( + solana, + TokenEdit { + group, + admin, + mint: mints[0].pubkey, + options: mango_v4::instruction::TokenEdit { + deposit_limit_opt: Some(2000), + ..token_edit_instruction_default() + }, + }, + ) + .await + .unwrap(); + + let default_deposit_ix = TokenDepositInstruction { + amount: 0, + reduce_only: false, + account: Pubkey::default(), + owner, + token_account: payer_token_account, + token_authority: payer, + bank_index: 0, + }; + + send_tx_expect_error!( + solana, + TokenDepositInstruction { + amount: 2001, + account: account1, + ..default_deposit_ix + }, + MangoError::BankDepositLimit + ); + + send_tx( + solana, + TokenDepositInstruction { + amount: 1001, + account: account1, + ..default_deposit_ix + }, + ) + .await + .unwrap(); + + send_tx_expect_error!( + solana, + TokenDepositInstruction { + amount: 1000, + account: account1, + ..default_deposit_ix + }, + MangoError::BankDepositLimit + ); + + send_tx_expect_error!( + solana, + TokenDepositInstruction { + amount: 1000, + account: account2, + ..default_deposit_ix + }, + MangoError::BankDepositLimit + ); + + send_tx( + solana, + TokenDepositInstruction { + amount: 998, // 999 does not work due to rounding + account: account2, + ..default_deposit_ix + }, + ) + .await + .unwrap(); + + send_tx_expect_error!( + solana, + TokenDepositInstruction { + amount: 1, + account: account2, + ..default_deposit_ix + }, + MangoError::BankDepositLimit + ); + + send_tx( + solana, + TokenWithdrawInstruction { + amount: 5, + allow_borrow: false, + account: account2, + owner, + token_account: payer_token_account, + bank_index: 0, + }, + ) + .await + .unwrap(); + + send_tx_expect_error!( + solana, + TokenDepositInstruction { + amount: 6, + account: account2, + ..default_deposit_ix + }, + MangoError::BankDepositLimit + ); + + send_tx( + solana, + TokenDepositInstruction { + amount: 5, + account: account2, + ..default_deposit_ix + }, + ) + .await + .unwrap(); + + Ok(()) +} diff --git a/programs/mango-v4/tests/cases/test_health_compute.rs b/programs/mango-v4/tests/cases/test_health_compute.rs index f3d018adb..35cdef8f9 100644 --- a/programs/mango-v4/tests/cases/test_health_compute.rs +++ b/programs/mango-v4/tests/cases/test_health_compute.rs @@ -73,6 +73,75 @@ async fn test_health_compute_tokens() -> Result<(), TransportError> { Ok(()) } +#[tokio::test] +async fn test_health_compute_tokens_during_maint_weight_shift() -> 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..8]; + + // + // SETUP: Create a group and an account + // + + let GroupWithTokens { group, .. } = GroupWithTokensConfig { + admin, + payer, + mints: mints.to_vec(), + ..GroupWithTokensConfig::default() + } + .create(solana) + .await; + + let account = + create_funded_account(&solana, group, owner, 0, &context.users[1], &[], 1000, 0).await; + + let now = solana.clock_timestamp().await; + for mint in mints { + send_tx( + solana, + TokenEdit { + group, + admin, + mint: mint.pubkey, + options: mango_v4::instruction::TokenEdit { + maint_weight_shift_start_opt: Some(now - 1000), + maint_weight_shift_end_opt: Some(now + 1000), + maint_weight_shift_asset_target_opt: Some(0.1), + maint_weight_shift_liab_target_opt: Some(1.1), + ..token_edit_instruction_default() + }, + }, + ) + .await + .unwrap(); + } + + let mut cu_measurements = vec![]; + for token_account in &context.users[0].token_accounts[..mints.len()] { + cu_measurements.push(deposit_cu_datapoint(solana, account, owner, *token_account).await); + } + + for (i, pair) in cu_measurements.windows(2).enumerate() { + println!( + "after adding token {}: {} (+{})", + i, + pair[1], + pair[1] - pair[0] + ); + } + + let avg_cu_increase = cu_measurements.windows(2).map(|p| p[1] - p[0]).sum::() + / (cu_measurements.len() - 1) as u64; + println!("average cu increase: {avg_cu_increase}"); + assert!(avg_cu_increase < 4200); + + Ok(()) +} + // Try to reach compute limits in health checks by having many serum markets in an account #[tokio::test] async fn test_health_compute_serum() -> Result<(), TransportError> { diff --git a/programs/mango-v4/tests/cases/test_margin_trade.rs b/programs/mango-v4/tests/cases/test_margin_trade.rs index 4c48f0727..add6b658e 100644 --- a/programs/mango-v4/tests/cases/test_margin_trade.rs +++ b/programs/mango-v4/tests/cases/test_margin_trade.rs @@ -613,3 +613,117 @@ async fn test_flash_loan_creates_ata_accounts() -> Result<(), BanksClientError> Ok(()) } + +#[tokio::test] +async fn test_margin_trade_deposit_limit() -> Result<(), BanksClientError> { + let mut test_builder = TestContextBuilder::new(); + test_builder.test().set_compute_max_units(100_000); + let context = test_builder.start_default().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_mint0_account = context.users[1].token_accounts[0]; + + // + // SETUP: Create a group, account, register a token (mint0) + // + + let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig { + admin, + payer, + mints: mints.to_vec(), + ..GroupWithTokensConfig::default() + } + .create(solana) + .await; + let bank = tokens[0].bank; + + // + // SETUP: deposit limit + // + send_tx( + solana, + TokenEdit { + group, + admin, + mint: tokens[0].mint.pubkey, + options: mango_v4::instruction::TokenEdit { + deposit_limit_opt: Some(1000), + ..token_edit_instruction_default() + }, + }, + ) + .await + .unwrap(); + + // + // create the test user account + // + + let deposit_amount_initial = 100; + let account = create_funded_account( + &solana, + group, + owner, + 0, + &context.users[1], + &mints[..1], + deposit_amount_initial, + 0, + ) + .await; + + // + // TEST: Margin trade + // + let margin_account = payer_mint0_account; + let target_token_account = context.users[0].token_accounts[0]; + let make_flash_loan_tx = |solana, deposit_amount| async move { + let mut tx = ClientTransaction::new(solana); + let loans = vec![FlashLoanPart { + bank, + token_account: target_token_account, + withdraw_amount: 0, + }]; + tx.add_instruction(FlashLoanBeginInstruction { + account, + owner, + loans: loans.clone(), + }) + .await; + tx.add_instruction_direct( + spl_token::instruction::transfer( + &spl_token::ID, + &margin_account, + &target_token_account, + &payer.pubkey(), + &[&payer.pubkey()], + deposit_amount, + ) + .unwrap(), + ); + tx.add_signer(payer); + tx.add_instruction(FlashLoanEndInstruction { + account, + owner, + loans, + // the test only accesses a single token: not a swap + flash_loan_type: mango_v4::accounts_ix::FlashLoanType::Unknown, + }) + .await; + tx + }; + + make_flash_loan_tx(solana, 901) + .await + .send_expect_error(MangoError::BankDepositLimit) + .await + .unwrap(); + + make_flash_loan_tx(solana, 899).await.send().await.unwrap(); + + Ok(()) +} diff --git a/programs/mango-v4/tests/cases/test_serum.rs b/programs/mango-v4/tests/cases/test_serum.rs index 9ad854515..90713303b 100644 --- a/programs/mango-v4/tests/cases/test_serum.rs +++ b/programs/mango-v4/tests/cases/test_serum.rs @@ -309,13 +309,13 @@ async fn test_serum_basics() -> Result<(), TransportError> { // // TEST: Place an order // - let (order_id, _) = order_placer.bid_maker(1.0, 100).await.unwrap(); + let (order_id, _) = order_placer.bid_maker(0.9, 100).await.unwrap(); check_prev_instruction_post_health(&solana, account).await; let native0 = account_position(solana, account, base_token.bank).await; let native1 = account_position(solana, account, quote_token.bank).await; assert_eq!(native0, 1000); - assert_eq!(native1, 900); + assert_eq!(native1, 910); let account_data = get_mango_account(solana, account).await; assert_eq!( @@ -342,13 +342,13 @@ async fn test_serum_basics() -> Result<(), TransportError> { let serum_orders = account_data.serum3_orders_by_raw_index(0).unwrap(); assert_eq!(serum_orders.base_borrows_without_fee, 0); assert_eq!(serum_orders.quote_borrows_without_fee, 0); - assert_eq!(serum_orders.base_deposits_reserved, 0); - assert_eq!(serum_orders.quote_deposits_reserved, 100); + assert_eq!(serum_orders.potential_base_tokens, 100); + assert_eq!(serum_orders.potential_quote_tokens, 90); let base_bank = solana.get_account::(base_token.bank).await; - assert_eq!(base_bank.deposits_in_serum, 0); + assert_eq!(base_bank.potential_serum_tokens, 100); let quote_bank = solana.get_account::(quote_token.bank).await; - assert_eq!(quote_bank.deposits_in_serum, 100); + assert_eq!(quote_bank.potential_serum_tokens, 90); assert!(order_id != 0); @@ -371,13 +371,13 @@ async fn test_serum_basics() -> Result<(), TransportError> { let serum_orders = account_data.serum3_orders_by_raw_index(0).unwrap(); assert_eq!(serum_orders.base_borrows_without_fee, 0); assert_eq!(serum_orders.quote_borrows_without_fee, 0); - assert_eq!(serum_orders.base_deposits_reserved, 0); - assert_eq!(serum_orders.quote_deposits_reserved, 0); + assert_eq!(serum_orders.potential_base_tokens, 0); + assert_eq!(serum_orders.potential_quote_tokens, 0); let base_bank = solana.get_account::(base_token.bank).await; - assert_eq!(base_bank.deposits_in_serum, 0); + assert_eq!(base_bank.potential_serum_tokens, 0); let quote_bank = solana.get_account::(quote_token.bank).await; - assert_eq!(quote_bank.deposits_in_serum, 0); + assert_eq!(quote_bank.potential_serum_tokens, 0); // Process events such that the OutEvent deactivates the closed order on open_orders context @@ -1150,89 +1150,72 @@ async fn test_serum_track_bid_ask() -> Result<(), TransportError> { // TEST: highest bid/lowest ask updating // - assert_eq!( - order_placer - .mango_serum_orders() - .await - .highest_placed_bid_inv, - 0.0 - ); - assert_eq!( - order_placer.mango_serum_orders().await.lowest_placed_ask, - 0.0 - ); + let srm = order_placer.mango_serum_orders().await; + assert_eq!(srm.highest_placed_bid_inv, 0.0); + assert_eq!(srm.lowest_placed_bid_inv, 0.0); + assert_eq!(srm.highest_placed_ask, 0.0); + assert_eq!(srm.lowest_placed_ask, 0.0); order_placer.bid_maker(10.0, 100).await.unwrap(); - assert_eq!( - order_placer - .mango_serum_orders() - .await - .highest_placed_bid_inv, - 1.0 / 10.0 - ); + + let srm = order_placer.mango_serum_orders().await; + assert_eq!(srm.highest_placed_bid_inv, 1.0 / 10.0); + assert_eq!(srm.lowest_placed_bid_inv, 1.0 / 10.0); + assert_eq!(srm.highest_placed_ask, 0.0); + assert_eq!(srm.lowest_placed_ask, 0.0); order_placer.bid_maker(9.0, 100).await.unwrap(); - assert_eq!( - order_placer - .mango_serum_orders() - .await - .highest_placed_bid_inv, - 1.0 / 10.0 - ); + + let srm = order_placer.mango_serum_orders().await; + assert_eq!(srm.highest_placed_bid_inv, 1.0 / 10.0); + assert_eq!(srm.lowest_placed_bid_inv, 1.0 / 9.0); + assert_eq!(srm.highest_placed_ask, 0.0); + assert_eq!(srm.lowest_placed_ask, 0.0); order_placer.bid_maker(11.0, 100).await.unwrap(); - assert_eq!( - order_placer - .mango_serum_orders() - .await - .highest_placed_bid_inv, - 1.0 / 11.0 - ); - assert_eq!( - order_placer.mango_serum_orders().await.lowest_placed_ask, - 0.0 - ); + let srm = order_placer.mango_serum_orders().await; + assert_eq!(srm.highest_placed_bid_inv, 1.0 / 11.0); + assert_eq!(srm.lowest_placed_bid_inv, 1.0 / 9.0); + assert_eq!(srm.highest_placed_ask, 0.0); + assert_eq!(srm.lowest_placed_ask, 0.0); + order_placer.ask(20.0, 100).await.unwrap(); - assert_eq!( - order_placer.mango_serum_orders().await.lowest_placed_ask, - 20.0 - ); - order_placer.ask(19.0, 100).await.unwrap(); - assert_eq!( - order_placer.mango_serum_orders().await.lowest_placed_ask, - 19.0 - ); - order_placer.ask(21.0, 100).await.unwrap(); - assert_eq!( - order_placer.mango_serum_orders().await.lowest_placed_ask, - 19.0 - ); - assert_eq!( - order_placer - .mango_serum_orders() - .await - .highest_placed_bid_inv, - 1.0 / 11.0 - ); + let srm = order_placer.mango_serum_orders().await; + assert_eq!(srm.highest_placed_bid_inv, 1.0 / 11.0); + assert_eq!(srm.lowest_placed_bid_inv, 1.0 / 9.0); + assert_eq!(srm.highest_placed_ask, 20.0); + assert_eq!(srm.lowest_placed_ask, 20.0); + + order_placer.ask(19.0, 100).await.unwrap(); + + let srm = order_placer.mango_serum_orders().await; + assert_eq!(srm.highest_placed_bid_inv, 1.0 / 11.0); + assert_eq!(srm.lowest_placed_bid_inv, 1.0 / 9.0); + assert_eq!(srm.highest_placed_ask, 20.0); + assert_eq!(srm.lowest_placed_ask, 19.0); + + order_placer.ask(21.0, 100).await.unwrap(); + + let srm = order_placer.mango_serum_orders().await; + assert_eq!(srm.highest_placed_bid_inv, 1.0 / 11.0); + assert_eq!(srm.lowest_placed_bid_inv, 1.0 / 9.0); + assert_eq!(srm.highest_placed_ask, 21.0); + assert_eq!(srm.lowest_placed_ask, 19.0); // // TEST: cancellation allows for resets // order_placer.cancel_all().await; - assert_eq!( - order_placer.mango_serum_orders().await.lowest_placed_ask, - 19.0 - ); - assert_eq!( - order_placer - .mango_serum_orders() - .await - .highest_placed_bid_inv, - 1.0 / 11.0 - ); + + // no immediate change + let srm = order_placer.mango_serum_orders().await; + assert_eq!(srm.highest_placed_bid_inv, 1.0 / 11.0); + assert_eq!(srm.lowest_placed_bid_inv, 1.0 / 9.0); + assert_eq!(srm.highest_placed_ask, 21.0); + assert_eq!(srm.lowest_placed_ask, 19.0); // Process events such that the OutEvent deactivates the closed order on open_orders context @@ -1242,36 +1225,36 @@ async fn test_serum_track_bid_ask() -> Result<(), TransportError> { // takes new value for bid, resets ask order_placer.bid_maker(1.0, 100).await.unwrap(); - assert_eq!( - order_placer.mango_serum_orders().await.lowest_placed_ask, - 0.0 - ); - assert_eq!( - order_placer - .mango_serum_orders() - .await - .highest_placed_bid_inv, - 1.0 - ); + + let srm = order_placer.mango_serum_orders().await; + assert_eq!(srm.highest_placed_bid_inv, 1.0); + assert_eq!(srm.lowest_placed_bid_inv, 1.0); + assert_eq!(srm.highest_placed_ask, 0.0); + assert_eq!(srm.lowest_placed_ask, 0.0); // // TEST: can reset even when there's still an order on the other side // let (oid, _) = order_placer.ask(10.0, 100).await.unwrap(); - assert_eq!( - order_placer.mango_serum_orders().await.lowest_placed_ask, - 10.0 - ); + + let srm = order_placer.mango_serum_orders().await; + assert_eq!(srm.highest_placed_bid_inv, 1.0); + assert_eq!(srm.lowest_placed_bid_inv, 1.0); + assert_eq!(srm.highest_placed_ask, 10.0); + assert_eq!(srm.lowest_placed_ask, 10.0); + order_placer.cancel(oid).await; context .serum .consume_spot_events(&serum_market_cookie, &[order_placer.open_orders]) .await; order_placer.ask(9.0, 100).await.unwrap(); - assert_eq!( - order_placer.mango_serum_orders().await.lowest_placed_ask, - 9.0 - ); + + let srm = order_placer.mango_serum_orders().await; + assert_eq!(srm.highest_placed_bid_inv, 1.0); + assert_eq!(srm.lowest_placed_bid_inv, 1.0); + assert_eq!(srm.highest_placed_ask, 9.0); + assert_eq!(srm.lowest_placed_ask, 9.0); Ok(()) } @@ -1305,10 +1288,10 @@ async fn test_serum_track_reserved_deposits() -> Result<(), TransportError> { let base_bank = solana.get_account::(base_bank).await; let quote_bank = solana.get_account::(quote_bank).await; ( - orders.base_deposits_reserved, - base_bank.deposits_in_serum, - orders.quote_deposits_reserved, - quote_bank.deposits_in_serum, + orders.potential_base_tokens, + base_bank.potential_serum_tokens, + orders.potential_quote_tokens, + quote_bank.potential_serum_tokens, ) }; @@ -1317,9 +1300,14 @@ async fn test_serum_track_reserved_deposits() -> Result<(), TransportError> { // order_placer.bid_maker(0.8, 2000).await.unwrap(); - order_placer.ask(1.2, 2000).await.unwrap(); assert_eq!(get_vals(solana).await, (2000, 2000, 1600, 1600)); + order_placer.ask(1.2, 2000).await.unwrap(); + assert_eq!( + get_vals(solana).await, + (2 * 2000, 2 * 2000, 1600 + 2400, 1600 + 2400) + ); + // // TEST: match partially on both sides, increasing the on-bank reserved amounts // because order_placer2 puts funds into the serum oo @@ -1333,9 +1321,12 @@ async fn test_serum_track_reserved_deposits() -> Result<(), TransportError> { &[order_placer.open_orders, order_placer2.open_orders], ) .await; - assert_eq!(get_vals(solana).await, (2000, 2000, 1600, 2801)); + // taker order directly converted to base, no change to quote + assert_eq!(get_vals(solana).await, (4000, 4000 + 1000, 4000, 4000)); + + // takes out 1000 base order_placer2.settle_v2(false).await; - assert_eq!(get_vals(solana).await, (2000, 2000, 1600, 1600)); + assert_eq!(get_vals(solana).await, (4000, 4000, 4000, 4000)); order_placer2.ask(0.8, 1000).await.unwrap(); context @@ -1345,16 +1336,19 @@ async fn test_serum_track_reserved_deposits() -> Result<(), TransportError> { &[order_placer.open_orders, order_placer2.open_orders], ) .await; - assert_eq!(get_vals(solana).await, (2000, 3000, 1600, 1600)); + // taker order directly converted to quote + assert_eq!(get_vals(solana).await, (4000, 4000, 4000, 4000 + 799)); + order_placer2.settle_v2(false).await; - assert_eq!(get_vals(solana).await, (2000, 2000, 1600, 1600)); + assert_eq!(get_vals(solana).await, (4000, 4000, 4000, 4000)); // // TEST: Settlement updates the values // order_placer.settle_v2(false).await; - assert_eq!(get_vals(solana).await, (1000, 1000, 800, 800)); + // remaining is bid 1000 @ 0.8; ask 1000 @ 1.2 + assert_eq!(get_vals(solana).await, (2000, 2000, 2000, 2000)); Ok(()) } @@ -1481,6 +1475,239 @@ async fn test_serum_compute() -> Result<(), TransportError> { Ok(()) } +#[tokio::test] +async fn test_serum_bands() -> Result<(), TransportError> { + let mut test_builder = TestContextBuilder::new(); + test_builder.test().set_compute_max_units(150_000); // Serum3PlaceOrder needs lots + let context = test_builder.start_default().await; + let solana = &context.solana.clone(); + + // + // SETUP: Create a group, accounts, market etc + // + let deposit_amount = 10000; + let CommonSetup { + group_with_tokens, + mut order_placer, + quote_token, + base_token, + .. + } = common_setup(&context, deposit_amount).await; + + // + // SETUP: Set oracle price for market to 100 + // + set_bank_stub_oracle_price( + solana, + group_with_tokens.group, + &base_token, + group_with_tokens.admin, + 200.0, + ) + .await; + set_bank_stub_oracle_price( + solana, + group_with_tokens.group, + "e_token, + group_with_tokens.admin, + 2.0, + ) + .await; + + // + // TEST: can place way over/under oracle + // + + order_placer.bid_maker(1.0, 100).await.unwrap(); + order_placer.ask(200.0, 100).await.unwrap(); + order_placer.cancel_all().await; + + // + // TEST: Can't when bands are enabled + // + send_tx( + solana, + Serum3EditMarketInstruction { + group: group_with_tokens.group, + admin: group_with_tokens.admin, + market: order_placer.serum_market, + options: mango_v4::instruction::Serum3EditMarket { + oracle_price_band_opt: Some(0.5), + ..serum3_edit_market_instruction_default() + }, + }, + ) + .await + .unwrap(); + + let r = order_placer.try_bid(65.0, 100, false).await; + assert!(r.is_err()); + let r = order_placer.try_ask(151.0, 100).await; + assert!(r.is_err()); + + order_placer.try_bid(67.0, 100, false).await.unwrap(); + order_placer.try_ask(149.0, 100).await.unwrap(); + + Ok(()) +} + +#[tokio::test] +async fn test_serum_deposit_limits() -> Result<(), TransportError> { + let mut test_builder = TestContextBuilder::new(); + test_builder.test().set_compute_max_units(150_000); // Serum3PlaceOrder needs lots + let context = test_builder.start_default().await; + let solana = &context.solana.clone(); + + // + // SETUP: Create a group, accounts, market etc + // + let deposit_amount = 5000; // for 10k tokens over both order_placers + let CommonSetup { + group_with_tokens, + mut order_placer, + quote_token, + base_token, + .. + } = common_setup2(&context, deposit_amount, 0).await; + + // + // SETUP: Set oracle price for market to 2 + // + set_bank_stub_oracle_price( + solana, + group_with_tokens.group, + &base_token, + group_with_tokens.admin, + 4.0, + ) + .await; + set_bank_stub_oracle_price( + solana, + group_with_tokens.group, + "e_token, + group_with_tokens.admin, + 2.0, + ) + .await; + + // + // SETUP: Base token: add deposit limit + // + send_tx( + solana, + TokenEdit { + group: group_with_tokens.group, + admin: group_with_tokens.admin, + mint: base_token.mint.pubkey, + options: mango_v4::instruction::TokenEdit { + deposit_limit_opt: Some(13000), + ..token_edit_instruction_default() + }, + }, + ) + .await + .unwrap(); + + let solana2 = context.solana.clone(); + let base_bank = base_token.bank; + let remaining_base = { + || async { + let b: Bank = solana2.get_account(base_bank).await; + b.remaining_deposits_until_limit().round().to_num::() + } + }; + + // + // TEST: even when placing all base tokens into an ask, they still count + // + + order_placer.ask(2.0, 5000).await.unwrap(); + assert_eq!(remaining_base().await, 3000); + + // + // TEST: if we bid to buy more base, the limit reduces + // + + order_placer.bid_maker(1.5, 1000).await.unwrap(); + assert_eq!(remaining_base().await, 2000); + + // + // TEST: if we bid too much for the limit, the order does not go through + // + + let r = order_placer.try_bid(1.5, 2001, false).await; + assert_mango_error(&r, MangoError::BankDepositLimit.into(), "dep limit".into()); + order_placer.try_bid(1.5, 1999, false).await.unwrap(); // not 2000 due to rounding + + // + // SETUP: Switch deposit limit to quote token + // + + send_tx( + solana, + TokenEdit { + group: group_with_tokens.group, + admin: group_with_tokens.admin, + mint: base_token.mint.pubkey, + options: mango_v4::instruction::TokenEdit { + deposit_limit_opt: Some(0), + ..token_edit_instruction_default() + }, + }, + ) + .await + .unwrap(); + send_tx( + solana, + TokenEdit { + group: group_with_tokens.group, + admin: group_with_tokens.admin, + mint: quote_token.mint.pubkey, + options: mango_v4::instruction::TokenEdit { + deposit_limit_opt: Some(13000), + ..token_edit_instruction_default() + }, + }, + ) + .await + .unwrap(); + + let solana2 = context.solana.clone(); + let quote_bank = quote_token.bank; + let remaining_quote = { + || async { + let b: Bank = solana2.get_account(quote_bank).await; + b.remaining_deposits_until_limit().round().to_num::() + } + }; + + order_placer.cancel_all().await; + + // + // TEST: even when placing all quote tokens into a bid, they still count + // + + order_placer.bid_maker(2.0, 2500).await.unwrap(); + assert_eq!(remaining_quote().await, 3000); + + // + // TEST: if we ask to get more quote, the limit reduces + // + + order_placer.ask(5.0, 200).await.unwrap(); + assert_eq!(remaining_quote().await, 2000); + + // + // TEST: if we bid too much for the limit, the order does not go through + // + + let r = order_placer.try_ask(5.0, 401).await; + assert_mango_error(&r, MangoError::BankDepositLimit.into(), "dep limit".into()); + order_placer.try_ask(5.0, 399).await.unwrap(); // not 400 due to rounding + + Ok(()) +} + struct CommonSetup { group_with_tokens: GroupWithTokens, serum_market_cookie: SpotMarketCookie, @@ -1491,6 +1718,14 @@ struct CommonSetup { } async fn common_setup(context: &TestContext, deposit_amount: u64) -> CommonSetup { + common_setup2(context, deposit_amount, 10000000).await +} + +async fn common_setup2( + context: &TestContext, + deposit_amount: u64, + vault_funding: u64, +) -> CommonSetup { let admin = TestKeypair::new(); let owner = context.users[0].key; let payer = context.users[1].key; @@ -1565,17 +1800,19 @@ async fn common_setup(context: &TestContext, deposit_amount: u64) -> CommonSetup ) .await; // to have enough funds in the vaults - create_funded_account( - &solana, - group, - owner, - 3, - &context.users[1], - mints, - 10000000, - 0, - ) - .await; + if vault_funding > 0 { + create_funded_account( + &solana, + group, + owner, + 3, + &context.users[1], + mints, + 10000000, + 0, + ) + .await; + } let open_orders = send_tx( solana, diff --git a/programs/mango-v4/tests/cases/test_token_conditional_swap.rs b/programs/mango-v4/tests/cases/test_token_conditional_swap.rs index 62827222d..eb4ce1e83 100644 --- a/programs/mango-v4/tests/cases/test_token_conditional_swap.rs +++ b/programs/mango-v4/tests/cases/test_token_conditional_swap.rs @@ -1013,3 +1013,145 @@ async fn test_token_conditional_swap_premium_auction() -> Result<(), TransportEr Ok(()) } + +#[tokio::test] +async fn test_token_conditional_swap_deposit_limit() -> Result<(), TransportError> { + pub use utils::assert_equal_f64_f64 as assert_equal_f_f; + + 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]; + + // + // SETUP: Create a group, account, tokens + // + + let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig { + admin, + payer, + mints: mints.to_vec(), + ..mango_setup::GroupWithTokensConfig::default() + } + .create(solana) + .await; + let quote_token = &tokens[0]; + let base_token = &tokens[1]; + + // total deposits on quote and base is 2x this value + let deposit_amount = 1_000f64; + let account = create_funded_account( + &solana, + group, + owner, + 0, + &context.users[1], + &mints[..1], + deposit_amount as u64, + 0, + ) + .await; + let liqor = create_funded_account( + &solana, + group, + owner, + 1, + &context.users[1], + mints, + deposit_amount as u64, + 0, + ) + .await; + create_funded_account( + &solana, + group, + owner, + 99, + &context.users[1], + &mints[1..], + deposit_amount as u64, + 0, + ) + .await; + + // + // SETUP: A base deposit limit + // + send_tx( + solana, + TokenEdit { + group, + admin, + mint: base_token.mint.pubkey, + options: mango_v4::instruction::TokenEdit { + deposit_limit_opt: Some(2500), + ..token_edit_instruction_default() + }, + }, + ) + .await + .unwrap(); + + // + // SETUP: Sell base token from "account" creating borrows that are deposited into liqor, + // increasing the total deposits + // + send_tx( + solana, + TokenConditionalSwapCreateInstruction { + account, + owner, + buy_mint: quote_token.mint.pubkey, + sell_mint: base_token.mint.pubkey, + max_buy: 900, + max_sell: 900, + price_lower_limit: 0.0, + price_upper_limit: 10.0, + price_premium_rate: 0.01, + allow_creating_deposits: true, + allow_creating_borrows: true, + }, + ) + .await + .unwrap(); + + // + // TEST: Large execution fails, a bit smaller is ok + // + + send_tx_expect_error!( + solana, + TokenConditionalSwapTriggerInstruction { + liqee: account, + liqor, + liqor_owner: owner, + index: 0, + max_buy_token_to_liqee: 100000, + max_sell_token_to_liqor: 501, + min_buy_token: 1, + min_taker_price: 0.0, + }, + MangoError::BankDepositLimit, + ); + + send_tx( + solana, + TokenConditionalSwapTriggerInstruction { + liqee: account, + liqor, + liqor_owner: owner, + index: 0, + max_buy_token_to_liqee: 100000, + max_sell_token_to_liqor: 499, + min_buy_token: 1, + min_taker_price: 0.0, + }, + ) + .await + .unwrap(); + + Ok(()) +} diff --git a/programs/mango-v4/tests/program_test/mango_client.rs b/programs/mango-v4/tests/program_test/mango_client.rs index a24e16356..99cf2c82b 100644 --- a/programs/mango-v4/tests/program_test/mango_client.rs +++ b/programs/mango-v4/tests/program_test/mango_client.rs @@ -69,6 +69,27 @@ pub async fn send_tx_get_metadata( .await } +#[macro_export] +macro_rules! send_tx_expect_error { + ($solana:expr, $ix:expr, $err:expr $(,)?) => { + let result = send_tx($solana, $ix).await; + let expected_err: u32 = $err.into(); + match result { + Ok(_) => assert!(false, "no error returned"), + Err(TransportError::TransactionError( + solana_sdk::transaction::TransactionError::InstructionError( + _, + solana_program::instruction::InstructionError::Custom(err_num), + ), + )) => { + assert_eq!(err_num, expected_err, "wrong error code"); + } + _ => assert!(false, "not a mango error"), + } + }; +} +pub use send_tx_expect_error; + /// Build a transaction from multiple instructions pub struct ClientTransaction { solana: Arc, @@ -111,6 +132,28 @@ impl<'a> ClientTransaction { Ok(()) } + pub async fn send_expect_error( + &self, + error: mango_v4::error::MangoError, + ) -> std::result::Result<(), BanksClientError> { + let tx_result = self + .solana + .process_transaction(&self.instructions, Some(&self.signers)) + .await?; + match tx_result.result { + Ok(_) => assert!(false, "no error returned"), + Err(solana_sdk::transaction::TransactionError::InstructionError( + _, + solana_program::instruction::InstructionError::Custom(err_num), + )) => { + let expected_err: u32 = error.into(); + assert_eq!(err_num, expected_err, "wrong error code"); + } + _ => assert!(false, "not a mango error"), + } + Ok(()) + } + // Tx error still returns success pub async fn send_get_metadata( &self, @@ -386,6 +429,17 @@ pub async fn account_init_health(solana: &SolanaCookie, account: Pubkey) -> f64 health_data.init_health.to_num::() } +pub async fn account_maint_health(solana: &SolanaCookie, account: Pubkey) -> f64 { + send_tx(solana, ComputeAccountDataInstruction { account }) + .await + .unwrap(); + let health_data = solana + .program_log_events::() + .pop() + .unwrap(); + health_data.maint_health.to_num::() +} + // Verifies that the "post_health: ..." log emitted by the previous instruction // matches the init health of the account. pub async fn check_prev_instruction_post_health(solana: &SolanaCookie, account: Pubkey) { @@ -997,6 +1051,10 @@ impl ClientInstruction for TokenRegisterInstruction { token_conditional_swap_taker_fee_rate: 0.0, token_conditional_swap_maker_fee_rate: 0.0, flash_loan_swap_fee_rate: 0.0, + interest_curve_scaling: 1.0, + interest_target_utilization: 0.5, + group_insurance_fund: true, + deposit_limit: 0, }; let bank = Pubkey::find_program_address( @@ -1241,6 +1299,15 @@ pub fn token_edit_instruction_default() -> mango_v4::instruction::TokenEdit { token_conditional_swap_taker_fee_rate_opt: None, token_conditional_swap_maker_fee_rate_opt: None, flash_loan_swap_fee_rate_opt: None, + interest_curve_scaling_opt: None, + interest_target_utilization_opt: None, + maint_weight_shift_start_opt: None, + maint_weight_shift_end_opt: None, + maint_weight_shift_asset_target_opt: None, + maint_weight_shift_liab_target_opt: None, + maint_weight_shift_abort: false, + set_fallback_oracle: false, + deposit_limit_opt: None, } } @@ -2254,6 +2321,7 @@ impl ClientInstruction for Serum3RegisterMarketInstruction { let instruction = Self::Instruction { market_index: self.market_index, name: "UUU/usdc".to_string(), + oracle_price_band: f32::MAX, }; let serum_market = Pubkey::find_program_address( @@ -2298,6 +2366,46 @@ impl ClientInstruction for Serum3RegisterMarketInstruction { } } +pub fn serum3_edit_market_instruction_default() -> mango_v4::instruction::Serum3EditMarket { + mango_v4::instruction::Serum3EditMarket { + reduce_only_opt: None, + force_close_opt: None, + name_opt: None, + oracle_price_band_opt: None, + } +} + +pub struct Serum3EditMarketInstruction { + pub group: Pubkey, + pub admin: TestKeypair, + pub market: Pubkey, + pub options: mango_v4::instruction::Serum3EditMarket, +} +#[async_trait::async_trait(?Send)] +impl ClientInstruction for Serum3EditMarketInstruction { + type Accounts = mango_v4::accounts::Serum3EditMarket; + type Instruction = mango_v4::instruction::Serum3EditMarket; + async fn to_instruction( + &self, + _account_loader: impl ClientAccountLoader + 'async_trait, + ) -> (Self::Accounts, instruction::Instruction) { + let program_id = mango_v4::id(); + + let accounts = Self::Accounts { + group: self.group, + admin: self.admin.pubkey(), + market: self.market, + }; + + let instruction = make_instruction(program_id, &accounts, &self.options); + (accounts, instruction) + } + + fn signers(&self) -> Vec { + vec![self.admin] + } +} + pub struct Serum3DeregisterMarketInstruction { pub group: Pubkey, pub admin: TestKeypair, @@ -2472,7 +2580,7 @@ pub struct Serum3PlaceOrderInstruction { #[async_trait::async_trait(?Send)] impl ClientInstruction for Serum3PlaceOrderInstruction { type Accounts = mango_v4::accounts::Serum3PlaceOrder; - type Instruction = mango_v4::instruction::Serum3PlaceOrder; + type Instruction = mango_v4::instruction::Serum3PlaceOrderV2; async fn to_instruction( &self, account_loader: impl ClientAccountLoader + 'async_trait, @@ -2526,7 +2634,7 @@ impl ClientInstruction for Serum3PlaceOrderInstruction { ) .unwrap(); - let health_check_metas = derive_health_check_remaining_account_metas( + let mut health_check_metas = derive_health_check_remaining_account_metas( &account_loader, &account, None, @@ -2535,11 +2643,17 @@ impl ClientInstruction for Serum3PlaceOrderInstruction { ) .await; - let payer_info = &match self.side { - Serum3Side::Bid => "e_info, - Serum3Side::Ask => &base_info, + let (payer_info, receiver_info) = &match self.side { + Serum3Side::Bid => ("e_info, &base_info), + Serum3Side::Ask => (&base_info, "e_info), }; + let receiver_active_index = account + .active_token_positions() + .position(|tp| tp.token_index == receiver_info.token_index) + .unwrap(); + health_check_metas[receiver_active_index].is_writable = true; + let accounts = Self::Accounts { group: account.fixed.group, account: self.account, diff --git a/programs/mango-v4/tests/program_test/utils.rs b/programs/mango-v4/tests/program_test/utils.rs index 216688a3f..d774f541f 100644 --- a/programs/mango-v4/tests/program_test/utils.rs +++ b/programs/mango-v4/tests/program_test/utils.rs @@ -87,15 +87,12 @@ pub fn assert_mango_error( ) { match result { Ok(_) => assert!(false, "No error returned"), - Err(TransportError::TransactionError(tx_err)) => match tx_err { - TransactionError::InstructionError(_, err) => match err { - InstructionError::Custom(err_num) => { - assert_eq!(*err_num, expected_error, "{}", comment); - } - _ => assert!(false, "Not a mango error"), - }, - _ => assert!(false, "Not a mango error"), - }, + Err(TransportError::TransactionError(TransactionError::InstructionError( + _, + InstructionError::Custom(err_num), + ))) => { + assert_eq!(*err_num, expected_error, "{}", comment); + } _ => assert!(false, "Not a mango error"), } } diff --git a/ts/client/src/accounts/bank.ts b/ts/client/src/accounts/bank.ts index 98650ac55..b46a521ae 100644 --- a/ts/client/src/accounts/bank.ts +++ b/ts/client/src/accounts/bank.ts @@ -42,6 +42,7 @@ export interface BankForHealth { scaledInitLiabWeight(price: I80F48): I80F48; nativeDeposits(): I80F48; nativeBorrows(): I80F48; + maintWeights(): [I80F48, I80F48]; depositWeightScaleStartQuote: number; borrowWeightScaleStartQuote: number; @@ -75,6 +76,9 @@ export class Bank implements BankForHealth { public maintLiabWeight: I80F48; public liquidationFee: I80F48; public dust: I80F48; + public maintWeightShiftDurationInv: I80F48; + public maintWeightShiftAssetTarget: I80F48; + public maintWeightShiftLiabTarget: I80F48; static from( publicKey: PublicKey, @@ -128,7 +132,13 @@ export class Bank implements BankForHealth { flashLoanSwapFeeRate: number; interestTargetUtilization: number; interestCurveScaling: number; - depositsInSerum: BN; + potentialSerumTokens: BN; + maintWeightShiftStart: BN; + maintWeightShiftEnd: BN; + maintWeightShiftDurationInv: I80F48Dto; + maintWeightShiftAssetTarget: I80F48Dto; + maintWeightShiftLiabTarget: I80F48Dto; + depositLimit: BN; }, ): Bank { return new Bank( @@ -182,7 +192,13 @@ export class Bank implements BankForHealth { obj.flashLoanSwapFeeRate, obj.interestTargetUtilization, obj.interestCurveScaling, - // obj.depositsInSerum, + obj.potentialSerumTokens, + obj.maintWeightShiftStart, + obj.maintWeightShiftEnd, + obj.maintWeightShiftDurationInv, + obj.maintWeightShiftAssetTarget, + obj.maintWeightShiftLiabTarget, + obj.depositLimit, ); } @@ -236,7 +252,14 @@ export class Bank implements BankForHealth { public tokenConditionalSwapMakerFeeRate: number, public flashLoanSwapFeeRate: number, public interestTargetUtilization: number, - public interestCurveScaling: number, // public depositsInSerum: BN, + public interestCurveScaling: number, + public potentialSerumTokens: BN, + public maintWeightShiftStart: BN, + public maintWeightShiftEnd: BN, + maintWeightShiftDurationInv: I80F48Dto, + maintWeightShiftAssetTarget: I80F48Dto, + maintWeightShiftLiabTarget: I80F48Dto, + public depositLimit: BN, ) { this.name = utf8.decode(new Uint8Array(name)).split('\x00')[0]; this.oracleConfig = { @@ -263,6 +286,9 @@ export class Bank implements BankForHealth { this.initLiabWeight = I80F48.from(initLiabWeight); this.liquidationFee = I80F48.from(liquidationFee); this.dust = I80F48.from(dust); + this.maintWeightShiftDurationInv = I80F48.from(maintWeightShiftDurationInv); + this.maintWeightShiftAssetTarget = I80F48.from(maintWeightShiftAssetTarget); + this.maintWeightShiftLiabTarget = I80F48.from(maintWeightShiftLiabTarget); this._price = undefined; this._uiPrice = undefined; this._oracleLastUpdatedSlot = undefined; @@ -384,6 +410,32 @@ export class Bank implements BankForHealth { ); } + maintWeights(): [I80F48, I80F48] { + const nowTs = new BN(Date.now() / 1000); + if ( + this.maintWeightShiftDurationInv.isZero() || + nowTs.lte(this.maintWeightShiftStart) + ) { + return [this.maintAssetWeight, this.maintLiabWeight]; + } else if (nowTs.gte(this.maintWeightShiftEnd)) { + return [ + this.maintWeightShiftAssetTarget, + this.maintWeightShiftLiabTarget, + ]; + } else { + const scale = I80F48.fromU64(nowTs.sub(this.maintWeightShiftStart)).mul( + this.maintWeightShiftDurationInv, + ); + const asset = this.maintAssetWeight.add( + this.maintWeightShiftAssetTarget.sub(this.maintAssetWeight).mul(scale), + ); + const liab = this.maintLiabWeight.add( + this.maintWeightShiftLiabTarget.sub(this.maintLiabWeight).mul(scale), + ); + return [asset, liab]; + } + } + getAssetPrice(): I80F48 { return this.price.min(I80F48.fromNumber(this.stablePriceModel.stablePrice)); } @@ -457,19 +509,22 @@ export class Bank implements BankForHealth { } const utilization = totalBorrows.div(totalDeposits); + const scaling = I80F48.fromNumber( + this.interestCurveScaling == 0.0 ? 1.0 : this.interestCurveScaling, + ); if (utilization.lt(this.util0)) { const slope = this.rate0.div(this.util0); - return slope.mul(utilization); + return slope.mul(utilization).mul(scaling); } else if (utilization.lt(this.util1)) { const extraUtil = utilization.sub(this.util0); const slope = this.rate1.sub(this.rate0).div(this.util1.sub(this.util0)); - return this.rate0.add(slope.mul(extraUtil)); + return this.rate0.add(slope.mul(extraUtil)).mul(scaling); } else { const extraUtil = utilization.sub(this.util1); const slope = this.maxRate .sub(this.rate1) .div(I80F48.fromNumber(1).sub(this.util1)); - return this.rate1.add(slope.mul(extraUtil)); + return this.rate1.add(slope.mul(extraUtil)).mul(scaling); } } diff --git a/ts/client/src/accounts/healthCache.spec.ts b/ts/client/src/accounts/healthCache.spec.ts index 3b362b0b0..aafaa7a61 100644 --- a/ts/client/src/accounts/healthCache.spec.ts +++ b/ts/client/src/accounts/healthCache.spec.ts @@ -57,6 +57,10 @@ function mockBankAndOracle( }, nativeDeposits: () => I80F48.fromNumber(deposits), nativeBorrows: () => I80F48.fromNumber(borrows), + maintWeights: () => [ + I80F48.fromNumber(1 - maintWeight), + I80F48.fromNumber(1 + maintWeight), + ], borrowWeightScaleStartQuote: borrowWeightScaleStartQuote, depositWeightScaleStartQuote: depositWeightScaleStartQuote, }; diff --git a/ts/client/src/accounts/healthCache.ts b/ts/client/src/accounts/healthCache.ts index 42b6b899e..2992eeef3 100644 --- a/ts/client/src/accounts/healthCache.ts +++ b/ts/client/src/accounts/healthCache.ts @@ -2,13 +2,7 @@ import { BN } from '@coral-xyz/anchor'; import { OpenOrders } from '@project-serum/serum'; import { PublicKey } from '@solana/web3.js'; import cloneDeep from 'lodash/cloneDeep'; -import { - I80F48, - I80F48Dto, - MAX_I80F48, - ONE_I80F48, - ZERO_I80F48, -} from '../numbers/I80F48'; +import { I80F48, MAX_I80F48, ONE_I80F48, ZERO_I80F48 } from '../numbers/I80F48'; import { toNativeI80F48ForQuote, toUiDecimals, @@ -158,14 +152,6 @@ export class HealthCache { return new HealthCache(tokenInfos, serum3Infos, perpInfos); } - static fromDto(dto): HealthCache { - return new HealthCache( - dto.tokenInfos.map((dto) => TokenInfo.fromDto(dto)), - dto.serum3Infos.map((dto) => Serum3Info.fromDto(dto)), - dto.perpInfos.map((dto) => PerpInfo.fromDto(dto)), - ); - } - computeSerum3Reservations(healthType: HealthType | undefined): { tokenMaxReserved: TokenMaxReserved[]; serum3Reserved: Serum3Reserved[]; @@ -1437,23 +1423,6 @@ export class TokenInfo { public balanceSpot: I80F48, ) {} - static fromDto(dto: TokenInfoDto): TokenInfo { - return new TokenInfo( - dto.tokenIndex as TokenIndex, - I80F48.from(dto.maintAssetWeight), - I80F48.from(dto.initAssetWeight), - I80F48.from(dto.initScaledAssetWeight), - I80F48.from(dto.maintLiabWeight), - I80F48.from(dto.initLiabWeight), - I80F48.from(dto.initScaledLiabWeight), - new Prices( - I80F48.from(dto.prices.oracle), - I80F48.from(dto.prices.stable), - ), - I80F48.from(dto.balanceSpot), - ); - } - static fromBank(bank: BankForHealth, nativeBalance?: I80F48): TokenInfo { const p = new Prices( bank.price, @@ -1462,12 +1431,15 @@ export class TokenInfo { // Use the liab price for computing weight scaling, because it's pessimistic and // causes the most unfavorable scaling. const liabPrice = p.liab(HealthType.init); + + const [maintAssetWeight, maintLiabWeight] = bank.maintWeights(); + return new TokenInfo( bank.tokenIndex, - bank.maintAssetWeight, + maintAssetWeight, bank.initAssetWeight, bank.scaledInitAssetWeight(liabPrice), - bank.maintLiabWeight, + maintLiabWeight, bank.initLiabWeight, bank.scaledInitLiabWeight(liabPrice), p, @@ -1564,18 +1536,6 @@ export class Serum3Info { public marketIndex: MarketIndex, ) {} - static fromDto(dto: Serum3InfoDto): Serum3Info { - return new Serum3Info( - I80F48.from(dto.reservedBase), - I80F48.from(dto.reservedQuote), - I80F48.from(dto.reservedBaseAsQuoteLowestAsk), - I80F48.from(dto.reservedQuoteAsBaseHighestBid), - dto.baseInfoIndex, - dto.quoteInfoIndex, - dto.marketIndex as MarketIndex, - ); - } - static emptyFromSerum3Market( serum3Market: Serum3Market, baseEntryIndex: number, @@ -1756,29 +1716,6 @@ export class PerpInfo { public hasOpenOrders: boolean, ) {} - static fromDto(dto: PerpInfoDto): PerpInfo { - return new PerpInfo( - dto.perpMarketIndex, - dto.settleTokenIndex as TokenIndex, - I80F48.from(dto.maintBaseAssetWeight), - I80F48.from(dto.initBaseAssetWeight), - I80F48.from(dto.maintBaseLiabWeight), - I80F48.from(dto.initBaseLiabWeight), - I80F48.from(dto.maintOverallAssetWeight), - I80F48.from(dto.initOverallAssetWeight), - dto.baseLotSize, - dto.baseLots, - dto.bidsBaseLots, - dto.asksBaseLots, - I80F48.from(dto.quote), - new Prices( - I80F48.from(dto.prices.oracle), - I80F48.from(dto.prices.stable), - ), - dto.hasOpenOrders, - ); - } - static fromPerpPosition( perpMarket: PerpMarket, perpPosition: PerpPosition, @@ -1972,82 +1909,3 @@ export class PerpInfo { )}`; } } - -export class HealthCacheDto { - tokenInfos: TokenInfoDto[]; - serum3Infos: Serum3InfoDto[]; - perpInfos: PerpInfoDto[]; -} -export class TokenInfoDto { - tokenIndex: number; - maintAssetWeight: I80F48Dto; - initAssetWeight: I80F48Dto; - initScaledAssetWeight: I80F48Dto; - maintLiabWeight: I80F48Dto; - initLiabWeight: I80F48Dto; - initScaledLiabWeight: I80F48Dto; - prices: { oracle: I80F48Dto; stable: I80F48Dto }; - balanceSpot: I80F48Dto; - - constructor( - tokenIndex: number, - maintAssetWeight: I80F48Dto, - initAssetWeight: I80F48Dto, - initScaledAssetWeight: I80F48Dto, - maintLiabWeight: I80F48Dto, - initLiabWeight: I80F48Dto, - initScaledLiabWeight: I80F48Dto, - prices: { oracle: I80F48Dto; stable: I80F48Dto }, - balanceNative: I80F48Dto, - ) { - this.tokenIndex = tokenIndex; - this.maintAssetWeight = maintAssetWeight; - this.initAssetWeight = initAssetWeight; - this.initScaledAssetWeight = initScaledAssetWeight; - this.maintLiabWeight = maintLiabWeight; - this.initLiabWeight = initLiabWeight; - this.initScaledLiabWeight = initScaledLiabWeight; - this.prices = prices; - this.balanceSpot = balanceNative; - } -} - -export class Serum3InfoDto { - reservedBase: I80F48Dto; - reservedQuote: I80F48Dto; - reservedBaseAsQuoteLowestAsk: I80F48Dto; - reservedQuoteAsBaseHighestBid: I80F48Dto; - baseInfoIndex: number; - quoteInfoIndex: number; - marketIndex: number; - - constructor( - reservedBase: I80F48Dto, - reservedQuote: I80F48Dto, - baseInfoIndex: number, - quoteInfoIndex: number, - ) { - this.reservedBase = reservedBase; - this.reservedQuote = reservedQuote; - this.baseInfoIndex = baseInfoIndex; - this.quoteInfoIndex = quoteInfoIndex; - } -} - -export class PerpInfoDto { - perpMarketIndex: number; - settleTokenIndex: number; - maintBaseAssetWeight: I80F48Dto; - initBaseAssetWeight: I80F48Dto; - maintBaseLiabWeight: I80F48Dto; - initBaseLiabWeight: I80F48Dto; - maintOverallAssetWeight: I80F48Dto; - initOverallAssetWeight: I80F48Dto; - public baseLotSize: BN; - public baseLots: BN; - public bidsBaseLots: BN; - public asksBaseLots: BN; - quote: I80F48Dto; - prices: { oracle: I80F48Dto; stable: I80F48Dto }; - hasOpenOrders: boolean; -} diff --git a/ts/client/src/client.ts b/ts/client/src/client.ts index d2bf78ef8..88933c062 100644 --- a/ts/client/src/client.ts +++ b/ts/client/src/client.ts @@ -462,6 +462,10 @@ export class MangoClient { params.tokenConditionalSwapTakerFeeRate, params.tokenConditionalSwapMakerFeeRate, params.flashLoanSwapFeeRate, + params.interestCurveScaling, + params.interestTargetUtilization, + params.groupInsuranceFund, + params.depositLimit, ) .accounts({ group: group.publicKey, @@ -537,6 +541,15 @@ export class MangoClient { params.tokenConditionalSwapTakerFeeRate, params.tokenConditionalSwapMakerFeeRate, params.flashLoanSwapFeeRate, + params.interestCurveScaling, + params.interestTargetUtilization, + params.maintWeightShiftStart, + params.maintWeightShiftEnd, + params.maintWeightShiftAssetTarget, + params.maintWeightShiftLiabTarget, + params.maintWeightShiftAbort ?? false, + false, // setFallbackOracle, unused + params.depositLimit, ) .accounts({ group: group.publicKey, @@ -1604,9 +1617,10 @@ export class MangoClient { quoteBank: Bank, marketIndex: number, name: string, + oraclePriceBand: number, ): Promise { const ix = await this.program.methods - .serum3RegisterMarket(marketIndex, name) + .serum3RegisterMarket(marketIndex, name, oraclePriceBand) .accounts({ group: group.publicKey, admin: (this.program.provider as AnchorProvider).wallet.publicKey, @@ -1626,11 +1640,12 @@ export class MangoClient { reduceOnly: boolean | null, forceClose: boolean | null, name: string | null, + oraclePriceBand: number | null, ): Promise { const serum3Market = group.serum3MarketsMapByMarketIndex.get(serum3MarketIndex); const ix = await this.program.methods - .serum3EditMarket(reduceOnly, forceClose, name) + .serum3EditMarket(reduceOnly, forceClose, name, oraclePriceBand) .accounts({ group: group.publicKey, admin: (this.program.provider as AnchorProvider).wallet.publicKey, @@ -1885,6 +1900,32 @@ export class MangoClient { orderType: Serum3OrderType, clientOrderId: number, limit: number, + ): Promise { + return await this.serum3PlaceOrderV2Ix( + group, + mangoAccount, + externalMarketPk, + side, + price, + size, + selfTradeBehavior, + orderType, + clientOrderId, + limit, + ); + } + + public async serum3PlaceOrderV1Ix( + group: Group, + mangoAccount: MangoAccount, + externalMarketPk: PublicKey, + side: Serum3Side, + price: number, + size: number, + selfTradeBehavior: Serum3SelfTradeBehavior, + orderType: Serum3OrderType, + clientOrderId: number, + limit: number, ): Promise { const ixs: TransactionInstruction[] = []; const serum3Market = group.serum3MarketsMapByExternal.get( @@ -1998,6 +2039,143 @@ export class MangoClient { return ixs; } + public async serum3PlaceOrderV2Ix( + group: Group, + mangoAccount: MangoAccount, + externalMarketPk: PublicKey, + side: Serum3Side, + price: number, + size: number, + selfTradeBehavior: Serum3SelfTradeBehavior, + orderType: Serum3OrderType, + clientOrderId: number, + limit: number, + ): Promise { + const ixs: TransactionInstruction[] = []; + const serum3Market = group.serum3MarketsMapByExternal.get( + externalMarketPk.toBase58(), + )!; + + let openOrderPk: PublicKey | undefined = undefined; + const banks: Bank[] = []; + const openOrdersForMarket: [Serum3Market, PublicKey][] = []; + if (!mangoAccount.getSerum3Account(serum3Market.marketIndex)) { + const ix = await this.serum3CreateOpenOrdersIx( + group, + mangoAccount, + serum3Market.serumMarketExternal, + ); + ixs.push(ix); + openOrderPk = await serum3Market.findOoPda( + this.program.programId, + mangoAccount.publicKey, + ); + openOrdersForMarket.push([serum3Market, openOrderPk]); + const baseTokenIndex = serum3Market.baseTokenIndex; + const quoteTokenIndex = serum3Market.quoteTokenIndex; + // only include banks if no deposit has been previously made for same token + banks.push(group.getFirstBankByTokenIndex(quoteTokenIndex)); + banks.push(group.getFirstBankByTokenIndex(baseTokenIndex)); + } + + const healthRemainingAccounts: PublicKey[] = + this.buildHealthRemainingAccounts( + group, + [mangoAccount], + banks, + [], + openOrdersForMarket, + ); + + const serum3MarketExternal = group.serum3ExternalMarketsMap.get( + externalMarketPk.toBase58(), + )!; + const serum3MarketExternalVaultSigner = + await generateSerum3MarketExternalVaultSignerAddress( + this.cluster, + serum3Market, + serum3MarketExternal, + ); + + const limitPrice = serum3MarketExternal.priceNumberToLots(price); + const maxBaseQuantity = serum3MarketExternal.baseSizeNumberToLots(size); + const isTaker = orderType !== Serum3OrderType.postOnly; + const maxQuoteQuantity = new BN( + Math.ceil( + serum3MarketExternal.decoded.quoteLotSize.toNumber() * + (1 + Math.max(serum3Market.getFeeRates(isTaker), 0)) * + serum3MarketExternal.baseSizeNumberToLots(size).toNumber() * + serum3MarketExternal.priceNumberToLots(price).toNumber(), + ), + ); + + const payerTokenIndex = ((): TokenIndex => { + if (side == Serum3Side.bid) { + return serum3Market.quoteTokenIndex; + } else { + return serum3Market.baseTokenIndex; + } + })(); + + const receiverTokenIndex = ((): TokenIndex => { + if (side == Serum3Side.bid) { + return serum3Market.baseTokenIndex; + } else { + return serum3Market.quoteTokenIndex; + } + })(); + + const payerBank = group.getFirstBankByTokenIndex(payerTokenIndex); + const receiverBank = group.getFirstBankByTokenIndex(receiverTokenIndex); + const ix = await this.program.methods + .serum3PlaceOrderV2( + side, + limitPrice, + maxBaseQuantity, + maxQuoteQuantity, + selfTradeBehavior, + orderType, + new BN(clientOrderId), + limit, + ) + .accounts({ + group: group.publicKey, + account: mangoAccount.publicKey, + owner: (this.program.provider as AnchorProvider).wallet.publicKey, + openOrders: + openOrderPk || + mangoAccount.getSerum3Account(serum3Market.marketIndex)?.openOrders, + serumMarket: serum3Market.publicKey, + serumProgram: OPENBOOK_PROGRAM_ID[this.cluster], + serumMarketExternal: serum3Market.serumMarketExternal, + marketBids: serum3MarketExternal.bidsAddress, + marketAsks: serum3MarketExternal.asksAddress, + marketEventQueue: serum3MarketExternal.decoded.eventQueue, + marketRequestQueue: serum3MarketExternal.decoded.requestQueue, + marketBaseVault: serum3MarketExternal.decoded.baseVault, + marketQuoteVault: serum3MarketExternal.decoded.quoteVault, + marketVaultSigner: serum3MarketExternalVaultSigner, + payerBank: payerBank.publicKey, + payerVault: payerBank.vault, + payerOracle: payerBank.oracle, + }) + .remainingAccounts( + healthRemainingAccounts.map( + (pk) => + ({ + pubkey: pk, + isWritable: receiverBank.publicKey.equals(pk) ? true : false, + isSigner: false, + } as AccountMeta), + ), + ) + .instruction(); + + ixs.push(ix); + + return ixs; + } + public async serum3PlaceOrder( group: Group, mangoAccount: MangoAccount, @@ -2010,7 +2188,7 @@ export class MangoClient { clientOrderId: number, limit: number, ): Promise { - const placeOrderIxs = await this.serum3PlaceOrderIx( + const placeOrderIxs = await this.serum3PlaceOrderV2Ix( group, mangoAccount, externalMarketPk, @@ -4875,7 +5053,7 @@ export class MangoClient { orderId, ), this.serum3SettleFundsV2Ix(group, mangoAccount, externalMarketPk), - this.serum3PlaceOrderIx( + this.serum3PlaceOrderV2Ix( group, mangoAccount, externalMarketPk, diff --git a/ts/client/src/clientIxParamBuilder.ts b/ts/client/src/clientIxParamBuilder.ts index d49c3c76d..17f70963f 100644 --- a/ts/client/src/clientIxParamBuilder.ts +++ b/ts/client/src/clientIxParamBuilder.ts @@ -25,6 +25,9 @@ export interface TokenRegisterParams { tokenConditionalSwapTakerFeeRate: number; tokenConditionalSwapMakerFeeRate: number; flashLoanSwapFeeRate: number; + interestCurveScaling: number; + interestTargetUtilization: number; + depositLimit: BN; } export const DefaultTokenRegisterParams: TokenRegisterParams = { @@ -35,10 +38,10 @@ export const DefaultTokenRegisterParams: TokenRegisterParams = { groupInsuranceFund: false, interestRateParams: { util0: 0.5, - rate0: 0.072, + rate0: 0.018, util1: 0.8, - rate1: 0.2, - maxRate: 2, + rate1: 0.05, + maxRate: 0.5, adjustmentFactor: 0.004, }, loanFeeRate: 0.0005, @@ -60,6 +63,9 @@ export const DefaultTokenRegisterParams: TokenRegisterParams = { tokenConditionalSwapTakerFeeRate: 0.0005, tokenConditionalSwapMakerFeeRate: 0.0005, flashLoanSwapFeeRate: 0.0005, + interestCurveScaling: 4.0, + interestTargetUtilization: 0.5, + depositLimit: new BN(0), }; export interface TokenEditParams { @@ -90,6 +96,14 @@ export interface TokenEditParams { tokenConditionalSwapTakerFeeRate: number | null; tokenConditionalSwapMakerFeeRate: number | null; flashLoanSwapFeeRate: number | null; + interestCurveScaling: number | null; + interestTargetUtilization: number | null; + maintWeightShiftStart: BN | null; + maintWeightShiftEnd: BN | null; + maintWeightShiftAssetTarget: number | null; + maintWeightShiftLiabTarget: number | null; + maintWeightShiftAbort: boolean | null; + depositLimit: BN | null; } export const NullTokenEditParams: TokenEditParams = { @@ -120,6 +134,14 @@ export const NullTokenEditParams: TokenEditParams = { tokenConditionalSwapTakerFeeRate: null, tokenConditionalSwapMakerFeeRate: null, flashLoanSwapFeeRate: null, + interestCurveScaling: null, + interestTargetUtilization: null, + maintWeightShiftStart: null, + maintWeightShiftEnd: null, + maintWeightShiftAssetTarget: null, + maintWeightShiftLiabTarget: null, + maintWeightShiftAbort: null, + depositLimit: null, }; export interface PerpEditParams { @@ -264,6 +286,7 @@ export interface IxGateParams { TokenConditionalSwapStart: boolean; TokenConditionalSwapCreatePremiumAuction: boolean; TokenConditionalSwapCreateLinearAuction: boolean; + Serum3PlaceOrderV2: boolean; } // Default with all ixs enabled, use with buildIxGate @@ -342,6 +365,7 @@ export const TrueIxGateParams: IxGateParams = { TokenConditionalSwapStart: true, TokenConditionalSwapCreatePremiumAuction: true, TokenConditionalSwapCreateLinearAuction: true, + Serum3PlaceOrderV2: true, }; // build ix gate e.g. buildIxGate(Builder(TrueIxGateParams).TokenDeposit(false).build()).toNumber(), @@ -430,6 +454,7 @@ export function buildIxGate(p: IxGateParams): BN { toggleIx(ixGate, p, 'TokenConditionalSwapStart', 68); toggleIx(ixGate, p, 'TokenConditionalSwapCreatePremiumAuction', 69); toggleIx(ixGate, p, 'TokenConditionalSwapCreateLinearAuction', 70); + toggleIx(ixGate, p, 'Serum3PlaceOrderV2', 71); return ixGate; } diff --git a/ts/client/src/mango_v4.ts b/ts/client/src/mango_v4.ts index aeef85028..861365d02 100644 --- a/ts/client/src/mango_v4.ts +++ b/ts/client/src/mango_v4.ts @@ -1,5 +1,5 @@ export type MangoV4 = { - "version": "0.20.0", + "version": "0.21.0", "name": "mango_v4", "instructions": [ { @@ -602,6 +602,22 @@ export type MangoV4 = { { "name": "flashLoanSwapFeeRate", "type": "f32" + }, + { + "name": "interestCurveScaling", + "type": "f32" + }, + { + "name": "interestTargetUtilization", + "type": "f32" + }, + { + "name": "groupInsuranceFund", + "type": "bool" + }, + { + "name": "depositLimit", + "type": "u64" } ] }, @@ -936,6 +952,56 @@ export type MangoV4 = { "type": { "option": "f32" } + }, + { + "name": "interestCurveScalingOpt", + "type": { + "option": "f32" + } + }, + { + "name": "interestTargetUtilizationOpt", + "type": { + "option": "f32" + } + }, + { + "name": "maintWeightShiftStartOpt", + "type": { + "option": "u64" + } + }, + { + "name": "maintWeightShiftEndOpt", + "type": { + "option": "u64" + } + }, + { + "name": "maintWeightShiftAssetTargetOpt", + "type": { + "option": "f32" + } + }, + { + "name": "maintWeightShiftLiabTargetOpt", + "type": { + "option": "f32" + } + }, + { + "name": "maintWeightShiftAbort", + "type": "bool" + }, + { + "name": "setFallbackOracle", + "type": "bool" + }, + { + "name": "depositLimitOpt", + "type": { + "option": "u64" + } } ] }, @@ -1817,8 +1883,7 @@ export type MangoV4 = { "isMut": true, "isSigner": false, "relations": [ - "group", - "owner" + "group" ] }, { @@ -2306,6 +2371,10 @@ export type MangoV4 = { { "name": "name", "type": "string" + }, + { + "name": "oraclePriceBand", + "type": "f32" } ] }, @@ -2349,6 +2418,12 @@ export type MangoV4 = { "type": { "option": "string" } + }, + { + "name": "oraclePriceBandOpt", + "type": { + "option": "f32" + } } ] }, @@ -2689,6 +2764,164 @@ export type MangoV4 = { } ] }, + { + "name": "serum3PlaceOrderV2", + "docs": [ + "requires the receiver_bank in the health account list to be writable" + ], + "accounts": [ + { + "name": "group", + "isMut": false, + "isSigner": false + }, + { + "name": "account", + "isMut": true, + "isSigner": false, + "relations": [ + "group" + ] + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "openOrders", + "isMut": true, + "isSigner": false + }, + { + "name": "serumMarket", + "isMut": false, + "isSigner": false, + "relations": [ + "group", + "serum_program", + "serum_market_external" + ] + }, + { + "name": "serumProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "serumMarketExternal", + "isMut": true, + "isSigner": false + }, + { + "name": "marketBids", + "isMut": true, + "isSigner": false + }, + { + "name": "marketAsks", + "isMut": true, + "isSigner": false + }, + { + "name": "marketEventQueue", + "isMut": true, + "isSigner": false + }, + { + "name": "marketRequestQueue", + "isMut": true, + "isSigner": false + }, + { + "name": "marketBaseVault", + "isMut": true, + "isSigner": false + }, + { + "name": "marketQuoteVault", + "isMut": true, + "isSigner": false + }, + { + "name": "marketVaultSigner", + "isMut": false, + "isSigner": false, + "docs": [ + "needed for the automatic settle_funds call" + ] + }, + { + "name": "payerBank", + "isMut": true, + "isSigner": false, + "docs": [ + "The bank that pays for the order, if necessary" + ], + "relations": [ + "group" + ] + }, + { + "name": "payerVault", + "isMut": true, + "isSigner": false, + "docs": [ + "The bank vault that pays for the order, if necessary" + ] + }, + { + "name": "payerOracle", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "side", + "type": { + "defined": "Serum3Side" + } + }, + { + "name": "limitPrice", + "type": "u64" + }, + { + "name": "maxBaseQty", + "type": "u64" + }, + { + "name": "maxNativeQuoteQtyIncludingFees", + "type": "u64" + }, + { + "name": "selfTradeBehavior", + "type": { + "defined": "Serum3SelfTradeBehavior" + } + }, + { + "name": "orderType", + "type": { + "defined": "Serum3OrderType" + } + }, + { + "name": "clientOrderId", + "type": "u64" + }, + { + "name": "limit", + "type": "u16" + } + ] + }, { "name": "serum3CancelOrder", "accounts": [ @@ -7057,15 +7290,56 @@ export type MangoV4 = { "type": "f64" }, { - "name": "depositsInSerum", - "type": "i64" + "name": "potentialSerumTokens", + "docs": [ + "Largest amount of tokens that might be added the the bank based on", + "serum open order execution." + ], + "type": "u64" + }, + { + "name": "maintWeightShiftStart", + "type": "u64" + }, + { + "name": "maintWeightShiftEnd", + "type": "u64" + }, + { + "name": "maintWeightShiftDurationInv", + "type": { + "defined": "I80F48" + } + }, + { + "name": "maintWeightShiftAssetTarget", + "type": { + "defined": "I80F48" + } + }, + { + "name": "maintWeightShiftLiabTarget", + "type": { + "defined": "I80F48" + } + }, + { + "name": "fallbackOracle", + "type": "publicKey" + }, + { + "name": "depositLimit", + "docs": [ + "zero means none, in token native" + ], + "type": "u64" }, { "name": "reserved", "type": { "array": [ "u8", - 2072 + 1968 ] } } @@ -7990,7 +8264,7 @@ export type MangoV4 = { "name": "settleFeeFlat", "docs": [ "In native units of settlement token, given to each settle call above the", - "settle_fee_amount_threshold." + "settle_fee_amount_threshold if settling at least 1% of perp base pos value." ], "type": "f32" }, @@ -8004,7 +8278,8 @@ export type MangoV4 = { { "name": "settleFeeFractionLowHealth", "docs": [ - "Fraction of pnl to pay out as fee if +pnl account has low health." + "Fraction of pnl to pay out as fee if +pnl account has low health.", + "(limited to 2x settle_fee_flat)" ], "type": "f32" }, @@ -8163,10 +8438,20 @@ export type MangoV4 = { "type": { "array": [ "u8", - 5 + 1 ] } }, + { + "name": "oraclePriceBand", + "docs": [ + "Limit orders must be <= oracle * (1+band) and >= oracle / (1+band)", + "", + "Zero value is the default due to migration and disables the limit,", + "same as f32::MAX." + ], + "type": "f32" + }, { "name": "registrationTime", "type": "u64" @@ -8602,26 +8887,41 @@ export type MangoV4 = { "type": "f64" }, { - "name": "baseDepositsReserved", + "name": "potentialBaseTokens", "docs": [ - "Tracks the amount of deposits that flowed into the serum open orders account.", + "An overestimate of the amount of tokens that might flow out of the open orders account.", "", - "The bank still considers these amounts user deposits (see deposits_in_serum)", - "and they need to be deducted from there when they flow back into the bank", - "as real tokens." + "The bank still considers these amounts user deposits (see Bank::potential_serum_tokens)", + "and that value needs to be updated in conjunction with these numbers.", + "", + "This estimation is based on the amount of tokens in the open orders account", + "(see update_bank_potential_tokens() in serum3_place_order and settle)" ], "type": "u64" }, { - "name": "quoteDepositsReserved", + "name": "potentialQuoteTokens", "type": "u64" }, + { + "name": "lowestPlacedBidInv", + "docs": [ + "Track lowest bid/highest ask, same way as for highest bid/lowest ask.", + "", + "0 is a special \"unset\" state." + ], + "type": "f64" + }, + { + "name": "highestPlacedAsk", + "type": "f64" + }, { "name": "reserved", "type": { "array": [ "u8", - 32 + 16 ] } } @@ -10284,6 +10584,9 @@ export type MangoV4 = { }, { "name": "TokenConditionalSwapCreateLinearAuction" + }, + { + "name": "Serum3PlaceOrderV2" } ] } @@ -11393,6 +11696,56 @@ export type MangoV4 = { } ] }, + { + "name": "UpdateRateLogV2", + "fields": [ + { + "name": "mangoGroup", + "type": "publicKey", + "index": false + }, + { + "name": "tokenIndex", + "type": "u16", + "index": false + }, + { + "name": "rate0", + "type": "i128", + "index": false + }, + { + "name": "util0", + "type": "i128", + "index": false + }, + { + "name": "rate1", + "type": "i128", + "index": false + }, + { + "name": "util1", + "type": "i128", + "index": false + }, + { + "name": "maxRate", + "type": "i128", + "index": false + }, + { + "name": "curveScaling", + "type": "f64", + "index": false + }, + { + "name": "targetUtilization", + "type": "f32", + "index": false + } + ] + }, { "name": "TokenLiqWithTokenLog", "fields": [ @@ -13155,12 +13508,27 @@ export type MangoV4 = { "code": 6059, "name": "TokenConditionalSwapTypeNotStartable", "msg": "token conditional swap type cannot be started" + }, + { + "code": 6060, + "name": "HealthAccountBankNotWritable", + "msg": "a bank in the health account list should be writable but is not" + }, + { + "code": 6061, + "name": "Serum3PriceBandExceeded", + "msg": "the market does not allow limit orders too far from the current oracle value" + }, + { + "code": 6062, + "name": "BankDepositLimit", + "msg": "deposit crosses the token's deposit limit" } ] }; export const IDL: MangoV4 = { - "version": "0.20.0", + "version": "0.21.0", "name": "mango_v4", "instructions": [ { @@ -13763,6 +14131,22 @@ export const IDL: MangoV4 = { { "name": "flashLoanSwapFeeRate", "type": "f32" + }, + { + "name": "interestCurveScaling", + "type": "f32" + }, + { + "name": "interestTargetUtilization", + "type": "f32" + }, + { + "name": "groupInsuranceFund", + "type": "bool" + }, + { + "name": "depositLimit", + "type": "u64" } ] }, @@ -14097,6 +14481,56 @@ export const IDL: MangoV4 = { "type": { "option": "f32" } + }, + { + "name": "interestCurveScalingOpt", + "type": { + "option": "f32" + } + }, + { + "name": "interestTargetUtilizationOpt", + "type": { + "option": "f32" + } + }, + { + "name": "maintWeightShiftStartOpt", + "type": { + "option": "u64" + } + }, + { + "name": "maintWeightShiftEndOpt", + "type": { + "option": "u64" + } + }, + { + "name": "maintWeightShiftAssetTargetOpt", + "type": { + "option": "f32" + } + }, + { + "name": "maintWeightShiftLiabTargetOpt", + "type": { + "option": "f32" + } + }, + { + "name": "maintWeightShiftAbort", + "type": "bool" + }, + { + "name": "setFallbackOracle", + "type": "bool" + }, + { + "name": "depositLimitOpt", + "type": { + "option": "u64" + } } ] }, @@ -14978,8 +15412,7 @@ export const IDL: MangoV4 = { "isMut": true, "isSigner": false, "relations": [ - "group", - "owner" + "group" ] }, { @@ -15467,6 +15900,10 @@ export const IDL: MangoV4 = { { "name": "name", "type": "string" + }, + { + "name": "oraclePriceBand", + "type": "f32" } ] }, @@ -15510,6 +15947,12 @@ export const IDL: MangoV4 = { "type": { "option": "string" } + }, + { + "name": "oraclePriceBandOpt", + "type": { + "option": "f32" + } } ] }, @@ -15850,6 +16293,164 @@ export const IDL: MangoV4 = { } ] }, + { + "name": "serum3PlaceOrderV2", + "docs": [ + "requires the receiver_bank in the health account list to be writable" + ], + "accounts": [ + { + "name": "group", + "isMut": false, + "isSigner": false + }, + { + "name": "account", + "isMut": true, + "isSigner": false, + "relations": [ + "group" + ] + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "openOrders", + "isMut": true, + "isSigner": false + }, + { + "name": "serumMarket", + "isMut": false, + "isSigner": false, + "relations": [ + "group", + "serum_program", + "serum_market_external" + ] + }, + { + "name": "serumProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "serumMarketExternal", + "isMut": true, + "isSigner": false + }, + { + "name": "marketBids", + "isMut": true, + "isSigner": false + }, + { + "name": "marketAsks", + "isMut": true, + "isSigner": false + }, + { + "name": "marketEventQueue", + "isMut": true, + "isSigner": false + }, + { + "name": "marketRequestQueue", + "isMut": true, + "isSigner": false + }, + { + "name": "marketBaseVault", + "isMut": true, + "isSigner": false + }, + { + "name": "marketQuoteVault", + "isMut": true, + "isSigner": false + }, + { + "name": "marketVaultSigner", + "isMut": false, + "isSigner": false, + "docs": [ + "needed for the automatic settle_funds call" + ] + }, + { + "name": "payerBank", + "isMut": true, + "isSigner": false, + "docs": [ + "The bank that pays for the order, if necessary" + ], + "relations": [ + "group" + ] + }, + { + "name": "payerVault", + "isMut": true, + "isSigner": false, + "docs": [ + "The bank vault that pays for the order, if necessary" + ] + }, + { + "name": "payerOracle", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "side", + "type": { + "defined": "Serum3Side" + } + }, + { + "name": "limitPrice", + "type": "u64" + }, + { + "name": "maxBaseQty", + "type": "u64" + }, + { + "name": "maxNativeQuoteQtyIncludingFees", + "type": "u64" + }, + { + "name": "selfTradeBehavior", + "type": { + "defined": "Serum3SelfTradeBehavior" + } + }, + { + "name": "orderType", + "type": { + "defined": "Serum3OrderType" + } + }, + { + "name": "clientOrderId", + "type": "u64" + }, + { + "name": "limit", + "type": "u16" + } + ] + }, { "name": "serum3CancelOrder", "accounts": [ @@ -20218,15 +20819,56 @@ export const IDL: MangoV4 = { "type": "f64" }, { - "name": "depositsInSerum", - "type": "i64" + "name": "potentialSerumTokens", + "docs": [ + "Largest amount of tokens that might be added the the bank based on", + "serum open order execution." + ], + "type": "u64" + }, + { + "name": "maintWeightShiftStart", + "type": "u64" + }, + { + "name": "maintWeightShiftEnd", + "type": "u64" + }, + { + "name": "maintWeightShiftDurationInv", + "type": { + "defined": "I80F48" + } + }, + { + "name": "maintWeightShiftAssetTarget", + "type": { + "defined": "I80F48" + } + }, + { + "name": "maintWeightShiftLiabTarget", + "type": { + "defined": "I80F48" + } + }, + { + "name": "fallbackOracle", + "type": "publicKey" + }, + { + "name": "depositLimit", + "docs": [ + "zero means none, in token native" + ], + "type": "u64" }, { "name": "reserved", "type": { "array": [ "u8", - 2072 + 1968 ] } } @@ -21151,7 +21793,7 @@ export const IDL: MangoV4 = { "name": "settleFeeFlat", "docs": [ "In native units of settlement token, given to each settle call above the", - "settle_fee_amount_threshold." + "settle_fee_amount_threshold if settling at least 1% of perp base pos value." ], "type": "f32" }, @@ -21165,7 +21807,8 @@ export const IDL: MangoV4 = { { "name": "settleFeeFractionLowHealth", "docs": [ - "Fraction of pnl to pay out as fee if +pnl account has low health." + "Fraction of pnl to pay out as fee if +pnl account has low health.", + "(limited to 2x settle_fee_flat)" ], "type": "f32" }, @@ -21324,10 +21967,20 @@ export const IDL: MangoV4 = { "type": { "array": [ "u8", - 5 + 1 ] } }, + { + "name": "oraclePriceBand", + "docs": [ + "Limit orders must be <= oracle * (1+band) and >= oracle / (1+band)", + "", + "Zero value is the default due to migration and disables the limit,", + "same as f32::MAX." + ], + "type": "f32" + }, { "name": "registrationTime", "type": "u64" @@ -21763,26 +22416,41 @@ export const IDL: MangoV4 = { "type": "f64" }, { - "name": "baseDepositsReserved", + "name": "potentialBaseTokens", "docs": [ - "Tracks the amount of deposits that flowed into the serum open orders account.", + "An overestimate of the amount of tokens that might flow out of the open orders account.", "", - "The bank still considers these amounts user deposits (see deposits_in_serum)", - "and they need to be deducted from there when they flow back into the bank", - "as real tokens." + "The bank still considers these amounts user deposits (see Bank::potential_serum_tokens)", + "and that value needs to be updated in conjunction with these numbers.", + "", + "This estimation is based on the amount of tokens in the open orders account", + "(see update_bank_potential_tokens() in serum3_place_order and settle)" ], "type": "u64" }, { - "name": "quoteDepositsReserved", + "name": "potentialQuoteTokens", "type": "u64" }, + { + "name": "lowestPlacedBidInv", + "docs": [ + "Track lowest bid/highest ask, same way as for highest bid/lowest ask.", + "", + "0 is a special \"unset\" state." + ], + "type": "f64" + }, + { + "name": "highestPlacedAsk", + "type": "f64" + }, { "name": "reserved", "type": { "array": [ "u8", - 32 + 16 ] } } @@ -23445,6 +24113,9 @@ export const IDL: MangoV4 = { }, { "name": "TokenConditionalSwapCreateLinearAuction" + }, + { + "name": "Serum3PlaceOrderV2" } ] } @@ -24554,6 +25225,56 @@ export const IDL: MangoV4 = { } ] }, + { + "name": "UpdateRateLogV2", + "fields": [ + { + "name": "mangoGroup", + "type": "publicKey", + "index": false + }, + { + "name": "tokenIndex", + "type": "u16", + "index": false + }, + { + "name": "rate0", + "type": "i128", + "index": false + }, + { + "name": "util0", + "type": "i128", + "index": false + }, + { + "name": "rate1", + "type": "i128", + "index": false + }, + { + "name": "util1", + "type": "i128", + "index": false + }, + { + "name": "maxRate", + "type": "i128", + "index": false + }, + { + "name": "curveScaling", + "type": "f64", + "index": false + }, + { + "name": "targetUtilization", + "type": "f32", + "index": false + } + ] + }, { "name": "TokenLiqWithTokenLog", "fields": [ @@ -26316,6 +27037,21 @@ export const IDL: MangoV4 = { "code": 6059, "name": "TokenConditionalSwapTypeNotStartable", "msg": "token conditional swap type cannot be started" + }, + { + "code": 6060, + "name": "HealthAccountBankNotWritable", + "msg": "a bank in the health account list should be writable but is not" + }, + { + "code": 6061, + "name": "Serum3PriceBandExceeded", + "msg": "the market does not allow limit orders too far from the current oracle value" + }, + { + "code": 6062, + "name": "BankDepositLimit", + "msg": "deposit crosses the token's deposit limit" } ] };