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 a0266a0dc..518c523cc 100644 --- a/mango_v4.json +++ b/mango_v4.json @@ -956,6 +956,34 @@ "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" } ] }, @@ -7079,12 +7107,38 @@ "name": "depositsInSerum", "type": "i64" }, + { + "name": "maintWeightShiftStart", + "type": "u64" + }, + { + "name": "maintWeightShiftEnd", + "type": "u64" + }, + { + "name": "maintWeightShiftDurationInv", + "type": { + "defined": "I80F48" + } + }, + { + "name": "maintWeightShiftAssetTarget", + "type": { + "defined": "I80F48" + } + }, + { + "name": "maintWeightShiftLiabTarget", + "type": { + "defined": "I80F48" + } + }, { "name": "reserved", "type": { "array": [ "u8", - 2072 + 2008 ] } } diff --git a/programs/mango-v4/src/health/cache.rs b/programs/mango-v4/src/health/cache.rs index 9f132cdba..2aa6ac5e6 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,13 +1236,15 @@ 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? @@ -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, @@ -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 )); } @@ -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 )); } diff --git a/programs/mango-v4/src/health/client.rs b/programs/mango-v4/src/health/client.rs index 867c3e5af..d4dba0447 100644 --- a/programs/mango-v4/src/health/client.rs +++ b/programs/mango-v4/src/health/client.rs @@ -1264,7 +1264,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 +1353,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/compute_account_data.rs b/programs/mango-v4/src/instructions/compute_account_data.rs index 4d4583687..3317679e2 100644 --- a/programs/mango-v4/src/instructions/compute_account_data.rs +++ b/programs/mango-v4/src/instructions/compute_account_data.rs @@ -13,7 +13,8 @@ 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); diff --git a/programs/mango-v4/src/instructions/flash_loan.rs b/programs/mango-v4/src/instructions/flash_loan.rs index 675cf4f2f..30cae5554 100644 --- a/programs/mango-v4/src/instructions/flash_loan.rs +++ b/programs/mango-v4/src/instructions/flash_loan.rs @@ -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 @@ -500,7 +501,7 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>( // 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..7400f3a92 100644 --- a/programs/mango-v4/src/instructions/health_region.rs +++ b/programs/mango-v4/src/instructions/health_region.rs @@ -87,7 +87,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 +108,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/perp_liq_base_or_positive_pnl.rs b/programs/mango-v4/src/instructions/perp_liq_base_or_positive_pnl.rs index 64084a348..c7bc98425 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 @@ -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); // @@ -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..2fb174d65 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 @@ -71,7 +71,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); @@ -197,8 +197,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); } @@ -509,7 +514,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..dd69b56db 100644 --- a/programs/mango-v4/src/instructions/perp_settle_fees.rs +++ b/programs/mango-v4/src/instructions/perp_settle_fees.rs @@ -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..024f38207 100644 --- a/programs/mango-v4/src/instructions/perp_settle_pnl.rs +++ b/programs/mango-v4/src/instructions/perp_settle_pnl.rs @@ -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); 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..8d9a96f36 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 @@ -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() diff --git a/programs/mango-v4/src/instructions/serum3_place_order.rs b/programs/mango-v4/src/instructions/serum3_place_order.rs index fbadf0927..a12cf1c4f 100644 --- a/programs/mango-v4/src/instructions/serum3_place_order.rs +++ b/programs/mango-v4/src/instructions/serum3_place_order.rs @@ -76,8 +76,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) 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..b57dbe2d8 100644 --- a/programs/mango-v4/src/instructions/token_conditional_swap_start.rs +++ b/programs/mango-v4/src/instructions/token_conditional_swap_start.rs @@ -44,7 +44,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)?; 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..4bb849e2e 100644 --- a/programs/mango-v4/src/instructions/token_conditional_swap_trigger.rs +++ b/programs/mango-v4/src/instructions/token_conditional_swap_trigger.rs @@ -87,7 +87,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 +118,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(()) @@ -797,7 +802,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..0275857e1 100644 --- a/programs/mango-v4/src/instructions/token_deposit.rs +++ b/programs/mango-v4/src/instructions/token_deposit.rs @@ -114,11 +114,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. diff --git a/programs/mango-v4/src/instructions/token_edit.rs b/programs/mango-v4/src/instructions/token_edit.rs index ee501abf8..e9e81d2a6 100644 --- a/programs/mango-v4/src/instructions/token_edit.rs +++ b/programs/mango-v4/src/instructions/token_edit.rs @@ -44,6 +44,11 @@ pub fn token_edit( 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, ) -> Result<()> { let group = ctx.accounts.group.load()?; @@ -362,6 +367,75 @@ pub fn token_edit( 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(), + ); + } } // account constraint #1 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..21a354296 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 @@ -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,8 +212,13 @@ 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 diff --git a/programs/mango-v4/src/instructions/token_liq_bankruptcy.rs b/programs/mango-v4/src/instructions/token_liq_bankruptcy.rs index 37a0ff3a6..990df14e5 100644 --- a/programs/mango-v4/src/instructions/token_liq_bankruptcy.rs +++ b/programs/mango-v4/src/instructions/token_liq_bankruptcy.rs @@ -42,9 +42,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 +106,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 @@ -165,8 +164,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); } 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..a516b44b6 100644 --- a/programs/mango-v4/src/instructions/token_liq_with_token.rs +++ b/programs/mango-v4/src/instructions/token_liq_with_token.rs @@ -20,6 +20,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 +40,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 +53,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 +69,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); } @@ -446,7 +451,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 +473,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 ee9ed1226..93ccd674c 100644 --- a/programs/mango-v4/src/instructions/token_register.rs +++ b/programs/mango-v4/src/instructions/token_register.rs @@ -113,7 +113,12 @@ pub fn token_register( interest_target_utilization, interest_curve_scaling: interest_curve_scaling.into(), deposits_in_serum: 0, - reserved: [0; 2072], + 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, + reserved: [0; 2008], }; if let Ok(oracle_price) = diff --git a/programs/mango-v4/src/instructions/token_register_trustless.rs b/programs/mango-v4/src/instructions/token_register_trustless.rs index 75f7177d3..60e28765f 100644 --- a/programs/mango-v4/src/instructions/token_register_trustless.rs +++ b/programs/mango-v4/src/instructions/token_register_trustless.rs @@ -97,7 +97,12 @@ pub fn token_register_trustless( interest_target_utilization: 0.5, interest_curve_scaling: 4.0, deposits_in_serum: 0, - reserved: [0; 2072], + 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, + reserved: [0; 2008], }; if let Ok(oracle_price) = 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 16ca665df..199f5853f 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 @@ -99,6 +99,11 @@ 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; + // 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!(UpdateIndexLog { mango_group: mint_info.group.key(), token_index: mint_info.token_index, @@ -135,6 +140,16 @@ 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; + } } } diff --git a/programs/mango-v4/src/instructions/token_withdraw.rs b/programs/mango-v4/src/instructions/token_withdraw.rs index 196dba333..8af16db00 100644 --- a/programs/mango-v4/src/instructions/token_withdraw.rs +++ b/programs/mango-v4/src/instructions/token_withdraw.rs @@ -14,6 +14,7 @@ pub fn token_withdraw(ctx: Context, amount: u64, allow_borrow: bo 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 +24,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) @@ -132,8 +133,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)?; diff --git a/programs/mango-v4/src/lib.rs b/programs/mango-v4/src/lib.rs index 154f62508..5e8591fd4 100644 --- a/programs/mango-v4/src/lib.rs +++ b/programs/mango-v4/src/lib.rs @@ -227,6 +227,11 @@ pub mod mango_v4 { 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, ) -> Result<()> { #[cfg(feature = "enable-gpl")] instructions::token_edit( @@ -260,6 +265,11 @@ pub mod mango_v4 { 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, )?; Ok(()) } diff --git a/programs/mango-v4/src/state/bank.rs b/programs/mango-v4/src/state/bank.rs index dcb176a71..7b66a6fa7 100644 --- a/programs/mango-v4/src/state/bank.rs +++ b/programs/mango-v4/src/state/bank.rs @@ -161,8 +161,14 @@ pub struct Bank { // can be negative due to multibank, then it'd need to be balanced in the keeper pub deposits_in_serum: i64, + 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, + #[derivative(Debug = "ignore")] - pub reserved: [u8; 2072], + pub reserved: [u8; 2008], } const_assert_eq!( size_of::(), @@ -195,7 +201,9 @@ const_assert_eq!( + 8 + 4 * 4 + 8 * 2 - + 2072 + + 8 * 2 + + 16 * 3 + + 2008 ); const_assert_eq!(size_of::(), 3064); const_assert_eq!(size_of::() % 8, 0); @@ -279,7 +287,12 @@ 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, + reserved: [0; 2008], } } @@ -307,6 +320,9 @@ impl Bank { 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(()) } @@ -338,6 +354,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<()> { @@ -1184,4 +1220,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/tests/cases/test_basic.rs b/programs/mango-v4/tests/cases/test_basic.rs index 0f58ce0c8..6df657ec9 100644 --- a/programs/mango-v4/tests/cases/test_basic.rs +++ b/programs/mango-v4/tests/cases/test_basic.rs @@ -414,3 +414,100 @@ 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(()) +} 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/program_test/mango_client.rs b/programs/mango-v4/tests/program_test/mango_client.rs index 848e9990e..2dde0116e 100644 --- a/programs/mango-v4/tests/program_test/mango_client.rs +++ b/programs/mango-v4/tests/program_test/mango_client.rs @@ -386,6 +386,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) { @@ -1245,6 +1256,11 @@ pub fn token_edit_instruction_default() -> mango_v4::instruction::TokenEdit { 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, } } diff --git a/ts/client/src/accounts/bank.ts b/ts/client/src/accounts/bank.ts index 9adf06234..6b491a773 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, @@ -126,6 +130,14 @@ export class Bank implements BankForHealth { tokenConditionalSwapTakerFeeRate: number; tokenConditionalSwapMakerFeeRate: number; flashLoanSwapFeeRate: number; + interestTargetUtilization: number; + interestCurveScaling: number; + depositsInSerum: BN; + maintWeightShiftStart: BN; + maintWeightShiftEnd: BN; + maintWeightShiftDurationInv: I80F48Dto; + maintWeightShiftAssetTarget: I80F48Dto; + maintWeightShiftLiabTarget: I80F48Dto; }, ): Bank { return new Bank( @@ -177,6 +189,14 @@ export class Bank implements BankForHealth { obj.tokenConditionalSwapTakerFeeRate, obj.tokenConditionalSwapMakerFeeRate, obj.flashLoanSwapFeeRate, + obj.interestTargetUtilization, + obj.interestCurveScaling, + obj.depositsInSerum, + obj.maintWeightShiftStart, + obj.maintWeightShiftEnd, + obj.maintWeightShiftDurationInv, + obj.maintWeightShiftAssetTarget, + obj.maintWeightShiftLiabTarget, ); } @@ -229,6 +249,14 @@ export class Bank implements BankForHealth { public tokenConditionalSwapTakerFeeRate: number, public tokenConditionalSwapMakerFeeRate: number, public flashLoanSwapFeeRate: number, + public interestTargetUtilization: number, + public interestCurveScaling: number, + public depositsInSerum: BN, + public maintWeightShiftStart: BN, + public maintWeightShiftEnd: BN, + maintWeightShiftDurationInv: I80F48Dto, + maintWeightShiftAssetTarget: I80F48Dto, + maintWeightShiftLiabTarget: I80F48Dto, ) { this.name = utf8.decode(new Uint8Array(name)).split('\x00')[0]; this.oracleConfig = { @@ -255,6 +283,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; @@ -376,6 +407,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)); } @@ -449,19 +506,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 42e3aab48..cf075a115 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 5d63fb9d2..46b45bf77 100644 --- a/ts/client/src/client.ts +++ b/ts/client/src/client.ts @@ -495,6 +495,11 @@ export class MangoClient { params.flashLoanSwapFeeRate, params.interestCurveScaling, params.interestTargetUtilization, + params.maintWeightShiftStart, + params.maintWeightShiftEnd, + params.maintWeightShiftAssetTarget, + params.maintWeightShiftLiabTarget, + params.maintWeightShiftAbort ?? false, ) .accounts({ group: group.publicKey, diff --git a/ts/client/src/clientIxParamBuilder.ts b/ts/client/src/clientIxParamBuilder.ts index d34245327..a86ec9c5f 100644 --- a/ts/client/src/clientIxParamBuilder.ts +++ b/ts/client/src/clientIxParamBuilder.ts @@ -96,6 +96,11 @@ export interface TokenEditParams { flashLoanSwapFeeRate: number | null; interestCurveScaling: number | null; interestTargetUtilization: number | null; + maintWeightShiftStart: BN | null; + maintWeightShiftEnd: BN | null; + maintWeightShiftAssetTarget: number | null; + maintWeightShiftLiabTarget: number | null; + maintWeightShiftAbort: boolean | null; } export const NullTokenEditParams: TokenEditParams = { @@ -128,6 +133,11 @@ export const NullTokenEditParams: TokenEditParams = { flashLoanSwapFeeRate: null, interestCurveScaling: null, interestTargetUtilization: null, + maintWeightShiftStart: null, + maintWeightShiftEnd: null, + maintWeightShiftAssetTarget: null, + maintWeightShiftLiabTarget: null, + maintWeightShiftAbort: null, }; export interface PerpEditParams { diff --git a/ts/client/src/mango_v4.ts b/ts/client/src/mango_v4.ts index 278af8091..b1b036df8 100644 --- a/ts/client/src/mango_v4.ts +++ b/ts/client/src/mango_v4.ts @@ -956,6 +956,34 @@ export type MangoV4 = { "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" } ] }, @@ -7079,12 +7107,38 @@ export type MangoV4 = { "name": "depositsInSerum", "type": "i64" }, + { + "name": "maintWeightShiftStart", + "type": "u64" + }, + { + "name": "maintWeightShiftEnd", + "type": "u64" + }, + { + "name": "maintWeightShiftDurationInv", + "type": { + "defined": "I80F48" + } + }, + { + "name": "maintWeightShiftAssetTarget", + "type": { + "defined": "I80F48" + } + }, + { + "name": "maintWeightShiftLiabTarget", + "type": { + "defined": "I80F48" + } + }, { "name": "reserved", "type": { "array": [ "u8", - 2072 + 2008 ] } } @@ -14187,6 +14241,34 @@ export const IDL: MangoV4 = { "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" } ] }, @@ -20310,12 +20392,38 @@ export const IDL: MangoV4 = { "name": "depositsInSerum", "type": "i64" }, + { + "name": "maintWeightShiftStart", + "type": "u64" + }, + { + "name": "maintWeightShiftEnd", + "type": "u64" + }, + { + "name": "maintWeightShiftDurationInv", + "type": { + "defined": "I80F48" + } + }, + { + "name": "maintWeightShiftAssetTarget", + "type": { + "defined": "I80F48" + } + }, + { + "name": "maintWeightShiftLiabTarget", + "type": { + "defined": "I80F48" + } + }, { "name": "reserved", "type": { "array": [ "u8", - 2072 + 2008 ] } }