From 01a958cd22c35bacba6a3f0d4ccbd604db65cde2 Mon Sep 17 00:00:00 2001 From: Nicholas Clarke Date: Wed, 28 Sep 2022 23:04:33 -0700 Subject: [PATCH] Clarkeni/onchain interest (#244) * Onchain interest calculation * Fix to TokenBalanceLog for token_liq_bankruptcy (was previously using liqee liab position for liqor liab position). * Log cumulative interest when token position is deactivated. --- .../mango-v4/src/instructions/flash_loan.rs | 2 +- .../src/instructions/token_deposit.rs | 2 +- .../src/instructions/token_liq_bankruptcy.rs | 53 ++++++----- .../src/instructions/token_liq_with_token.rs | 24 ++--- .../src/instructions/token_withdraw.rs | 2 +- programs/mango-v4/src/logs.rs | 9 ++ programs/mango-v4/src/state/bank.rs | 37 +++++++- programs/mango-v4/src/state/mango_account.rs | 26 +++++- .../src/state/mango_account_components.rs | 20 +++- ts/client/src/mango_v4.ts | 92 ++++++++++++++++++- 10 files changed, 221 insertions(+), 46 deletions(-) diff --git a/programs/mango-v4/src/instructions/flash_loan.rs b/programs/mango-v4/src/instructions/flash_loan.rs index 05de67c06..3c4328533 100644 --- a/programs/mango-v4/src/instructions/flash_loan.rs +++ b/programs/mango-v4/src/instructions/flash_loan.rs @@ -394,7 +394,7 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>( // Deactivate inactive token accounts after health check for raw_token_index in deactivated_token_positions { - account.deactivate_token_position(raw_token_index); + account.deactivate_token_position_and_log(raw_token_index, ctx.accounts.account.key()); } Ok(()) diff --git a/programs/mango-v4/src/instructions/token_deposit.rs b/programs/mango-v4/src/instructions/token_deposit.rs index c0fab0c14..e040dd9dc 100644 --- a/programs/mango-v4/src/instructions/token_deposit.rs +++ b/programs/mango-v4/src/instructions/token_deposit.rs @@ -112,7 +112,7 @@ pub fn token_deposit(ctx: Context, amount: u64) -> Result<()> { // Deposits can deactivate a position if they cancel out a previous borrow. // if !position_is_active { - account.deactivate_token_position(raw_token_index); + account.deactivate_token_position_and_log(raw_token_index, ctx.accounts.account.key()); } emit!(DepositLog { diff --git a/programs/mango-v4/src/instructions/token_liq_bankruptcy.rs b/programs/mango-v4/src/instructions/token_liq_bankruptcy.rs index d904428a1..ac1768538 100644 --- a/programs/mango-v4/src/instructions/token_liq_bankruptcy.rs +++ b/programs/mango-v4/src/instructions/token_liq_bankruptcy.rs @@ -170,7 +170,16 @@ pub fn token_liq_bankruptcy( let (liqor_quote, liqor_quote_raw_token_index, _) = liqor.ensure_token_position(QUOTE_TOKEN_INDEX)?; let liqor_quote_active = quote_bank.deposit(liqor_quote, insurance_transfer_i80f48)?; - let liqor_quote_indexed_position = liqor_quote.indexed_position; + + // liqor quote + emit!(TokenBalanceLog { + mango_group: ctx.accounts.group.key(), + mango_account: ctx.accounts.liqor.key(), + token_index: QUOTE_TOKEN_INDEX, + indexed_position: liqor_quote.indexed_position.to_bits(), + deposit_index: quote_deposit_index.to_bits(), + borrow_index: quote_borrow_index.to_bits(), + }); // transfer liab from liqee to liqor let (liqor_liab, liqor_liab_raw_token_index, _) = @@ -178,6 +187,16 @@ pub fn token_liq_bankruptcy( let (liqor_liab_active, loan_origination_fee) = liab_bank.withdraw_with_fee(liqor_liab, liab_transfer)?; + // liqor liab + emit!(TokenBalanceLog { + mango_group: ctx.accounts.group.key(), + mango_account: ctx.accounts.liqor.key(), + token_index: liab_token_index, + indexed_position: liqor_liab.indexed_position.to_bits(), + deposit_index: liab_deposit_index.to_bits(), + borrow_index: liab_borrow_index.to_bits(), + }); + // Check liqor's health if !liqor.fixed.is_in_health_region() { let liqor_health = @@ -185,16 +204,6 @@ pub fn token_liq_bankruptcy( require!(liqor_health >= 0, MangoError::HealthMustBePositive); } - // liqor quote - emit!(TokenBalanceLog { - mango_group: ctx.accounts.group.key(), - mango_account: ctx.accounts.liqor.key(), - token_index: QUOTE_TOKEN_INDEX, - indexed_position: liqor_quote_indexed_position.to_bits(), - deposit_index: quote_deposit_index.to_bits(), - borrow_index: quote_borrow_index.to_bits(), - }); - if loan_origination_fee.is_positive() { emit!(WithdrawLoanOriginationFeeLog { mango_group: ctx.accounts.group.key(), @@ -206,10 +215,16 @@ pub fn token_liq_bankruptcy( } if !liqor_quote_active { - liqor.deactivate_token_position(liqor_quote_raw_token_index); + liqor.deactivate_token_position_and_log( + liqor_quote_raw_token_index, + ctx.accounts.liqor.key(), + ); } if !liqor_liab_active { - liqor.deactivate_token_position(liqor_liab_raw_token_index); + liqor.deactivate_token_position_and_log( + liqor_liab_raw_token_index, + ctx.accounts.liqor.key(), + ); } } else { // For liab_token_index == QUOTE_TOKEN_INDEX: the insurance fund deposits directly into liqee, @@ -265,16 +280,6 @@ pub fn token_liq_bankruptcy( require_eq!(liqee_liab.indexed_position, I80F48::ZERO); } - // liqor liab - emit!(TokenBalanceLog { - mango_group: ctx.accounts.group.key(), - mango_account: ctx.accounts.liqor.key(), - token_index: liab_token_index, - indexed_position: liqee_liab.indexed_position.to_bits(), - deposit_index: liab_deposit_index.to_bits(), - borrow_index: liab_borrow_index.to_bits(), - }); - // liqee liab emit!(TokenBalanceLog { mango_group: ctx.accounts.group.key(), @@ -297,7 +302,7 @@ pub fn token_liq_bankruptcy( .maybe_recover_from_being_liquidated(liqee_init_health); if !liqee_liab_active { - liqee.deactivate_token_position(liqee_raw_token_index); + liqee.deactivate_token_position_and_log(liqee_raw_token_index, ctx.accounts.liqee.key()); } emit!(LiquidateTokenBankruptcyLog { 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 76fae6b82..55c8aa9c3 100644 --- a/programs/mango-v4/src/instructions/token_liq_with_token.rs +++ b/programs/mango-v4/src/instructions/token_liq_with_token.rs @@ -143,24 +143,24 @@ pub fn token_liq_with_token( // Apply the balance changes to the liqor and liqee accounts let liqee_liab_position = liqee.token_position_mut_by_raw_index(liqee_liab_raw_index); let liqee_liab_active = liab_bank.deposit_with_dusting(liqee_liab_position, liab_transfer)?; - let liqee_liab_position_indexed = liqee_liab_position.indexed_position; + let liqee_liab_indexed_position = liqee_liab_position.indexed_position; let (liqor_liab_position, liqor_liab_raw_index, _) = liqor.ensure_token_position(liab_token_index)?; let (liqor_liab_active, loan_origination_fee) = liab_bank.withdraw_with_fee(liqor_liab_position, liab_transfer)?; - let liqor_liab_position_indexed = liqor_liab_position.indexed_position; + let liqor_liab_indexed_position = liqor_liab_position.indexed_position; let liqee_liab_native_after = liqee_liab_position.native(liab_bank); let (liqor_asset_position, liqor_asset_raw_index, _) = liqor.ensure_token_position(asset_token_index)?; let liqor_asset_active = asset_bank.deposit(liqor_asset_position, asset_transfer)?; - let liqor_asset_position_indexed = liqor_asset_position.indexed_position; + let liqor_asset_indexed_position = liqor_asset_position.indexed_position; let liqee_asset_position = liqee.token_position_mut_by_raw_index(liqee_asset_raw_index); let liqee_asset_active = asset_bank.withdraw_without_fee_with_dusting(liqee_asset_position, asset_transfer)?; - let liqee_asset_position_indexed = liqee_asset_position.indexed_position; + let liqee_asset_indexed_position = liqee_asset_position.indexed_position; let liqee_assets_native_after = liqee_asset_position.native(asset_bank); // Update the health cache @@ -184,7 +184,7 @@ pub fn token_liq_with_token( mango_group: ctx.accounts.group.key(), mango_account: ctx.accounts.liqee.key(), token_index: asset_token_index, - indexed_position: liqee_asset_position_indexed.to_bits(), + indexed_position: liqee_asset_indexed_position.to_bits(), deposit_index: asset_bank.deposit_index.to_bits(), borrow_index: asset_bank.borrow_index.to_bits(), }); @@ -193,7 +193,7 @@ pub fn token_liq_with_token( mango_group: ctx.accounts.group.key(), mango_account: ctx.accounts.liqee.key(), token_index: liab_token_index, - indexed_position: liqee_liab_position_indexed.to_bits(), + indexed_position: liqee_liab_indexed_position.to_bits(), deposit_index: liab_bank.deposit_index.to_bits(), borrow_index: liab_bank.borrow_index.to_bits(), }); @@ -202,7 +202,7 @@ pub fn token_liq_with_token( mango_group: ctx.accounts.group.key(), mango_account: ctx.accounts.liqor.key(), token_index: asset_token_index, - indexed_position: liqor_asset_position_indexed.to_bits(), + indexed_position: liqor_asset_indexed_position.to_bits(), deposit_index: asset_bank.deposit_index.to_bits(), borrow_index: asset_bank.borrow_index.to_bits(), }); @@ -211,7 +211,7 @@ pub fn token_liq_with_token( mango_group: ctx.accounts.group.key(), mango_account: ctx.accounts.liqor.key(), token_index: liab_token_index, - indexed_position: liqor_liab_position_indexed.to_bits(), + indexed_position: liqor_liab_indexed_position.to_bits(), deposit_index: liab_bank.deposit_index.to_bits(), borrow_index: liab_bank.borrow_index.to_bits(), }); @@ -228,16 +228,16 @@ pub fn token_liq_with_token( // Since we use a scanning account retriever, it's safe to deactivate inactive token positions if !liqee_asset_active { - liqee.deactivate_token_position(liqee_asset_raw_index); + liqee.deactivate_token_position_and_log(liqee_asset_raw_index, ctx.accounts.liqee.key()); } if !liqee_liab_active { - liqee.deactivate_token_position(liqee_liab_raw_index); + liqee.deactivate_token_position_and_log(liqee_liab_raw_index, ctx.accounts.liqee.key()); } if !liqor_asset_active { - liqor.deactivate_token_position(liqor_asset_raw_index); + liqor.deactivate_token_position_and_log(liqor_asset_raw_index, ctx.accounts.liqor.key()); } if !liqor_liab_active { - liqor.deactivate_token_position(liqor_liab_raw_index) + liqor.deactivate_token_position_and_log(liqor_liab_raw_index, ctx.accounts.liqor.key()) } // Check liqee health again diff --git a/programs/mango-v4/src/instructions/token_withdraw.rs b/programs/mango-v4/src/instructions/token_withdraw.rs index d2063ed6f..479ce08ca 100644 --- a/programs/mango-v4/src/instructions/token_withdraw.rs +++ b/programs/mango-v4/src/instructions/token_withdraw.rs @@ -152,7 +152,7 @@ pub fn token_withdraw(ctx: Context, amount: u64, allow_borrow: bo // deactivated. // if !position_is_active { - account.deactivate_token_position(raw_token_index); + account.deactivate_token_position_and_log(raw_token_index, ctx.accounts.account.key()); } emit!(WithdrawLog { diff --git a/programs/mango-v4/src/logs.rs b/programs/mango-v4/src/logs.rs index ef2be05be..9af48dfc0 100644 --- a/programs/mango-v4/src/logs.rs +++ b/programs/mango-v4/src/logs.rs @@ -207,3 +207,12 @@ pub struct LiquidateTokenBankruptcyLog { pub insurance_transfer: i128, pub socialized_loss: i128, } + +#[event] +pub struct DeactivateTokenPositionLog { + pub mango_group: Pubkey, + pub mango_account: Pubkey, + pub token_index: u16, + pub cumulative_deposit_interest: f32, + pub cumulative_borrow_interest: f32, +} diff --git a/programs/mango-v4/src/state/bank.rs b/programs/mango-v4/src/state/bank.rs index c28c80101..b2a47275f 100644 --- a/programs/mango-v4/src/state/bank.rs +++ b/programs/mango-v4/src/state/bank.rs @@ -210,6 +210,7 @@ impl Bank { ) -> Result { require_gte!(native_amount, 0); let native_position = position.native(self); + let opening_indexed_position = position.indexed_position; // Adding DELTA to amount/index helps because (amount/index)*index <= amount, but // we want to ensure that users can withdraw the same amount they have deposited, so @@ -235,12 +236,14 @@ impl Bank { // pay back borrows only, leaving a negative position cm!(self.indexed_borrows -= indexed_change); position.indexed_position = new_indexed_value; + self.update_cumulative_interest(position, opening_indexed_position); return Ok(true); } else if new_native_position < I80F48::ONE && allow_dusting { // if there's less than one token deposited, zero the position cm!(self.dust += new_native_position); cm!(self.indexed_borrows += position.indexed_position); position.indexed_position = I80F48::ZERO; + self.update_cumulative_interest(position, opening_indexed_position); return Ok(false); } @@ -256,6 +259,7 @@ impl Bank { let indexed_change = div_rounding_up(native_amount, self.deposit_index); cm!(self.indexed_deposits += indexed_change); cm!(position.indexed_position += indexed_change); + self.update_cumulative_interest(position, opening_indexed_position); Ok(true) } @@ -317,6 +321,7 @@ impl Bank { ) -> Result<(bool, I80F48)> { require_gte!(native_amount, 0); let native_position = position.native(self); + let opening_indexed_position = position.indexed_position; if native_position.is_positive() { let new_native_position = cm!(native_position - native_amount); @@ -327,12 +332,14 @@ impl Bank { cm!(self.dust += new_native_position); cm!(self.indexed_deposits -= position.indexed_position); position.indexed_position = I80F48::ZERO; + self.update_cumulative_interest(position, opening_indexed_position); return Ok((false, I80F48::ZERO)); } else { // withdraw some deposits leaving a positive balance let indexed_change = cm!(native_amount / self.deposit_index); cm!(self.indexed_deposits -= indexed_change); cm!(position.indexed_position -= indexed_change); + self.update_cumulative_interest(position, opening_indexed_position); return Ok((true, I80F48::ZERO)); } } @@ -355,6 +362,7 @@ impl Bank { let indexed_change = cm!(native_amount / self.borrow_index); cm!(self.indexed_borrows += indexed_change); cm!(position.indexed_position -= indexed_change); + self.update_cumulative_interest(position, opening_indexed_position); Ok((true, loan_origination_fee)) } @@ -401,6 +409,30 @@ impl Bank { } } + pub fn update_cumulative_interest( + &self, + position: &mut TokenPosition, + opening_indexed_position: I80F48, + ) { + if opening_indexed_position.is_positive() { + let interest = + cm!((self.deposit_index - position.previous_index) * opening_indexed_position) + .to_num::(); + position.cumulative_deposit_interest += interest; + } else { + let interest = + cm!((self.borrow_index - position.previous_index) * opening_indexed_position) + .to_num::(); + position.cumulative_borrow_interest += interest; + } + + if position.indexed_position.is_positive() { + position.previous_index = self.deposit_index + } else { + position.previous_index = self.borrow_index + } + } + pub fn compute_index( &self, indexed_total_deposits: I80F48, @@ -634,8 +666,11 @@ mod tests { indexed_position: I80F48::ZERO, token_index: 0, in_use_count: if is_in_use { 1 } else { 0 }, + cumulative_deposit_interest: 0.0, + cumulative_borrow_interest: 0.0, + previous_index: I80F48::ZERO, padding: Default::default(), - reserved: [0; 40], + reserved: [0; 16], }; account.indexed_position = indexed(I80F48::from_num(start), &bank); diff --git a/programs/mango-v4/src/state/mango_account.rs b/programs/mango-v4/src/state/mango_account.rs index a7a9a24ee..f16bacf2a 100644 --- a/programs/mango-v4/src/state/mango_account.rs +++ b/programs/mango-v4/src/state/mango_account.rs @@ -24,6 +24,7 @@ use super::TokenIndex; use super::FREE_ORDER_SLOT; use super::{HealthCache, HealthType}; use super::{PerpPosition, Serum3Orders, TokenPosition}; +use crate::logs::DeactivateTokenPositionLog; use checked_math as cm; type BorshVecLength = u32; @@ -72,10 +73,12 @@ pub struct MangoAccount { pub padding: [u8; 1], + // (Display only) // Cumulative (deposits - withdraws) // using USD prices at the time of the deposit/withdraw // in USD units with 6 decimals pub net_deposits: i64, + // (Display only) // Cumulative settles on perp positions // TODO: unimplemented pub net_settled: i64, @@ -616,8 +619,11 @@ impl< indexed_position: I80F48::ZERO, token_index, in_use_count: 0, + cumulative_deposit_interest: 0.0, + cumulative_borrow_interest: 0.0, + previous_index: I80F48::ZERO, padding: Default::default(), - reserved: [0; 40], + reserved: [0; 16], }; } Ok((v, raw_index, bank_index)) @@ -632,6 +638,24 @@ impl< self.token_position_mut_by_raw_index(raw_index).token_index = TokenIndex::MAX; } + pub fn deactivate_token_position_and_log( + &mut self, + raw_index: usize, + mango_account_pubkey: Pubkey, + ) { + let mango_group = self.fixed.deref_or_borrow().group; + let token_position = self.token_position_mut_by_raw_index(raw_index); + assert!(token_position.in_use_count == 0); + emit!(DeactivateTokenPositionLog { + mango_group: mango_group, + mango_account: mango_account_pubkey, + token_index: token_position.token_index, + cumulative_deposit_interest: token_position.cumulative_deposit_interest, + cumulative_borrow_interest: token_position.cumulative_borrow_interest, + }); + self.token_position_mut_by_raw_index(raw_index).token_index = TokenIndex::MAX; + } + // get mut Serum3Orders at raw_index pub fn serum3_orders_mut_by_raw_index(&mut self, raw_index: usize) -> &mut Serum3Orders { let offset = self.header().serum3_offset(raw_index); diff --git a/programs/mango-v4/src/state/mango_account_components.rs b/programs/mango-v4/src/state/mango_account_components.rs index 32c4924d1..f774b1099 100644 --- a/programs/mango-v4/src/state/mango_account_components.rs +++ b/programs/mango-v4/src/state/mango_account_components.rs @@ -32,13 +32,24 @@ pub struct TokenPosition { pub padding: [u8; 5], #[derivative(Debug = "ignore")] - pub reserved: [u8; 40], + pub reserved: [u8; 16], + + // bookkeeping variable for onchain interest calculation + // either deposit_index or borrow_index at last indexed_position change + pub previous_index: I80F48, + + // (Display only) + // Cumulative deposit interest in token native units + pub cumulative_deposit_interest: f32, + // (Display only) + // Cumulative borrow interest in token native units + pub cumulative_borrow_interest: f32, } unsafe impl bytemuck::Pod for TokenPosition {} unsafe impl bytemuck::Zeroable for TokenPosition {} -const_assert_eq!(size_of::(), 24 + 40); +const_assert_eq!(size_of::(), 64); const_assert_eq!(size_of::() % 8, 0); impl Default for TokenPosition { @@ -47,8 +58,11 @@ impl Default for TokenPosition { indexed_position: I80F48::ZERO, token_index: TokenIndex::MAX, in_use_count: 0, + cumulative_deposit_interest: 0.0, + cumulative_borrow_interest: 0.0, + previous_index: I80F48::ZERO, padding: Default::default(), - reserved: [0; 40], + reserved: [0; 16], } } } diff --git a/ts/client/src/mango_v4.ts b/ts/client/src/mango_v4.ts index e5d24c10a..c65f1b7b6 100644 --- a/ts/client/src/mango_v4.ts +++ b/ts/client/src/mango_v4.ts @@ -4487,9 +4487,23 @@ export type MangoV4 = { "type": { "array": [ "u8", - 40 + 8 ] } + }, + { + "name": "previousIndex", + "type": { + "defined": "I80F48" + } + }, + { + "name": "cumulativeDepositInterest", + "type": "i64" + }, + { + "name": "cumulativeBorrowInterest", + "type": "i64" } ] } @@ -5978,6 +5992,36 @@ export type MangoV4 = { "index": false } ] + }, + { + "name": "DeactivateTokenPositionLog", + "fields": [ + { + "name": "mangoGroup", + "type": "publicKey", + "index": false + }, + { + "name": "mangoAccount", + "type": "publicKey", + "index": false + }, + { + "name": "tokenIndex", + "type": "u16", + "index": false + }, + { + "name": "cumulativeDepositInterest", + "type": "i64", + "index": false + }, + { + "name": "cumulativeBorrowInterest", + "type": "i64", + "index": false + } + ] } ], "errors": [ @@ -10588,9 +10632,23 @@ export const IDL: MangoV4 = { "type": { "array": [ "u8", - 40 + 8 ] } + }, + { + "name": "previousIndex", + "type": { + "defined": "I80F48" + } + }, + { + "name": "cumulativeDepositInterest", + "type": "i64" + }, + { + "name": "cumulativeBorrowInterest", + "type": "i64" } ] } @@ -12079,6 +12137,36 @@ export const IDL: MangoV4 = { "index": false } ] + }, + { + "name": "DeactivateTokenPositionLog", + "fields": [ + { + "name": "mangoGroup", + "type": "publicKey", + "index": false + }, + { + "name": "mangoAccount", + "type": "publicKey", + "index": false + }, + { + "name": "tokenIndex", + "type": "u16", + "index": false + }, + { + "name": "cumulativeDepositInterest", + "type": "i64", + "index": false + }, + { + "name": "cumulativeBorrowInterest", + "type": "i64", + "index": false + } + ] } ], "errors": [