diff --git a/keeper/src/crank.rs b/keeper/src/crank.rs index 74ba0484f..c65c24e22 100644 --- a/keeper/src/crank.rs +++ b/keeper/src/crank.rs @@ -2,7 +2,7 @@ use std::{sync::Arc, time::Duration}; use crate::MangoClient; -use anchor_lang::__private::bytemuck::cast_ref; +use anchor_lang::{__private::bytemuck::cast_ref, solana_program}; use futures::Future; use mango_v4::state::{EventQueue, EventType, FillEvent, OutEvent, PerpMarket, TokenIndex}; use solana_sdk::{ @@ -19,7 +19,7 @@ pub async fn runner( .banks_cache .values() .map(|banks_for_a_token| { - loop_update_index( + loop_update_index_and_rate( mango_client.clone(), banks_for_a_token.get(0).unwrap().1.token_index, ) @@ -48,7 +48,7 @@ pub async fn runner( Ok(()) } -pub async fn loop_update_index(mango_client: Arc, token_index: TokenIndex) { +pub async fn loop_update_index_and_rate(mango_client: Arc, token_index: TokenIndex) { let mut interval = time::interval(Duration::from_secs(5)); loop { interval.tick().await; @@ -74,11 +74,15 @@ pub async fn loop_update_index(mango_client: Arc, token_index: Toke let mut ix = Instruction { program_id: mango_v4::id(), accounts: anchor_lang::ToAccountMetas::to_account_metas( - &mango_v4::accounts::TokenUpdateIndex { mint_info, oracle }, + &mango_v4::accounts::TokenUpdateIndexAndRate { + mint_info, + oracle, + instructions: solana_program::sysvar::instructions::id(), + }, None, ), data: anchor_lang::InstructionData::data( - &mango_v4::instruction::TokenUpdateIndex {}, + &mango_v4::instruction::TokenUpdateIndexAndRate {}, ), }; let mut banks = bank_pubkeys_for_a_token @@ -97,7 +101,11 @@ pub async fn loop_update_index(mango_client: Arc, token_index: Toke if let Err(e) = sig_result { log::error!("{:?}", e) } else { - log::info!("update_index {} {:?}", token_name, sig_result.unwrap()) + log::info!( + "update_index_and_rate {} {:?}", + token_name, + sig_result.unwrap() + ) } Ok(()) diff --git a/programs/mango-v4/src/instructions/flash_loan3.rs b/programs/mango-v4/src/instructions/flash_loan3.rs index c3e375853..12926b682 100644 --- a/programs/mango-v4/src/instructions/flash_loan3.rs +++ b/programs/mango-v4/src/instructions/flash_loan3.rs @@ -115,7 +115,7 @@ pub fn flash_loan3_begin<'key, 'accounts, 'remaining, 'info>( let ix = match tx_instructions::load_instruction_at_checked(index, ixs) { Ok(ix) => ix, Err(ProgramError::InvalidArgument) => break, // past the last instruction - Err(e) => Err(e)?, + Err(e) => return Err(e.into()), }; // Check that the mango program key is not used @@ -129,9 +129,9 @@ pub fn flash_loan3_begin<'key, 'accounts, 'remaining, 'info>( found_end = true; // must be the FlashLoan3End instruction - require_msg!( - &ix.data[0..8] == &[163, 231, 155, 56, 201, 68, 84, 148], - "the next Mango instruction after FlashLoan3Begin must be FlashLoan3End" + require!( + ix.data[0..8] == [163, 231, 155, 56, 201, 68, 84, 148], + MangoError::SomeError ); // check that the same vaults are passed diff --git a/programs/mango-v4/src/instructions/mod.rs b/programs/mango-v4/src/instructions/mod.rs index a032b0089..c2845c2ee 100644 --- a/programs/mango-v4/src/instructions/mod.rs +++ b/programs/mango-v4/src/instructions/mod.rs @@ -37,7 +37,7 @@ pub use token_deposit::*; pub use token_deregister::*; pub use token_edit::*; pub use token_register::*; -pub use token_update_index::*; +pub use token_update_index_and_rate::*; pub use token_withdraw::*; mod account_close; @@ -79,5 +79,5 @@ mod token_deposit; mod token_deregister; mod token_edit; mod token_register; -mod token_update_index; +mod token_update_index_and_rate; mod token_withdraw; diff --git a/programs/mango-v4/src/instructions/token_edit.rs b/programs/mango-v4/src/instructions/token_edit.rs index 691fe0e2b..c708c8b26 100644 --- a/programs/mango-v4/src/instructions/token_edit.rs +++ b/programs/mango-v4/src/instructions/token_edit.rs @@ -73,6 +73,7 @@ pub fn token_edit( if let Some(ref interest_rate_params) = interest_rate_params_opt { // TODO: add a require! verifying relation between the parameters + bank.adjustment_factor = I80F48::from_num(interest_rate_params.adjustment_factor); bank.util0 = I80F48::from_num(interest_rate_params.util0); bank.rate0 = I80F48::from_num(interest_rate_params.rate0); bank.util1 = I80F48::from_num(interest_rate_params.util1); diff --git a/programs/mango-v4/src/instructions/token_register.rs b/programs/mango-v4/src/instructions/token_register.rs index c3cb13c74..aa152efb7 100644 --- a/programs/mango-v4/src/instructions/token_register.rs +++ b/programs/mango-v4/src/instructions/token_register.rs @@ -82,6 +82,7 @@ pub struct InterestRateParams { pub util1: f32, pub rate1: f32, pub max_rate: f32, + pub adjustment_factor: f32, } // TODO: should this be "configure_mint", we pass an explicit index, and allow @@ -128,8 +129,11 @@ pub fn token_register( cached_indexed_total_borrows: I80F48::ZERO, indexed_deposits: I80F48::ZERO, indexed_borrows: I80F48::ZERO, - last_updated: Clock::get()?.unix_timestamp, + index_last_updated: Clock::get()?.unix_timestamp, + bank_rate_last_updated: Clock::get()?.unix_timestamp, // TODO: add a require! verifying relation between the parameters + avg_utilization: I80F48::ZERO, + adjustment_factor: I80F48::from_num(interest_rate_params.adjustment_factor), util0: I80F48::from_num(interest_rate_params.util0), rate0: I80F48::from_num(interest_rate_params.rate0), util1: I80F48::from_num(interest_rate_params.util1), diff --git a/programs/mango-v4/src/instructions/token_update_index.rs b/programs/mango-v4/src/instructions/token_update_index.rs deleted file mode 100644 index 39d3d15c0..000000000 --- a/programs/mango-v4/src/instructions/token_update_index.rs +++ /dev/null @@ -1,86 +0,0 @@ -use anchor_lang::prelude::*; - -use crate::logs::UpdateIndexLog; -use crate::{ - accounts_zerocopy::{AccountInfoRef, LoadMutZeroCopyRef, LoadZeroCopyRef}, - state::{oracle_price, Bank, MintInfo}, -}; -use checked_math as cm; -use fixed::types::I80F48; -#[derive(Accounts)] -pub struct TokenUpdateIndex<'info> { - pub mint_info: AccountLoader<'info, MintInfo>, - pub oracle: UncheckedAccount<'info>, -} - -pub fn token_update_index(ctx: Context) -> Result<()> { - let mint_info = ctx.accounts.mint_info.load()?; - require_keys_eq!(mint_info.oracle.key(), ctx.accounts.oracle.key()); - - ctx.accounts - .mint_info - .load()? - .verify_banks_ais(ctx.remaining_accounts)?; - - let mut indexed_total_deposits = I80F48::ZERO; - let mut indexed_total_borrows = I80F48::ZERO; - for ai in ctx.remaining_accounts.iter() { - let bank = ai.load::()?; - indexed_total_deposits = cm!(indexed_total_deposits + bank.indexed_deposits); - indexed_total_borrows = cm!(indexed_total_borrows + bank.indexed_borrows); - } - - let now_ts = Clock::get()?.unix_timestamp; - let (diff_ts, deposit_index, borrow_index, oracle_conf_filter, base_token_decimals) = { - let mut some_bank = ctx.remaining_accounts[0].load_mut::()?; - - // TODO: should we enforce a minimum window between 2 update_index ix calls? - let diff_ts = I80F48::from_num(now_ts - some_bank.last_updated); - - let (deposit_index, borrow_index) = - some_bank.compute_index(indexed_total_deposits, indexed_total_borrows, diff_ts)?; - - ( - diff_ts, - deposit_index, - borrow_index, - some_bank.oracle_config.conf_filter, - some_bank.mint_decimals, - ) - }; - - msg!("indexed_total_deposits {}", indexed_total_deposits); - msg!("indexed_total_borrows {}", indexed_total_borrows); - msg!("diff_ts {}", diff_ts); - msg!("deposit_index {}", deposit_index); - msg!("borrow_index {}", borrow_index); - - for ai in ctx.remaining_accounts.iter() { - let mut bank = ai.load_mut::()?; - - bank.cached_indexed_total_deposits = indexed_total_deposits; - bank.cached_indexed_total_borrows = indexed_total_borrows; - - bank.last_updated = now_ts; - bank.charge_loan_fee(diff_ts); - - bank.deposit_index = deposit_index; - bank.borrow_index = borrow_index; - } - - let price = oracle_price( - &AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?, - oracle_conf_filter, - base_token_decimals, - )?; - - emit!(UpdateIndexLog { - mango_group: mint_info.group.key(), - token_index: mint_info.token_index, - deposit_index: deposit_index.to_bits(), - borrow_index: borrow_index.to_bits(), - price: price.to_bits(), - }); - - Ok(()) -} 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 new file mode 100644 index 000000000..5714024f9 --- /dev/null +++ b/programs/mango-v4/src/instructions/token_update_index_and_rate.rs @@ -0,0 +1,163 @@ +use anchor_lang::prelude::*; + +use crate::error::MangoError; +use crate::logs::{UpdateIndexLog, UpdateRateLog}; +use crate::state::HOUR; +use crate::{ + accounts_zerocopy::{AccountInfoRef, LoadMutZeroCopyRef, LoadZeroCopyRef}, + state::{oracle_price, Bank, MintInfo}, +}; +use anchor_lang::solana_program::sysvar::instructions as tx_instructions; +use checked_math as cm; +use fixed::types::I80F48; + +#[derive(Accounts)] +pub struct TokenUpdateIndexAndRate<'info> { + #[account( + has_one = oracle + )] + pub mint_info: AccountLoader<'info, MintInfo>, + + pub oracle: UncheckedAccount<'info>, + + #[account(address = tx_instructions::ID)] + pub instructions: UncheckedAccount<'info>, +} + +pub fn token_update_index_and_rate(ctx: Context) -> Result<()> { + { + let ixs = ctx.accounts.instructions.as_ref(); + + let mut index = 0; + loop { + let ix = match tx_instructions::load_instruction_at_checked(index, ixs) { + Ok(ix) => ix, + Err(ProgramError::InvalidArgument) => break, + Err(e) => return Err(e.into()), + }; + + // 1. we want to forbid token deposit and token withdraw and similar + // (serum3 place order could be used as a withdraw and a serum3 cancel order as a deposit) + // to be called in same tx as this ix to prevent index or rate manipulation, + // for now we just whitelist to other token_update_index_and_rate ix + // 2. we want to forbid cpi, since ix we would like to blacklist could just be called from cpi + require!( + ix.program_id == crate::id() + && ix.data[0..8] == [131, 136, 194, 39, 11, 50, 10, 198], // token_update_index_and_rate + MangoError::SomeError + ); + + index += 1; + } + } + + let mint_info = ctx.accounts.mint_info.load()?; + + ctx.accounts + .mint_info + .load()? + .verify_banks_ais(ctx.remaining_accounts)?; + + let now_ts = Clock::get()?.unix_timestamp; + + // compute indexed_total + let mut indexed_total_deposits = I80F48::ZERO; + let mut indexed_total_borrows = I80F48::ZERO; + for ai in ctx.remaining_accounts.iter() { + let bank = ai.load::()?; + indexed_total_deposits = cm!(indexed_total_deposits + bank.indexed_deposits); + indexed_total_borrows = cm!(indexed_total_borrows + bank.indexed_borrows); + } + + // compute and set latest index and average utilization on each bank + { + let some_bank = ctx.remaining_accounts[0].load::()?; + + let now_ts_i80f48 = I80F48::from_num(now_ts); + let diff_ts = I80F48::from_num(now_ts - some_bank.index_last_updated); + + let (deposit_index, borrow_index) = + some_bank.compute_index(indexed_total_deposits, indexed_total_borrows, diff_ts)?; + + let new_avg_utilization = some_bank.compute_new_avg_utilization( + indexed_total_deposits, + indexed_total_borrows, + now_ts_i80f48, + ); + + let price = oracle_price( + &AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?, + some_bank.oracle_config.conf_filter, + some_bank.mint_decimals, + )?; + emit!(UpdateIndexLog { + mango_group: mint_info.group.key(), + token_index: mint_info.token_index, + deposit_index: deposit_index.to_bits(), + borrow_index: borrow_index.to_bits(), + avg_utilization: new_avg_utilization.to_bits(), + price: price.to_bits() + }); + + drop(some_bank); + + msg!("indexed_total_deposits {}", indexed_total_deposits); + msg!("indexed_total_borrows {}", indexed_total_borrows); + msg!("diff_ts {}", diff_ts); + msg!("deposit_index {}", deposit_index); + msg!("borrow_index {}", borrow_index); + msg!("avg_utilization {}", new_avg_utilization); + + for ai in ctx.remaining_accounts.iter() { + let mut bank = ai.load_mut::()?; + + bank.cached_indexed_total_deposits = indexed_total_deposits; + bank.cached_indexed_total_borrows = indexed_total_borrows; + + bank.index_last_updated = now_ts; + bank.charge_loan_fee(diff_ts); + + bank.deposit_index = deposit_index; + bank.borrow_index = borrow_index; + + bank.avg_utilization = new_avg_utilization; + } + } + + // compute optimal rates, and max rate and set them on the bank + { + let some_bank = ctx.remaining_accounts[0].load::()?; + + 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(); + + emit!(UpdateRateLog { + mango_group: mint_info.group.key(), + token_index: mint_info.token_index, + rate0: rate0.to_bits(), + rate1: rate1.to_bits(), + max_rate: max_rate.to_bits(), + }); + + drop(some_bank); + + msg!("rate0 {}", rate0); + msg!("rate1 {}", rate1); + msg!("max_rate {}", max_rate); + + for ai in ctx.remaining_accounts.iter() { + let mut bank = ai.load_mut::()?; + + bank.bank_rate_last_updated = now_ts; + bank.rate0 = rate0; + bank.rate1 = rate1; + bank.max_rate = max_rate; + } + } + } + + Ok(()) +} diff --git a/programs/mango-v4/src/lib.rs b/programs/mango-v4/src/lib.rs index f914cde0b..fd8da39fc 100644 --- a/programs/mango-v4/src/lib.rs +++ b/programs/mango-v4/src/lib.rs @@ -118,8 +118,8 @@ pub mod mango_v4 { instructions::token_deregister(ctx, token_index) } - pub fn token_update_index(ctx: Context) -> Result<()> { - instructions::token_update_index(ctx) + pub fn token_update_index_and_rate(ctx: Context) -> Result<()> { + instructions::token_update_index_and_rate(ctx) } pub fn account_create( @@ -159,10 +159,12 @@ pub mod mango_v4 { instructions::stub_oracle_set(ctx, price) } + // NOTE: keep disc synced in token_update_index_and_rate ix pub fn token_deposit(ctx: Context, amount: u64) -> Result<()> { instructions::token_deposit(ctx, amount) } + // NOTE: keep disc synced in token_update_index_and_rate ix pub fn token_withdraw( ctx: Context, amount: u64, @@ -199,6 +201,7 @@ pub mod mango_v4 { instructions::flash_loan3_begin(ctx, loan_amounts) } + // NOTE: keep disc synced in flash_loan3.rs pub fn flash_loan3_end<'key, 'accounts, 'remaining, 'info>( ctx: Context<'key, 'accounts, 'remaining, 'info, FlashLoan3End<'info>>, ) -> Result<()> { diff --git a/programs/mango-v4/src/logs.rs b/programs/mango-v4/src/logs.rs index e4fe4138d..3762cfc90 100644 --- a/programs/mango-v4/src/logs.rs +++ b/programs/mango-v4/src/logs.rs @@ -130,9 +130,19 @@ pub struct UpdateFundingLog { pub struct UpdateIndexLog { pub mango_group: Pubkey, pub token_index: u16, - pub deposit_index: i128, // I80F48 - pub borrow_index: i128, // I80F48 - pub price: i128, // I80F48 + pub deposit_index: i128, // I80F48 + pub borrow_index: i128, // I80F48 + pub avg_utilization: i128, // I80F48 + pub price: i128, // I80F48 +} + +#[event] +pub struct UpdateRateLog { + pub mango_group: Pubkey, + pub token_index: u16, + pub rate0: i128, // I80F48 + pub rate1: i128, // I80F48 + pub max_rate: i128, // I80F48 } #[event] diff --git a/programs/mango-v4/src/state/bank.rs b/programs/mango-v4/src/state/bank.rs index b04df37a7..6033a7321 100644 --- a/programs/mango-v4/src/state/bank.rs +++ b/programs/mango-v4/src/state/bank.rs @@ -7,8 +7,10 @@ use static_assertions::const_assert_eq; use std::mem::size_of; -pub const DAY: I80F48 = I80F48!(86400); -pub const YEAR: I80F48 = I80F48!(31536000); +pub const HOUR: i64 = 3600; +pub const DAY: i64 = 86400; +pub const DAY_I80F48: I80F48 = I80F48!(86400); +pub const YEAR_I80F48: I80F48 = I80F48!(31536000); #[account(zero_copy)] pub struct Bank { @@ -28,8 +30,8 @@ pub struct Bank { pub deposit_index: I80F48, pub borrow_index: I80F48, - /// total deposits/borrows, only updated during UpdateIndex - /// TODO: These values could be dropped from the bank, they're written in UpdateIndex + /// total deposits/borrows, only updated during UpdateIndexAndRate + /// TODO: These values could be dropped from the bank, they're written in UpdateIndexAndRate /// and never read. pub cached_indexed_total_deposits: I80F48, pub cached_indexed_total_borrows: I80F48, @@ -42,11 +44,16 @@ pub struct Bank { /// /// The vault amount is not deducable from these values. /// - /// These become meaningful when summed over all banks (like in update_index). + /// These become meaningful when summed over all banks (like in update_index_and_rate). pub indexed_deposits: I80F48, pub indexed_borrows: I80F48, - pub last_updated: i64, + pub index_last_updated: i64, + pub bank_rate_last_updated: i64, + + pub avg_utilization: I80F48, + + pub adjustment_factor: I80F48, pub util0: I80F48, pub rate0: I80F48, pub util1: I80F48, @@ -92,7 +99,7 @@ pub struct Bank { } const_assert_eq!( size_of::(), - 16 + 32 * 4 + 8 + 16 * 21 + 2 * 8 + 2 + 1 + 1 + 4 + 8 + 16 + 32 * 4 + 8 * 2 + 16 * 23 + 2 * 8 + 2 + 1 + 1 + 4 + 8 ); const_assert_eq!(size_of::() % 8, 0); @@ -117,7 +124,9 @@ impl std::fmt::Debug for Bank { ) .field("indexed_deposits", &self.indexed_deposits) .field("indexed_borrows", &self.indexed_borrows) - .field("last_updated", &self.last_updated) + .field("index_last_updated", &self.index_last_updated) + .field("bank_rate_last_updated", &self.bank_rate_last_updated) + .field("avg_utilization", &self.avg_utilization) .field("util0", &self.util0) .field("rate0", &self.rate0) .field("util1", &self.util1) @@ -158,7 +167,10 @@ impl Bank { cached_indexed_total_borrows: existing_bank.cached_indexed_total_borrows, indexed_deposits: I80F48::ZERO, indexed_borrows: I80F48::ZERO, - last_updated: existing_bank.last_updated, + index_last_updated: existing_bank.index_last_updated, + bank_rate_last_updated: existing_bank.bank_rate_last_updated, + avg_utilization: existing_bank.avg_utilization, + adjustment_factor: existing_bank.adjustment_factor, util0: existing_bank.util0, rate0: existing_bank.rate0, util1: existing_bank.util1, @@ -375,31 +387,32 @@ impl Bank { pub fn charge_loan_fee(&mut self, diff_ts: I80F48) { let native_borrows_old = self.native_borrows(); self.indexed_borrows = - cm!((self.indexed_borrows * (I80F48::ONE + self.loan_fee_rate * (diff_ts / YEAR)))); + cm!((self.indexed_borrows + * (I80F48::ONE + self.loan_fee_rate * (diff_ts / YEAR_I80F48)))); self.collected_fees_native = cm!(self.collected_fees_native + self.native_borrows() - native_borrows_old); } pub fn compute_index( - &mut self, + &self, indexed_total_deposits: I80F48, indexed_total_borrows: I80F48, diff_ts: I80F48, ) -> Result<(I80F48, I80F48)> { // compute index based on utilization - let native_total_deposits = self.deposit_index * indexed_total_deposits; - let native_total_borrows = self.borrow_index * indexed_total_borrows; + let native_total_deposits = cm!(self.deposit_index * indexed_total_deposits); + let native_total_borrows = cm!(self.borrow_index * indexed_total_borrows); - let utilization = if native_total_deposits == I80F48::ZERO { + let instantaneous_utilization = if native_total_deposits == I80F48::ZERO { I80F48::ZERO } else { cm!(native_total_borrows / native_total_deposits) }; - let interest_rate = self.compute_interest_rate(utilization); + let borrow_interest_rate = self.compute_interest_rate(instantaneous_utilization); - let borrow_interest: I80F48 = cm!(interest_rate * diff_ts); - let deposit_interest = cm!(borrow_interest * utilization); + let borrow_interest: I80F48 = cm!(borrow_interest_rate * diff_ts); + let deposit_interest = cm!(borrow_interest * instantaneous_utilization); // msg!("utilization {}", utilization); // msg!("interest_rate {}", interest_rate); @@ -410,9 +423,10 @@ impl Bank { return Ok((self.deposit_index, self.borrow_index)); } - let borrow_index = cm!((self.borrow_index * borrow_interest) / YEAR + self.borrow_index); + let borrow_index = + cm!((self.borrow_index * borrow_interest) / YEAR_I80F48 + self.borrow_index); let deposit_index = - cm!((self.deposit_index * deposit_interest) / YEAR + self.deposit_index); + cm!((self.deposit_index * deposit_interest) / YEAR_I80F48 + self.deposit_index); Ok((deposit_index, borrow_index)) } @@ -441,7 +455,6 @@ impl Bank { rate1: I80F48, max_rate: I80F48, ) -> I80F48 { - // TODO: daffy: use optimal interest from oracle if utilization <= util0 { let slope = cm!(rate0 / util0); cm!(slope * utilization) @@ -455,6 +468,50 @@ impl Bank { cm!(rate1 + slope * extra_util) } } + + // compute new avg utilization + pub fn compute_new_avg_utilization( + &self, + indexed_total_deposits: I80F48, + indexed_total_borrows: I80F48, + now_ts: I80F48, + ) -> I80F48 { + if now_ts == I80F48::ZERO { + return I80F48::ZERO; + } + + let native_total_deposits = self.deposit_index * indexed_total_deposits; + let native_total_borrows = self.borrow_index * indexed_total_borrows; + let instantaneous_utilization = if native_total_deposits == I80F48::ZERO { + I80F48::ZERO + } else { + cm!(native_total_borrows / native_total_deposits) + }; + + // combine old and new with relevant factors to form new avg_utilization + // scaling factor for previous avg_utilization is old_ts/new_ts + // scaling factor for instantaneous utilization is (new_ts - old_ts) / new_ts + let bank_rate_last_updated_i80f48 = I80F48::from_num(self.bank_rate_last_updated); + (self.avg_utilization * bank_rate_last_updated_i80f48 + + instantaneous_utilization * (now_ts - bank_rate_last_updated_i80f48)) + / now_ts + } + + // computes new optimal rates and max rate + pub fn compute_rates(&self) -> (I80F48, I80F48, I80F48) { + // since we have 3 interest rate legs, consider the middle point of the middle leg as the optimal util + let optimal_util = (self.util0 + self.util1) / 2; + // use avg_utilization and not instantaneous_utilization so that rates cannot be manupulated easily + let util_diff = self.avg_utilization - optimal_util; + // move rates up when utilization is above optimal utilization, and vice versa + let adjustment = I80F48::ONE + self.adjustment_factor * util_diff; + // irrespective of which leg current utilization is in, update all rates + ( + cm!(self.rate0 * adjustment), + cm!(self.rate1 * adjustment), + cm!(self.max_rate * adjustment), + ) + } } #[macro_export] @@ -583,4 +640,34 @@ mod tests { } Ok(()) } + + #[test] + fn test_compute_new_avg_utilization() { + let mut bank = Bank::zeroed(); + bank.deposit_index = I80F48::from_num(1.0); + bank.borrow_index = I80F48::from_num(1.0); + bank.bank_rate_last_updated = 0; + + let compute_new_avg_utilization_runner = + |bank: &mut Bank, utilization: I80F48, now_ts: i64| { + bank.avg_utilization = bank.compute_new_avg_utilization( + I80F48::ONE, + utilization, + I80F48::from_num(now_ts), + ); + bank.bank_rate_last_updated = now_ts; + }; + + compute_new_avg_utilization_runner(&mut bank, I80F48::ZERO, 0); + assert_eq!(bank.avg_utilization, I80F48::ZERO); + + compute_new_avg_utilization_runner(&mut bank, I80F48::from_num(0.5), 10); + assert!((bank.avg_utilization - I80F48::from_num(0.5)).abs() < 0.0001); + + compute_new_avg_utilization_runner(&mut bank, I80F48::from_num(0.8), 15); + assert!((bank.avg_utilization - I80F48::from_num(0.6)).abs() < 0.0001); + + compute_new_avg_utilization_runner(&mut bank, I80F48::ONE, 20); + assert!((bank.avg_utilization - I80F48::from_num(0.7)).abs() < 0.0001); + } } diff --git a/programs/mango-v4/src/state/perp_market.rs b/programs/mango-v4/src/state/perp_market.rs index 9f067b6dd..ff293ec1f 100644 --- a/programs/mango-v4/src/state/perp_market.rs +++ b/programs/mango-v4/src/state/perp_market.rs @@ -6,10 +6,10 @@ use fixed::types::I80F48; use static_assertions::const_assert_eq; use crate::state::orderbook::order_type::Side; -use crate::state::{TokenIndex, DAY}; +use crate::state::TokenIndex; use crate::util::checked_math as cm; -use super::{Book, OracleConfig}; +use super::{Book, OracleConfig, DAY_I80F48}; pub type PerpMarketIndex = u16; @@ -134,7 +134,7 @@ impl PerpMarket { }; let diff_ts = I80F48::from_num(now_ts - self.funding_last_updated as u64); - let time_factor = cm!(diff_ts / DAY); + let time_factor = cm!(diff_ts / DAY_I80F48); let base_lot_size = I80F48::from_num(self.base_lot_size); let funding_delta = cm!(index_price * diff_price * base_lot_size * time_factor); diff --git a/programs/mango-v4/tests/program_test/mango_client.rs b/programs/mango-v4/tests/program_test/mango_client.rs index e7e16df45..dabb0f6e2 100644 --- a/programs/mango-v4/tests/program_test/mango_client.rs +++ b/programs/mango-v4/tests/program_test/mango_client.rs @@ -769,6 +769,7 @@ impl ClientInstruction for TokenDepositInstruction { pub struct TokenRegisterInstruction<'keypair> { pub token_index: TokenIndex, pub decimals: u8, + pub adjustment_factor: f32, pub util0: f32, pub rate0: f32, pub util1: f32, @@ -805,6 +806,7 @@ impl<'keypair> ClientInstruction for TokenRegisterInstruction<'keypair> { conf_filter: I80F48::from_num::(0.10), }, interest_rate_params: InterestRateParams { + adjustment_factor: self.adjustment_factor, util0: self.util0, rate0: self.rate0, util1: self.util1, @@ -2545,13 +2547,13 @@ impl ClientInstruction for BenchmarkInstruction { vec![] } } -pub struct TokenUpdateIndexInstruction { +pub struct TokenUpdateIndexAndRateInstruction { pub mint_info: Pubkey, } #[async_trait::async_trait(?Send)] -impl ClientInstruction for TokenUpdateIndexInstruction { - type Accounts = mango_v4::accounts::TokenUpdateIndex; - type Instruction = mango_v4::instruction::TokenUpdateIndex; +impl ClientInstruction for TokenUpdateIndexAndRateInstruction { + type Accounts = mango_v4::accounts::TokenUpdateIndexAndRate; + type Instruction = mango_v4::instruction::TokenUpdateIndexAndRate; async fn to_instruction( &self, loader: impl ClientAccountLoader + 'async_trait, @@ -2564,6 +2566,7 @@ impl ClientInstruction for TokenUpdateIndexInstruction { let accounts = Self::Accounts { mint_info: self.mint_info, oracle: mint_info.oracle, + instructions: solana_program::sysvar::instructions::id(), }; let mut instruction = make_instruction(program_id, &accounts, instruction); diff --git a/programs/mango-v4/tests/program_test/mango_setup.rs b/programs/mango-v4/tests/program_test/mango_setup.rs index 9f10d618f..e0a5b066b 100644 --- a/programs/mango-v4/tests/program_test/mango_setup.rs +++ b/programs/mango-v4/tests/program_test/mango_setup.rs @@ -83,6 +83,7 @@ impl<'a> GroupWithTokensConfig<'a> { TokenRegisterInstruction { token_index, decimals: mint.decimals, + adjustment_factor: 0.01, util0: 0.40, rate0: 0.07, util1: 0.80, diff --git a/programs/mango-v4/tests/test_basic.rs b/programs/mango-v4/tests/test_basic.rs index 6b31eaec8..954eb77a5 100644 --- a/programs/mango-v4/tests/test_basic.rs +++ b/programs/mango-v4/tests/test_basic.rs @@ -155,7 +155,7 @@ async fn test_basic() -> Result<(), TransportError> { // withdraw whatever is remaining, can't close bank vault without this send_tx( solana, - TokenUpdateIndexInstruction { + TokenUpdateIndexAndRateInstruction { mint_info: tokens[0].mint_info, }, ) @@ -179,7 +179,7 @@ async fn test_basic() -> Result<(), TransportError> { // close account send_tx( solana, - AccountCloseInstruction { + CloseAccountInstruction { group, account, owner, @@ -217,7 +217,7 @@ async fn test_basic() -> Result<(), TransportError> { // close stub oracle send_tx( solana, - StubOracleCloseInstruction { + CloseStubOracleInstruction { group, mint: bank_data.mint, admin, @@ -230,7 +230,7 @@ async fn test_basic() -> Result<(), TransportError> { // close group send_tx( solana, - GroupCloseInstruction { + CloseGroupInstruction { group, admin, sol_destination: payer.pubkey(), diff --git a/programs/mango-v4/tests/test_update_index.rs b/programs/mango-v4/tests/test_token_update_index_and_rate.rs similarity index 78% rename from programs/mango-v4/tests/test_update_index.rs rename to programs/mango-v4/tests/test_token_update_index_and_rate.rs index 2efc9ba49..02aa61001 100644 --- a/programs/mango-v4/tests/test_update_index.rs +++ b/programs/mango-v4/tests/test_token_update_index_and_rate.rs @@ -9,7 +9,7 @@ use program_test::*; mod program_test; #[tokio::test] -async fn test_update_index() -> Result<(), TransportError> { +async fn test_token_update_index_and_rate() -> Result<(), TransportError> { let context = TestContext::new().await; let solana = &context.solana.clone(); @@ -99,24 +99,30 @@ async fn test_update_index() -> Result<(), TransportError> { .await .unwrap(); - let bank_before_update_index = solana.get_account::(tokens[0].bank).await; + let bank_before_update_index_and_rate = solana.get_account::(tokens[0].bank).await; solana.advance_clock().await; send_tx( solana, - TokenUpdateIndexInstruction { + TokenUpdateIndexAndRateInstruction { mint_info: tokens[0].mint_info, }, ) .await .unwrap(); - let bank_after_update_index = solana.get_account::(tokens[0].bank).await; - dbg!(bank_after_update_index); - dbg!(bank_after_update_index); - assert!(bank_before_update_index.deposit_index < bank_after_update_index.deposit_index); - assert!(bank_before_update_index.borrow_index < bank_after_update_index.borrow_index); + let bank_after_update_index_and_rate = solana.get_account::(tokens[0].bank).await; + dbg!(bank_after_update_index_and_rate); + dbg!(bank_after_update_index_and_rate); + assert!( + bank_before_update_index_and_rate.deposit_index + < bank_after_update_index_and_rate.deposit_index + ); + assert!( + bank_before_update_index_and_rate.borrow_index + < bank_after_update_index_and_rate.borrow_index + ); Ok(()) } diff --git a/ts/client/src/client.ts b/ts/client/src/client.ts index 2d5fca15e..560e0ca24 100644 --- a/ts/client/src/client.ts +++ b/ts/client/src/client.ts @@ -140,6 +140,7 @@ export class MangoClient { oracleConfFilter: number, tokenIndex: number, name: string, + adjustmentFactor: number, util0: number, rate0: number, util1: number, @@ -163,7 +164,7 @@ export class MangoClient { val: I80F48.fromNumber(oracleConfFilter).getData(), }, } as any, // future: nested custom types dont typecheck, fix if possible? - { util0, rate0, util1, rate1, maxRate }, + { adjustmentFactor, util0, rate0, util1, rate1, maxRate }, loanFeeRate, loanOriginationFeeRate, maintAssetWeight, @@ -188,6 +189,7 @@ export class MangoClient { tokenName: string, oracle: PublicKey, oracleConfFilter: number, + adjustmentFactor: number, util0: number, rate0: number, util1: number, @@ -213,7 +215,7 @@ export class MangoClient { val: I80F48.fromNumber(oracleConfFilter).getData(), }, } as any, // future: nested custom types dont typecheck, fix if possible? - { util0, rate0, util1, rate1, maxRate }, + { adjustmentFactor, util0, rate0, util1, rate1, maxRate }, loanFeeRate, loanOriginationFeeRate, maintAssetWeight, diff --git a/ts/client/src/mango_v4.ts b/ts/client/src/mango_v4.ts index 467df8fad..e13cbc5cc 100644 --- a/ts/client/src/mango_v4.ts +++ b/ts/client/src/mango_v4.ts @@ -3,7 +3,7 @@ export type MangoV4 = { "name": "mango_v4", "instructions": [ { - "name": "groupCreate", + "name": "createGroup", "accounts": [ { "name": "group", @@ -91,7 +91,7 @@ export type MangoV4 = { ] }, { - "name": "groupClose", + "name": "closeGroup", "accounts": [ { "name": "group", @@ -568,7 +568,7 @@ export type MangoV4 = { ] }, { - "name": "tokenUpdateIndex", + "name": "updateIndexAndRate", "accounts": [ { "name": "mintInfo", @@ -584,7 +584,7 @@ export type MangoV4 = { "args": [] }, { - "name": "accountCreate", + "name": "createAccount", "accounts": [ { "name": "group", @@ -648,7 +648,7 @@ export type MangoV4 = { ] }, { - "name": "accountEdit", + "name": "editAccount", "accounts": [ { "name": "group", @@ -682,7 +682,7 @@ export type MangoV4 = { ] }, { - "name": "accountClose", + "name": "closeAccount", "accounts": [ { "name": "group", @@ -713,7 +713,7 @@ export type MangoV4 = { "args": [] }, { - "name": "stubOracleCreate", + "name": "createStubOracle", "accounts": [ { "name": "group", @@ -776,7 +776,7 @@ export type MangoV4 = { ] }, { - "name": "stubOracleClose", + "name": "closeStubOracle", "accounts": [ { "name": "group", @@ -807,7 +807,7 @@ export type MangoV4 = { "args": [] }, { - "name": "stubOracleSet", + "name": "setStubOracle", "accounts": [ { "name": "group", @@ -2560,9 +2560,25 @@ export type MangoV4 = { } }, { - "name": "lastUpdated", + "name": "indexLastUpdated", "type": "i64" }, + { + "name": "bankRateLastUpdated", + "type": "i64" + }, + { + "name": "avgUtilization", + "type": { + "defined": "I80F48" + } + }, + { + "name": "adjustmentFactor", + "type": { + "defined": "I80F48" + } + }, { "name": "util0", "type": { @@ -3362,6 +3378,10 @@ export type MangoV4 = { { "name": "maxRate", "type": "f32" + }, + { + "name": "adjustmentFactor", + "type": "f32" } ] } @@ -4399,6 +4419,41 @@ export type MangoV4 = { } ] }, + { + "name": "UpdateRateLog", + "fields": [ + { + "name": "mangoGroup", + "type": "publicKey", + "index": false + }, + { + "name": "tokenIndex", + "type": "u16", + "index": false + }, + { + "name": "avgUtilization", + "type": "i128", + "index": false + }, + { + "name": "rate0", + "type": "i128", + "index": false + }, + { + "name": "rate1", + "type": "i128", + "index": false + }, + { + "name": "maxRate", + "type": "i128", + "index": false + } + ] + }, { "name": "LiquidateTokenAndTokenLog", "fields": [ @@ -4539,7 +4594,7 @@ export const IDL: MangoV4 = { "name": "mango_v4", "instructions": [ { - "name": "groupCreate", + "name": "createGroup", "accounts": [ { "name": "group", @@ -4627,7 +4682,7 @@ export const IDL: MangoV4 = { ] }, { - "name": "groupClose", + "name": "closeGroup", "accounts": [ { "name": "group", @@ -5104,7 +5159,7 @@ export const IDL: MangoV4 = { ] }, { - "name": "tokenUpdateIndex", + "name": "updateIndexAndRate", "accounts": [ { "name": "mintInfo", @@ -5120,7 +5175,7 @@ export const IDL: MangoV4 = { "args": [] }, { - "name": "accountCreate", + "name": "createAccount", "accounts": [ { "name": "group", @@ -5184,7 +5239,7 @@ export const IDL: MangoV4 = { ] }, { - "name": "accountEdit", + "name": "editAccount", "accounts": [ { "name": "group", @@ -5218,7 +5273,7 @@ export const IDL: MangoV4 = { ] }, { - "name": "accountClose", + "name": "closeAccount", "accounts": [ { "name": "group", @@ -5249,7 +5304,7 @@ export const IDL: MangoV4 = { "args": [] }, { - "name": "stubOracleCreate", + "name": "createStubOracle", "accounts": [ { "name": "group", @@ -5312,7 +5367,7 @@ export const IDL: MangoV4 = { ] }, { - "name": "stubOracleClose", + "name": "closeStubOracle", "accounts": [ { "name": "group", @@ -5343,7 +5398,7 @@ export const IDL: MangoV4 = { "args": [] }, { - "name": "stubOracleSet", + "name": "setStubOracle", "accounts": [ { "name": "group", @@ -7096,9 +7151,25 @@ export const IDL: MangoV4 = { } }, { - "name": "lastUpdated", + "name": "indexLastUpdated", "type": "i64" }, + { + "name": "bankRateLastUpdated", + "type": "i64" + }, + { + "name": "avgUtilization", + "type": { + "defined": "I80F48" + } + }, + { + "name": "adjustmentFactor", + "type": { + "defined": "I80F48" + } + }, { "name": "util0", "type": { @@ -7898,6 +7969,10 @@ export const IDL: MangoV4 = { { "name": "maxRate", "type": "f32" + }, + { + "name": "adjustmentFactor", + "type": "f32" } ] } @@ -8935,6 +9010,41 @@ export const IDL: MangoV4 = { } ] }, + { + "name": "UpdateRateLog", + "fields": [ + { + "name": "mangoGroup", + "type": "publicKey", + "index": false + }, + { + "name": "tokenIndex", + "type": "u16", + "index": false + }, + { + "name": "avgUtilization", + "type": "i128", + "index": false + }, + { + "name": "rate0", + "type": "i128", + "index": false + }, + { + "name": "rate1", + "type": "i128", + "index": false + }, + { + "name": "maxRate", + "type": "i128", + "index": false + } + ] + }, { "name": "LiquidateTokenAndTokenLog", "fields": [ diff --git a/ts/client/src/scripts/example1-admin.ts b/ts/client/src/scripts/example1-admin.ts index ef3d4c364..40a531a95 100644 --- a/ts/client/src/scripts/example1-admin.ts +++ b/ts/client/src/scripts/example1-admin.ts @@ -74,6 +74,7 @@ async function main() { 0.1, 1, // tokenIndex 'BTC', + 0.01, 0.4, 0.07, 0.8, @@ -112,6 +113,7 @@ async function main() { 0.1, 0, // tokenIndex 'USDC', + 0.01, 0.4, 0.07, 0.8, @@ -140,6 +142,7 @@ async function main() { 0.1, 2, // tokenIndex 'SOL', + 0.01, 0.4, 0.07, 0.8, @@ -170,6 +173,7 @@ async function main() { 0.1, 3, // tokenIndex 'ORCA', + 0.01, 0.4, 0.07, 0.8, @@ -266,6 +270,7 @@ async function main() { 'USDC', btcDevnetOracle, 0.1, + 0.01, 0.3, 0.08, 0.81, @@ -292,6 +297,7 @@ async function main() { 'USDC', usdcDevnetOracle.publicKey, 0.1, + 0.01, 0.4, 0.07, 0.8, diff --git a/ts/client/src/scripts/mb-example1-admin.ts b/ts/client/src/scripts/mb-example1-admin.ts index 73ba98660..3d2ad8e4b 100644 --- a/ts/client/src/scripts/mb-example1-admin.ts +++ b/ts/client/src/scripts/mb-example1-admin.ts @@ -60,6 +60,7 @@ async function main() { 0.1, 0, 'BTC', + 0.01, 0.4, 0.07, 0.8, @@ -100,6 +101,7 @@ async function main() { 0.1, 1, 'USDC', + 0.01, 0.4, 0.07, 0.8, @@ -130,6 +132,7 @@ async function main() { 0.1, 2, // tokenIndex 'SOL', + 0.01, 0.4, 0.07, 0.8,