From ae8ab6d34e730bb1b57357ca64fc168f4952cf32 Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Wed, 6 Mar 2024 13:22:47 +0100 Subject: [PATCH 01/23] wip - for sanity, the liquidator likely must have lendable positions too - instruction for creating and closing an unlendable position - heaps of tests, rs client changes, ts client changes :/ Done: - when dealing with the vault, always keep unlendable_deposits in the vault (unless withdrawing a no-lending position) - no-lending positions can't work as settle token positions for perps --- programs/mango-v4/src/error.rs | 2 + .../instructions/admin_perp_withdraw_fees.rs | 8 +- .../instructions/admin_token_withdraw_fees.rs | 7 +- .../mango-v4/src/instructions/flash_loan.rs | 12 +++ .../src/instructions/serum3_place_order.rs | 8 ++ .../src/instructions/token_force_withdraw.rs | 7 +- .../src/instructions/token_register.rs | 4 +- .../instructions/token_register_trustless.rs | 4 +- .../src/instructions/token_withdraw.rs | 7 +- programs/mango-v4/src/state/bank.rs | 96 ++++++++++++++----- programs/mango-v4/src/state/mango_account.rs | 8 ++ .../src/state/mango_account_components.rs | 43 ++++++--- 12 files changed, 161 insertions(+), 45 deletions(-) diff --git a/programs/mango-v4/src/error.rs b/programs/mango-v4/src/error.rs index c7510ba3cb..9268fc4f1a 100644 --- a/programs/mango-v4/src/error.rs +++ b/programs/mango-v4/src/error.rs @@ -147,6 +147,8 @@ pub enum MangoError { TokenAssetLiquidationDisabled, #[msg("for borrows the bank must be in the health account list")] BorrowsRequireHealthAccountBank, + #[msg("perp settle token position does not support borrows")] + PerpSettleTokenPositionMustSupportBorrows, } impl MangoError { diff --git a/programs/mango-v4/src/instructions/admin_perp_withdraw_fees.rs b/programs/mango-v4/src/instructions/admin_perp_withdraw_fees.rs index 0c43929a9b..5589faaca5 100644 --- a/programs/mango-v4/src/instructions/admin_perp_withdraw_fees.rs +++ b/programs/mango-v4/src/instructions/admin_perp_withdraw_fees.rs @@ -6,10 +6,16 @@ use crate::{accounts_ix::*, group_seeds}; pub fn admin_perp_withdraw_fees(ctx: Context) -> Result<()> { let group = ctx.accounts.group.load()?; let mut perp_market = ctx.accounts.perp_market.load_mut()?; + let bank = ctx.accounts.bank.load()?; let group_seeds = group_seeds!(group); let fees = perp_market.fees_settled.floor().to_num::() - perp_market.fees_withdrawn; - let amount = fees.min(ctx.accounts.vault.amount); + let amount = fees.min( + ctx.accounts + .vault + .amount + .saturating_sub(bank.unlendable_deposits), + ); token::transfer( ctx.accounts.transfer_ctx().with_signer(&[group_seeds]), amount, diff --git a/programs/mango-v4/src/instructions/admin_token_withdraw_fees.rs b/programs/mango-v4/src/instructions/admin_token_withdraw_fees.rs index fc8f349a5e..4c58c0fbc1 100644 --- a/programs/mango-v4/src/instructions/admin_token_withdraw_fees.rs +++ b/programs/mango-v4/src/instructions/admin_token_withdraw_fees.rs @@ -9,7 +9,12 @@ pub fn admin_token_withdraw_fees(ctx: Context) -> Result let group_seeds = group_seeds!(group); let fees = bank.collected_fees_native.floor().to_num::() - bank.fees_withdrawn; - let amount = fees.min(ctx.accounts.vault.amount); + let amount = fees.min( + ctx.accounts + .vault + .amount + .saturating_sub(bank.unlendable_deposits), + ); token::transfer( ctx.accounts.transfer_ctx().with_signer(&[group_seeds]), amount, diff --git a/programs/mango-v4/src/instructions/flash_loan.rs b/programs/mango-v4/src/instructions/flash_loan.rs index 09defac677..ea11be6411 100644 --- a/programs/mango-v4/src/instructions/flash_loan.rs +++ b/programs/mango-v4/src/instructions/flash_loan.rs @@ -257,6 +257,7 @@ struct TokenVaultChange { bank_index: usize, raw_token_index: usize, amount: I80F48, + vault_balance: u64, } pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>( @@ -364,6 +365,7 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>( bank_index: i, raw_token_index, amount: change, + vault_balance: token_account.amount, }); } @@ -487,6 +489,16 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>( bank.check_deposit_and_oo_limit()?; } + if change.amount < 0 { + // Verify that the no-lending amount on the vault remains. If the acting account + // itself had a no-lending position, the unlendable_deposits has already been reduced. + require_gte!( + change.vault_balance, + bank.unlendable_deposits, + MangoError::InsufficentBankVaultFunds + ); + } + bank.flash_loan_approved_amount = 0; bank.flash_loan_token_account_initial = u64::MAX; diff --git a/programs/mango-v4/src/instructions/serum3_place_order.rs b/programs/mango-v4/src/instructions/serum3_place_order.rs index 80d43fb18a..04ae58a833 100644 --- a/programs/mango-v4/src/instructions/serum3_place_order.rs +++ b/programs/mango-v4/src/instructions/serum3_place_order.rs @@ -331,6 +331,14 @@ pub fn serum3_place_order( )? }; + // Verify that the no-lending amount on the vault remains. If the acting account + // itself had a no-lending position, the unlendable_deposits has already been reduced. + require_gte!( + after_vault, + payer_bank.unlendable_deposits, + MangoError::InsufficentBankVaultFunds + ); + // Deposit limit check, receiver side: // Placing an order can always increase the receiver bank deposits on fill. { diff --git a/programs/mango-v4/src/instructions/token_force_withdraw.rs b/programs/mango-v4/src/instructions/token_force_withdraw.rs index 70eade859b..1479ce9e32 100644 --- a/programs/mango-v4/src/instructions/token_force_withdraw.rs +++ b/programs/mango-v4/src/instructions/token_force_withdraw.rs @@ -36,11 +36,12 @@ pub fn token_force_withdraw(ctx: Context) -> Result<()> { let position_is_active = bank.withdraw_without_fee(position, amount_i80f48, now_ts)?; // Provide a readable error message in case the vault doesn't have enough tokens - if ctx.accounts.vault.amount < amount { + // Note that unlendable_deposits has already been reduced above. + if ctx.accounts.vault.amount < bank.unlendable_deposits + amount { return err!(MangoError::InsufficentBankVaultFunds).with_context(|| { format!( - "bank vault does not have enough tokens, need {} but have {}", - amount, ctx.accounts.vault.amount + "bank vault does not have enough tokens, need {} but have {} ({} unlendable)", + amount, ctx.accounts.vault.amount, bank.unlendable_deposits ) }); } diff --git a/programs/mango-v4/src/instructions/token_register.rs b/programs/mango-v4/src/instructions/token_register.rs index 7d545ad97e..e9fd7ac5d9 100644 --- a/programs/mango-v4/src/instructions/token_register.rs +++ b/programs/mango-v4/src/instructions/token_register.rs @@ -133,7 +133,9 @@ pub fn token_register( collected_liquidation_fees: I80F48::ZERO, collected_collateral_fees: I80F48::ZERO, collateral_fee_per_day, - reserved: [0; 1900], + padding2: Default::default(), + unlendable_deposits: 0, + reserved: [0; 1888], }; let oracle_ref = &AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?; diff --git a/programs/mango-v4/src/instructions/token_register_trustless.rs b/programs/mango-v4/src/instructions/token_register_trustless.rs index 6b62842286..c65a5c7cde 100644 --- a/programs/mango-v4/src/instructions/token_register_trustless.rs +++ b/programs/mango-v4/src/instructions/token_register_trustless.rs @@ -111,7 +111,9 @@ pub fn token_register_trustless( collected_liquidation_fees: I80F48::ZERO, collected_collateral_fees: I80F48::ZERO, collateral_fee_per_day: 0.0, // TODO - reserved: [0; 1900], + padding2: Default::default(), + unlendable_deposits: 0, + reserved: [0; 1888], }; let oracle_ref = &AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?; if let Ok(oracle_price) = bank.oracle_price(&OracleAccountInfos::from_reader(oracle_ref), None) diff --git a/programs/mango-v4/src/instructions/token_withdraw.rs b/programs/mango-v4/src/instructions/token_withdraw.rs index 4b79760b79..a03f34b1f7 100644 --- a/programs/mango-v4/src/instructions/token_withdraw.rs +++ b/programs/mango-v4/src/instructions/token_withdraw.rs @@ -91,11 +91,12 @@ pub fn token_withdraw(ctx: Context, amount: u64, allow_borrow: bo let bank = ctx.accounts.bank.load()?; // Provide a readable error message in case the vault doesn't have enough tokens - if ctx.accounts.vault.amount < amount { + // Note that unlendable_deposits has already been reduced above. + if ctx.accounts.vault.amount < bank.unlendable_deposits + amount { return err!(MangoError::InsufficentBankVaultFunds).with_context(|| { format!( - "bank vault does not have enough tokens, need {} but have {}", - amount, ctx.accounts.vault.amount + "bank vault does not have enough tokens, need {} but have {} ({} unlendable)", + amount, ctx.accounts.vault.amount, bank.unlendable_deposits ) }); } diff --git a/programs/mango-v4/src/state/bank.rs b/programs/mango-v4/src/state/bank.rs index cfc6aea3d0..92659334f0 100644 --- a/programs/mango-v4/src/state/bank.rs +++ b/programs/mango-v4/src/state/bank.rs @@ -232,7 +232,12 @@ pub struct Bank { pub collateral_fee_per_day: f32, #[derivative(Debug = "ignore")] - pub reserved: [u8; 1900], + pub padding2: [u8; 4], + + pub unlendable_deposits: u64, + + #[derivative(Debug = "ignore")] + pub reserved: [u8; 1888], } const_assert_eq!( size_of::(), @@ -271,7 +276,9 @@ const_assert_eq!( + 8 + 16 * 4 + 4 - + 1900 + + 4 + + 8 + + 1888 ); const_assert_eq!(size_of::(), 3064); const_assert_eq!(size_of::() % 8, 0); @@ -322,6 +329,7 @@ impl Bank { flash_loan_token_account_initial: u64::MAX, net_borrows_in_window: 0, potential_serum_tokens: 0, + unlendable_deposits: 0, bump, bank_num, @@ -382,7 +390,8 @@ impl Bank { zero_util_rate: existing_bank.zero_util_rate, platform_liquidation_fee: existing_bank.platform_liquidation_fee, collateral_fee_per_day: existing_bank.collateral_fee_per_day, - reserved: [0; 1900], + padding2: [0; 4], + reserved: [0; 1888], } } @@ -534,7 +543,7 @@ impl Bank { native_amount: I80F48, now_ts: u64, ) -> Result { - self.deposit_internal_wrapper(position, native_amount, !position.is_in_use(), now_ts) + self.deposit_internal_wrapper(position, native_amount, position.can_auto_close(), now_ts) } /// Like `deposit()`, but allows dusting of in-use accounts. @@ -547,7 +556,7 @@ impl Bank { now_ts: u64, ) -> Result { self.deposit_internal_wrapper(position, native_amount, true, now_ts) - .map(|not_dusted| not_dusted || position.is_in_use()) + .map(|not_dusted| not_dusted || !position.can_auto_close()) } pub fn deposit_internal_wrapper( @@ -557,10 +566,21 @@ impl Bank { allow_dusting: bool, now_ts: u64, ) -> Result { - let opening_indexed_position = position.indexed_position; - let result = self.deposit_internal(position, native_amount, allow_dusting, now_ts)?; - self.update_cumulative_interest(position, opening_indexed_position); - Ok(result) + if position.allow_lending() { + let opening_indexed_position = position.indexed_position; + let result = self.deposit_internal(position, native_amount, allow_dusting, now_ts)?; + self.update_cumulative_interest(position, opening_indexed_position); + Ok(result) + } else { + let deposit_amount = native_amount.floor(); + self.unlendable_deposits += deposit_amount.to_num::(); + self.dust += native_amount - deposit_amount; + position.indexed_position += deposit_amount; + // TODO: do these positions auto-delete themselves, like normal ones? + // sounds like users might accidentally switch over to lendable positions that way, + // better make it explicit! + Ok(true) + } } /// Internal function to deposit funds @@ -572,6 +592,7 @@ impl Bank { now_ts: u64, ) -> Result { require_gte!(native_amount, 0); + assert!(position.allow_lending()); let native_position = position.native(self); @@ -646,7 +667,7 @@ impl Bank { position, native_amount, false, - !position.is_in_use(), + position.can_auto_close(), now_ts, )? .position_is_active; @@ -664,7 +685,7 @@ impl Bank { now_ts: u64, ) -> Result { self.withdraw_internal_wrapper(position, native_amount, false, true, now_ts) - .map(|withdraw_result| withdraw_result.position_is_active || position.is_in_use()) + .map(|withdraw_result| withdraw_result.position_is_active || !position.can_auto_close()) } /// Withdraws `native_amount` while applying the loan origination fee if a borrow is created. @@ -681,7 +702,13 @@ impl Bank { native_amount: I80F48, now_ts: u64, ) -> Result { - self.withdraw_internal_wrapper(position, native_amount, true, !position.is_in_use(), now_ts) + self.withdraw_internal_wrapper( + position, + native_amount, + true, + position.can_auto_close(), + now_ts, + ) } /// Internal function to withdraw funds @@ -693,16 +720,31 @@ impl Bank { allow_dusting: bool, now_ts: u64, ) -> Result { - let opening_indexed_position = position.indexed_position; - let res = self.withdraw_internal( - position, - native_amount, - with_loan_origination_fee, - allow_dusting, - now_ts, - ); - self.update_cumulative_interest(position, opening_indexed_position); - res + if position.allow_lending() { + let opening_indexed_position = position.indexed_position; + let res = self.withdraw_internal( + position, + native_amount, + with_loan_origination_fee, + allow_dusting, + now_ts, + ); + self.update_cumulative_interest(position, opening_indexed_position); + res + } else { + // TODO: might there be trouble by rounding up here? + let withdraw_amount = native_amount.ceil(); + self.unlendable_deposits -= withdraw_amount.to_num::(); + position.indexed_position -= withdraw_amount; + // TODO: do these positions auto-delete themselves, like normal ones? + // sounds like users might accidentally switch over to lendable positions that way, + // better make it explicit! + Ok(WithdrawResult { + position_is_active: true, + loan_amount: I80F48::ZERO, + loan_origination_fee: I80F48::ZERO, + }) + } } /// Internal function to withdraw funds @@ -789,7 +831,7 @@ impl Bank { position, loan_origination_fee, false, - !position.is_in_use(), + position.can_auto_close(), now_ts, )? .position_is_active; @@ -804,7 +846,7 @@ impl Bank { /// Returns true if the position remains active pub fn dust_if_possible(&mut self, position: &mut TokenPosition, now_ts: u64) -> Result { - if position.is_in_use() { + if !position.can_auto_close() { return Ok(true); } let native = position.native(self); @@ -998,6 +1040,10 @@ impl Bank { position: &mut TokenPosition, opening_indexed_position: I80F48, ) { + if !position.allow_lending() { + return; + } + if opening_indexed_position.is_positive() { let interest = ((self.deposit_index - position.previous_index) * opening_indexed_position) @@ -1340,6 +1386,7 @@ mod tests { indexed_position: I80F48::ZERO, token_index: 0, in_use_count: u16::from(is_in_use), + disable_lending: 0, cumulative_deposit_interest: 0.0, cumulative_borrow_interest: 0.0, previous_index: I80F48::ZERO, @@ -1467,6 +1514,7 @@ mod tests { indexed_position: I80F48::ZERO, token_index: 0, in_use_count: 1, + disable_lending: 0, cumulative_deposit_interest: 0.0, cumulative_borrow_interest: 0.0, previous_index: I80F48::ZERO, diff --git a/programs/mango-v4/src/state/mango_account.rs b/programs/mango-v4/src/state/mango_account.rs index 99ea087817..c9bbf13108 100644 --- a/programs/mango-v4/src/state/mango_account.rs +++ b/programs/mango-v4/src/state/mango_account.rs @@ -1012,6 +1012,7 @@ impl< indexed_position: I80F48::ZERO, token_index, in_use_count: 0, + disable_lending: 0, cumulative_deposit_interest: 0.0, cumulative_borrow_interest: 0.0, previous_index: I80F48::ZERO, @@ -1161,6 +1162,13 @@ impl< perp_position.market_index = perp_market_index; let settle_token_position = self.ensure_token_position(settle_token_index)?.0; + // no-lending positions can't have negative balances can't work with perps: + // settlement must be able to move the position arbitrarily + require_msg_typed!( + settle_token_position.allow_lending(), + MangoError::PerpSettleTokenPositionMustSupportBorrows, + "the account's settle token position (index {settle_token_index}) is a no-lending position, unsuitable for perps" + ); settle_token_position.increment_in_use(); } } diff --git a/programs/mango-v4/src/state/mango_account_components.rs b/programs/mango-v4/src/state/mango_account_components.rs index 987f0e8a7d..1e7becb6f6 100644 --- a/programs/mango-v4/src/state/mango_account_components.rs +++ b/programs/mango-v4/src/state/mango_account_components.rs @@ -29,8 +29,20 @@ pub struct TokenPosition { /// incremented when a market requires this position to stay alive pub in_use_count: u16, + /// set to 1 when these deposits may not be lent out + /// + /// This has wide ranging consequences: + /// - only deposits possible, no borrows (TODO: that means no perps because settlement may cause borrows?) + /// - not accounted for in Bank.indexedDeposits + /// - instead tracked in Bank.unlendableDeposits (to ensure the vault always has them) + /// - indexed_position becomes stores straight token-native amount, fractional will always be 0 + /// + /// TODO: completely borks logging of indexed_position in many instructions + /// TODO: this means all fractional deposits/withdraws are dusted?! + pub disable_lending: u8, + #[derivative(Debug = "ignore")] - pub padding: [u8; 4], + pub padding: [u8; 3], // bookkeeping variable for onchain interest calculation // either deposit_index or borrow_index at last indexed_position change @@ -59,6 +71,7 @@ impl Default for TokenPosition { indexed_position: I80F48::ZERO, token_index: TokenIndex::MAX, in_use_count: 0, + disable_lending: 0, cumulative_deposit_interest: 0.0, cumulative_borrow_interest: 0.0, previous_index: I80F48::ZERO, @@ -78,28 +91,32 @@ impl TokenPosition { } pub fn native(&self, bank: &Bank) -> I80F48 { - if self.indexed_position.is_positive() { - self.indexed_position * bank.deposit_index + if self.allow_lending() { + if self.indexed_position.is_positive() { + self.indexed_position * bank.deposit_index + } else { + self.indexed_position * bank.borrow_index + } } else { - self.indexed_position * bank.borrow_index + self.indexed_position } } #[cfg(feature = "client")] pub fn ui(&self, bank: &Bank) -> I80F48 { - if self.indexed_position.is_positive() { - (self.indexed_position * bank.deposit_index) - / I80F48::from_num(10u64.pow(bank.mint_decimals as u32)) - } else { - (self.indexed_position * bank.borrow_index) - / I80F48::from_num(10u64.pow(bank.mint_decimals as u32)) - } + let native = self.native(bank); + native / I80F48::from_num(10u64.pow(bank.mint_decimals as u32)) } pub fn is_in_use(&self) -> bool { self.in_use_count > 0 } + // Positions that disable lending never auto-close + pub fn can_auto_close(&self) -> bool { + !self.is_in_use() && self.allow_lending() + } + pub fn increment_in_use(&mut self) { self.in_use_count += 1; // panic on overflow } @@ -107,6 +124,10 @@ impl TokenPosition { pub fn decrement_in_use(&mut self) { self.in_use_count = self.in_use_count.saturating_sub(1); } + + pub fn allow_lending(&self) -> bool { + self.disable_lending == 0 + } } #[zero_copy] From b1de91f25939653b954638702fa3045f36d7315e Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Wed, 6 Mar 2024 15:42:32 +0100 Subject: [PATCH 02/23] also dust surplus on withdraw --- programs/mango-v4/src/state/bank.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/programs/mango-v4/src/state/bank.rs b/programs/mango-v4/src/state/bank.rs index 92659334f0..bb45ba2c85 100644 --- a/programs/mango-v4/src/state/bank.rs +++ b/programs/mango-v4/src/state/bank.rs @@ -576,9 +576,6 @@ impl Bank { self.unlendable_deposits += deposit_amount.to_num::(); self.dust += native_amount - deposit_amount; position.indexed_position += deposit_amount; - // TODO: do these positions auto-delete themselves, like normal ones? - // sounds like users might accidentally switch over to lendable positions that way, - // better make it explicit! Ok(true) } } @@ -734,11 +731,10 @@ impl Bank { } else { // TODO: might there be trouble by rounding up here? let withdraw_amount = native_amount.ceil(); + self.dust += withdraw_amount - native_amount; self.unlendable_deposits -= withdraw_amount.to_num::(); position.indexed_position -= withdraw_amount; - // TODO: do these positions auto-delete themselves, like normal ones? - // sounds like users might accidentally switch over to lendable positions that way, - // better make it explicit! + require_gte!(position.indexed_position, I80F48::ZERO); // TODO: error Ok(WithdrawResult { position_is_active: true, loan_amount: I80F48::ZERO, From b97ec7bda1ff3b506aa104b95d80d77ce5e9319a Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Thu, 7 Mar 2024 13:17:00 +0100 Subject: [PATCH 03/23] ix to create and close positions --- programs/mango-v4/src/accounts_ix/mod.rs | 2 ++ .../token_create_or_close_position.rs | 25 ++++++++++++++ programs/mango-v4/src/error.rs | 6 ++++ programs/mango-v4/src/instructions/mod.rs | 4 +++ .../src/instructions/token_close_position.rs | 29 ++++++++++++++++ .../src/instructions/token_create_position.rs | 34 +++++++++++++++++++ programs/mango-v4/src/state/group.rs | 2 ++ 7 files changed, 102 insertions(+) create mode 100644 programs/mango-v4/src/accounts_ix/token_create_or_close_position.rs create mode 100644 programs/mango-v4/src/instructions/token_close_position.rs create mode 100644 programs/mango-v4/src/instructions/token_create_position.rs diff --git a/programs/mango-v4/src/accounts_ix/mod.rs b/programs/mango-v4/src/accounts_ix/mod.rs index df8ea1f307..fba7d1e1e1 100644 --- a/programs/mango-v4/src/accounts_ix/mod.rs +++ b/programs/mango-v4/src/accounts_ix/mod.rs @@ -64,6 +64,7 @@ pub use token_conditional_swap_cancel::*; pub use token_conditional_swap_create::*; pub use token_conditional_swap_start::*; pub use token_conditional_swap_trigger::*; +pub use token_create_or_close_position::*; pub use token_deposit::*; pub use token_deregister::*; pub use token_edit::*; @@ -142,6 +143,7 @@ mod token_conditional_swap_cancel; mod token_conditional_swap_create; mod token_conditional_swap_start; mod token_conditional_swap_trigger; +mod token_create_or_close_position; mod token_deposit; mod token_deregister; mod token_edit; diff --git a/programs/mango-v4/src/accounts_ix/token_create_or_close_position.rs b/programs/mango-v4/src/accounts_ix/token_create_or_close_position.rs new file mode 100644 index 0000000000..1459d9d845 --- /dev/null +++ b/programs/mango-v4/src/accounts_ix/token_create_or_close_position.rs @@ -0,0 +1,25 @@ +use anchor_lang::prelude::*; + +use crate::error::*; +use crate::state::*; + +#[derive(Accounts)] +pub struct TokenCreateOrClosePosition<'info> { + // ix gate checking happens in instruction code + pub group: AccountLoader<'info, Group>, + + #[account( + mut, + has_one = group, + constraint = account.load()?.is_operational() @ MangoError::AccountIsFrozen, + constraint = account.load()?.is_owner_or_delegate(owner.key()), + )] + pub account: AccountLoader<'info, MangoAccountFixed>, + pub owner: Signer<'info>, + + #[account( + mut, + has_one = group, + )] + pub bank: AccountLoader<'info, Bank>, +} diff --git a/programs/mango-v4/src/error.rs b/programs/mango-v4/src/error.rs index 9268fc4f1a..2ea3e91f46 100644 --- a/programs/mango-v4/src/error.rs +++ b/programs/mango-v4/src/error.rs @@ -149,6 +149,12 @@ pub enum MangoError { BorrowsRequireHealthAccountBank, #[msg("perp settle token position does not support borrows")] PerpSettleTokenPositionMustSupportBorrows, + #[msg("the token position is still in use by another position")] + TokenPositionIsInUse, + #[msg("the token position has a non-zero balance")] + TokenPositionBalanceNotZero, + #[msg("cannot have a lending and no-lending token position at the same time")] + TokenPositionWithDifferentSettingAlreadyExists, } impl MangoError { diff --git a/programs/mango-v4/src/instructions/mod.rs b/programs/mango-v4/src/instructions/mod.rs index faa5d8e88b..c37944b46f 100644 --- a/programs/mango-v4/src/instructions/mod.rs +++ b/programs/mango-v4/src/instructions/mod.rs @@ -51,10 +51,12 @@ pub use stub_oracle_create::*; pub use stub_oracle_set::*; pub use token_add_bank::*; pub use token_charge_collateral_fees::*; +pub use token_close_position::*; pub use token_conditional_swap_cancel::*; pub use token_conditional_swap_create::*; pub use token_conditional_swap_start::*; pub use token_conditional_swap_trigger::*; +pub use token_create_position::*; pub use token_deposit::*; pub use token_deregister::*; pub use token_edit::*; @@ -120,10 +122,12 @@ mod stub_oracle_create; mod stub_oracle_set; mod token_add_bank; mod token_charge_collateral_fees; +mod token_close_position; mod token_conditional_swap_cancel; mod token_conditional_swap_create; mod token_conditional_swap_start; mod token_conditional_swap_trigger; +mod token_create_position; mod token_deposit; mod token_deregister; mod token_edit; diff --git a/programs/mango-v4/src/instructions/token_close_position.rs b/programs/mango-v4/src/instructions/token_close_position.rs new file mode 100644 index 0000000000..cd014f920c --- /dev/null +++ b/programs/mango-v4/src/instructions/token_close_position.rs @@ -0,0 +1,29 @@ +use anchor_lang::prelude::*; +use fixed::types::I80F48; + +use crate::accounts_ix::*; +use crate::error::*; +use crate::state::*; + +pub fn token_close_position(ctx: Context) -> Result<()> { + let group = ctx.accounts.group.load()?; + require!( + group.is_ix_enabled(IxGate::TokenClosePosition), + MangoError::IxIsDisabled + ); + + let mut account = ctx.accounts.account.load_full_mut()?; + let bank = ctx.accounts.bank.load()?; + + let (tp, raw_index) = account.token_position_and_raw_index(bank.token_index)?; + require!(!tp.is_in_use(), MangoError::TokenPositionIsInUse); + require_eq!( + tp.native(&bank), + I80F48::ZERO, + MangoError::TokenPositionBalanceNotZero + ); + + account.deactivate_token_position_and_log(raw_index, ctx.accounts.account.key()); + + Ok(()) +} diff --git a/programs/mango-v4/src/instructions/token_create_position.rs b/programs/mango-v4/src/instructions/token_create_position.rs new file mode 100644 index 0000000000..04b7a2542f --- /dev/null +++ b/programs/mango-v4/src/instructions/token_create_position.rs @@ -0,0 +1,34 @@ +use anchor_lang::prelude::*; + +use crate::accounts_ix::*; +use crate::error::*; +use crate::state::*; + +pub fn token_create_position( + ctx: Context, + allow_lending: bool, +) -> Result<()> { + let group = ctx.accounts.group.load()?; + require!( + group.is_ix_enabled(IxGate::TokenCreatePosition), + MangoError::IxIsDisabled + ); + + let mut account = ctx.accounts.account.load_full_mut()?; + let bank = ctx.accounts.bank.load()?; + + // If there is one already, make it a no-op + if let Ok(tp) = account.token_position(bank.token_index) { + require_eq!( + tp.allow_lending(), + allow_lending, + MangoError::TokenPositionWithDifferentSettingAlreadyExists + ); + return Ok(()); + } + + let (tp, _, _) = account.ensure_token_position(bank.token_index)?; + tp.disable_lending = if allow_lending { 0 } else { 1 }; + + Ok(()) +} diff --git a/programs/mango-v4/src/state/group.rs b/programs/mango-v4/src/state/group.rs index 19fc8db03b..7909f651ed 100644 --- a/programs/mango-v4/src/state/group.rs +++ b/programs/mango-v4/src/state/group.rs @@ -246,6 +246,8 @@ pub enum IxGate { TokenConditionalSwapCreateLinearAuction = 70, Serum3PlaceOrderV2 = 71, TokenForceWithdraw = 72, + TokenCreatePosition = 73, + TokenClosePosition = 74, // NOTE: Adding new variants requires matching changes in ts and the ix_gate_set instruction. } From ca97f9125b6f9a8ea76bfe8f848fa52ba9fe3047 Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Thu, 7 Mar 2024 14:16:11 +0100 Subject: [PATCH 04/23] function for token balance logs --- .../account_buyback_fees_with_mngo.rs | 29 +++++------ .../mango-v4/src/instructions/flash_loan.rs | 12 ++--- .../perp_liq_base_or_positive_pnl.rs | 24 ++------- .../perp_liq_negative_pnl_or_bankruptcy.rs | 33 +++---------- .../src/instructions/perp_settle_fees.rs | 12 ++--- .../src/instructions/perp_settle_pnl.rs | 35 ++++--------- .../src/instructions/serum3_place_order.rs | 14 ++---- .../token_charge_collateral_fees.rs | 12 ++--- .../token_conditional_swap_start.rs | 21 ++------ .../token_conditional_swap_trigger.rs | 48 +++--------------- .../src/instructions/token_deposit.rs | 9 +--- .../token_force_close_borrows_with_token.rs | 48 +++--------------- .../src/instructions/token_force_withdraw.rs | 12 ++--- .../src/instructions/token_liq_bankruptcy.rs | 39 +++------------ .../src/instructions/token_liq_with_token.rs | 49 +++---------------- .../src/instructions/token_withdraw.rs | 15 ++---- programs/mango-v4/src/logs.rs | 14 +++++- 17 files changed, 94 insertions(+), 332 deletions(-) diff --git a/programs/mango-v4/src/instructions/account_buyback_fees_with_mngo.rs b/programs/mango-v4/src/instructions/account_buyback_fees_with_mngo.rs index f876fd4702..04ee05fb96 100644 --- a/programs/mango-v4/src/instructions/account_buyback_fees_with_mngo.rs +++ b/programs/mango-v4/src/instructions/account_buyback_fees_with_mngo.rs @@ -3,11 +3,12 @@ use fixed::types::I80F48; use crate::accounts_zerocopy::*; use crate::error::MangoError; +use crate::logs::emit_token_balance_log; use crate::state::*; use crate::accounts_ix::*; -use crate::logs::{emit_stack, AccountBuybackFeesWithMngoLog, TokenBalanceLog}; +use crate::logs::{emit_stack, AccountBuybackFeesWithMngoLog}; pub fn account_buyback_fees_with_mngo( ctx: Context, @@ -107,14 +108,11 @@ pub fn account_buyback_fees_with_mngo( ); let in_use = mngo_bank.withdraw_without_fee(account_mngo_token_position, max_buyback_mngo, now_ts)?; - emit_stack(TokenBalanceLog { - mango_group: ctx.accounts.group.key(), - mango_account: ctx.accounts.account.key(), - token_index: mngo_bank.token_index, - indexed_position: account_mngo_token_position.indexed_position.to_bits(), - deposit_index: mngo_bank.deposit_index.to_bits(), - borrow_index: mngo_bank.borrow_index.to_bits(), - }); + emit_token_balance_log( + ctx.accounts.account.key(), + &mngo_bank, + account_mngo_token_position, + ); if !in_use { account.deactivate_token_position_and_log( account_mngo_raw_token_index, @@ -139,14 +137,11 @@ pub fn account_buyback_fees_with_mngo( ); } let in_use = fees_bank.deposit(account_fees_token_position, max_buyback_fees, now_ts)?; - emit_stack(TokenBalanceLog { - mango_group: ctx.accounts.group.key(), - mango_account: ctx.accounts.account.key(), - token_index: fees_bank.token_index, - indexed_position: account_fees_token_position.indexed_position.to_bits(), - deposit_index: fees_bank.deposit_index.to_bits(), - borrow_index: fees_bank.borrow_index.to_bits(), - }); + emit_token_balance_log( + ctx.accounts.account.key(), + &fees_bank, + account_fees_token_position, + ); if !in_use { account.deactivate_token_position_and_log( account_fees_raw_token_index, diff --git a/programs/mango-v4/src/instructions/flash_loan.rs b/programs/mango-v4/src/instructions/flash_loan.rs index ea11be6411..8e72ad5970 100644 --- a/programs/mango-v4/src/instructions/flash_loan.rs +++ b/programs/mango-v4/src/instructions/flash_loan.rs @@ -3,7 +3,8 @@ use crate::accounts_zerocopy::*; use crate::error::*; use crate::group_seeds; use crate::health::*; -use crate::logs::{emit_stack, FlashLoanLogV3, FlashLoanTokenDetailV3, TokenBalanceLog}; +use crate::logs::emit_token_balance_log; +use crate::logs::{emit_stack, FlashLoanLogV3, FlashLoanTokenDetailV3}; use crate::state::*; use crate::util::clock_now; @@ -514,14 +515,7 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>( approved_amount: approved_amount_u64, }); - emit_stack(TokenBalanceLog { - mango_group: group.key(), - mango_account: ctx.accounts.account.key(), - token_index: bank.token_index as u16, - indexed_position: position.indexed_position.to_bits(), - deposit_index: bank.deposit_index.to_bits(), - borrow_index: bank.borrow_index.to_bits(), - }); + emit_token_balance_log(ctx.accounts.account.key(), &bank, position); } emit_stack(FlashLoanLogV3 { 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 54d1916561..ed6d536329 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 @@ -5,10 +5,11 @@ use fixed::types::I80F48; use crate::accounts_zerocopy::*; use crate::error::*; use crate::health::*; +use crate::logs::emit_token_balance_log; use crate::state::*; use crate::accounts_ix::*; -use crate::logs::{emit_perp_balances, emit_stack, PerpLiqBaseOrPositivePnlLogV3, TokenBalanceLog}; +use crate::logs::{emit_perp_balances, emit_stack, PerpLiqBaseOrPositivePnlLogV3}; /// This instruction deals with increasing health by: /// - reducing the liqee's base position @@ -137,25 +138,10 @@ pub fn perp_liq_base_or_positive_pnl( if pnl_transfer != 0 { let liqee_token_position = liqee.token_position(settle_token_index)?; - let liqor_token_position = liqor.token_position(settle_token_index)?; - - emit_stack(TokenBalanceLog { - mango_group: ctx.accounts.group.key(), - mango_account: ctx.accounts.liqee.key(), - token_index: settle_token_index, - indexed_position: liqee_token_position.indexed_position.to_bits(), - deposit_index: settle_bank.deposit_index.to_bits(), - borrow_index: settle_bank.borrow_index.to_bits(), - }); + emit_token_balance_log(ctx.accounts.liqee.key(), &settle_bank, liqee_token_position); - emit_stack(TokenBalanceLog { - mango_group: ctx.accounts.group.key(), - mango_account: ctx.accounts.liqor.key(), - token_index: settle_token_index, - indexed_position: liqor_token_position.indexed_position.to_bits(), - deposit_index: settle_bank.deposit_index.to_bits(), - borrow_index: settle_bank.borrow_index.to_bits(), - }); + let liqor_token_position = liqor.token_position(settle_token_index)?; + emit_token_balance_log(ctx.accounts.liqor.key(), &settle_bank, liqor_token_position); } if base_transfer != 0 || pnl_transfer != 0 { 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 49b8416f9c..342905815b 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 @@ -9,9 +9,9 @@ use crate::accounts_ix::*; use crate::accounts_zerocopy::AccountInfoRef; use crate::error::*; use crate::health::*; +use crate::logs::emit_token_balance_log; use crate::logs::{ emit_perp_balances, emit_stack, PerpLiqBankruptcyLog, PerpLiqNegativePnlOrBankruptcyLog, - TokenBalanceLog, }; use crate::state::*; @@ -138,37 +138,20 @@ pub fn perp_liq_negative_pnl_or_bankruptcy( if settlement > 0 { let settle_bank = ctx.accounts.settle_bank.load()?; let liqor_token_position = liqor.token_position(settle_token_index)?; - emit_stack(TokenBalanceLog { - mango_group, - mango_account: ctx.accounts.liqor.key(), - token_index: settle_token_index, - indexed_position: liqor_token_position.indexed_position.to_bits(), - deposit_index: settle_bank.deposit_index.to_bits(), - borrow_index: settle_bank.borrow_index.to_bits(), - }); + emit_token_balance_log(ctx.accounts.liqor.key(), &settle_bank, liqor_token_position); let liqee_token_position = liqee.token_position(settle_token_index)?; - emit_stack(TokenBalanceLog { - mango_group, - mango_account: ctx.accounts.liqee.key(), - token_index: settle_token_index, - indexed_position: liqee_token_position.indexed_position.to_bits(), - deposit_index: settle_bank.deposit_index.to_bits(), - borrow_index: settle_bank.borrow_index.to_bits(), - }); + emit_token_balance_log(ctx.accounts.liqee.key(), &settle_bank, liqee_token_position); } if insurance_transfer > 0 { let insurance_bank = ctx.accounts.insurance_bank.load()?; let liqor_token_position = liqor.token_position(insurance_bank.token_index)?; - emit_stack(TokenBalanceLog { - mango_group, - mango_account: ctx.accounts.liqor.key(), - token_index: insurance_bank.token_index, - indexed_position: liqor_token_position.indexed_position.to_bits(), - deposit_index: insurance_bank.deposit_index.to_bits(), - borrow_index: insurance_bank.borrow_index.to_bits(), - }); + emit_token_balance_log( + ctx.accounts.liqor.key(), + &insurance_bank, + liqor_token_position, + ); } let liqee_perp_position = liqee.perp_position(perp_market_index)?; diff --git a/programs/mango-v4/src/instructions/perp_settle_fees.rs b/programs/mango-v4/src/instructions/perp_settle_fees.rs index 627f484c5b..63e5b40560 100644 --- a/programs/mango-v4/src/instructions/perp_settle_fees.rs +++ b/programs/mango-v4/src/instructions/perp_settle_fees.rs @@ -5,10 +5,11 @@ use fixed::types::I80F48; use crate::accounts_zerocopy::*; use crate::error::*; use crate::health::{compute_health, new_fixed_order_account_retriever, HealthType}; +use crate::logs::emit_token_balance_log; use crate::state::*; use crate::accounts_ix::*; -use crate::logs::{emit_perp_balances, emit_stack, PerpSettleFeesLog, TokenBalanceLog}; +use crate::logs::{emit_perp_balances, emit_stack, PerpSettleFeesLog}; use crate::util::clock_now; pub fn perp_settle_fees(ctx: Context, max_settle_amount: u64) -> Result<()> { @@ -103,14 +104,7 @@ pub fn perp_settle_fees(ctx: Context, max_settle_amount: u64) -> // Update the settled balance on the market itself perp_market.fees_settled += settlement; - emit_stack(TokenBalanceLog { - mango_group: ctx.accounts.group.key(), - mango_account: ctx.accounts.account.key(), - token_index: perp_market.settle_token_index, - indexed_position: token_position.indexed_position.to_bits(), - deposit_index: settle_bank.deposit_index.to_bits(), - borrow_index: settle_bank.borrow_index.to_bits(), - }); + emit_token_balance_log(ctx.accounts.account.key(), &settle_bank, token_position); emit_stack(PerpSettleFeesLog { mango_group: ctx.accounts.group.key(), diff --git a/programs/mango-v4/src/instructions/perp_settle_pnl.rs b/programs/mango-v4/src/instructions/perp_settle_pnl.rs index 3e8bc79278..795dca1be5 100644 --- a/programs/mango-v4/src/instructions/perp_settle_pnl.rs +++ b/programs/mango-v4/src/instructions/perp_settle_pnl.rs @@ -6,7 +6,8 @@ use crate::accounts_ix::*; use crate::accounts_zerocopy::*; use crate::error::*; use crate::health::{new_health_cache, HealthType, ScanningAccountRetriever}; -use crate::logs::{emit_perp_balances, emit_stack, PerpSettlePnlLog, TokenBalanceLog}; +use crate::logs::emit_token_balance_log; +use crate::logs::{emit_perp_balances, emit_stack, PerpSettlePnlLog}; use crate::state::*; pub fn perp_settle_pnl(ctx: Context) -> Result<()> { @@ -191,23 +192,8 @@ pub fn perp_settle_pnl(ctx: Context) -> Result<()> { // settled back and forth repeatedly. settle_bank.withdraw_without_fee(b_token_position, settlement, now_ts)?; - emit_stack(TokenBalanceLog { - mango_group: ctx.accounts.group.key(), - mango_account: ctx.accounts.account_a.key(), - token_index: settle_token_index, - indexed_position: a_token_position.indexed_position.to_bits(), - deposit_index: settle_bank.deposit_index.to_bits(), - borrow_index: settle_bank.borrow_index.to_bits(), - }); - - emit_stack(TokenBalanceLog { - mango_group: ctx.accounts.group.key(), - mango_account: ctx.accounts.account_b.key(), - token_index: settle_token_index, - indexed_position: b_token_position.indexed_position.to_bits(), - deposit_index: settle_bank.deposit_index.to_bits(), - borrow_index: settle_bank.borrow_index.to_bits(), - }); + emit_token_balance_log(ctx.accounts.account_a.key(), &settle_bank, a_token_position); + emit_token_balance_log(ctx.accounts.account_b.key(), &settle_bank, b_token_position); // settler might be the same as account a or b drop(account_a); @@ -226,14 +212,11 @@ pub fn perp_settle_pnl(ctx: Context) -> Result<()> { settler.ensure_token_position(settle_token_index)?; let settler_token_position_active = settle_bank.deposit(settler_token_position, fee, now_ts)?; - emit_stack(TokenBalanceLog { - mango_group: ctx.accounts.group.key(), - mango_account: ctx.accounts.settler.key(), - token_index: settler_token_position.token_index, - indexed_position: settler_token_position.indexed_position.to_bits(), - deposit_index: settle_bank.deposit_index.to_bits(), - borrow_index: settle_bank.borrow_index.to_bits(), - }); + emit_token_balance_log( + ctx.accounts.settler.key(), + &settle_bank, + settler_token_position, + ); if !settler_token_position_active { settler diff --git a/programs/mango-v4/src/instructions/serum3_place_order.rs b/programs/mango-v4/src/instructions/serum3_place_order.rs index 04ae58a833..074f445375 100644 --- a/programs/mango-v4/src/instructions/serum3_place_order.rs +++ b/programs/mango-v4/src/instructions/serum3_place_order.rs @@ -2,10 +2,11 @@ use crate::accounts_zerocopy::*; use crate::error::*; use crate::health::*; use crate::i80f48::ClampToInt; +use crate::logs::emit_token_balance_log; use crate::state::*; use crate::accounts_ix::*; -use crate::logs::{emit_stack, Serum3OpenOrdersBalanceLogV2, TokenBalanceLog}; +use crate::logs::{emit_stack, Serum3OpenOrdersBalanceLogV2}; use crate::serum3_cpi::{ load_market_state, load_open_orders_ref, OpenOrdersAmounts, OpenOrdersSlim, }; @@ -538,6 +539,7 @@ fn apply_vault_difference( } else { bank.withdraw_without_fee(position, -needed_change, now_ts)?; } + emit_token_balance_log(account_pk, bank, position); let native_after = position.native(bank); let native_change = native_after - native_before; // amount of tokens transfered to serum3 reserved that were borrowed @@ -547,7 +549,6 @@ fn apply_vault_difference( .abs() .to_num::(); - let indexed_position = position.indexed_position; let market = account.serum3_orders_mut(serum_market_index).unwrap(); let borrows_without_fee; if bank.token_index == market.base_token_index { @@ -568,15 +569,6 @@ fn apply_vault_difference( *borrows_without_fee = (*borrows_without_fee).saturating_sub(needed_change.to_num::()); } - emit_stack(TokenBalanceLog { - mango_group: bank.group, - mango_account: account_pk, - token_index: bank.token_index, - indexed_position: indexed_position.to_bits(), - deposit_index: bank.deposit_index.to_bits(), - borrow_index: bank.borrow_index.to_bits(), - }); - Ok(VaultDifference { token_index: bank.token_index, native_change, diff --git a/programs/mango-v4/src/instructions/token_charge_collateral_fees.rs b/programs/mango-v4/src/instructions/token_charge_collateral_fees.rs index 944c2722d3..b6c1208e79 100644 --- a/programs/mango-v4/src/instructions/token_charge_collateral_fees.rs +++ b/programs/mango-v4/src/instructions/token_charge_collateral_fees.rs @@ -1,12 +1,13 @@ use crate::accounts_zerocopy::*; use crate::health::*; +use crate::logs::emit_token_balance_log; use crate::state::*; use crate::util::clock_now; use anchor_lang::prelude::*; use fixed::types::I80F48; use crate::accounts_ix::*; -use crate::logs::{emit_stack, TokenBalanceLog, TokenCollateralFeeLog}; +use crate::logs::{emit_stack, TokenCollateralFeeLog}; pub fn token_charge_collateral_fees(ctx: Context) -> Result<()> { let group = ctx.accounts.group.load()?; @@ -116,14 +117,7 @@ pub fn token_charge_collateral_fees(ctx: Context) -> price: token_info.prices.oracle.to_bits(), }); - emit_stack(TokenBalanceLog { - mango_group: ctx.accounts.group.key(), - mango_account: ctx.accounts.account.key(), - token_index: bank.token_index, - indexed_position: token_position.indexed_position.to_bits(), - deposit_index: bank.deposit_index.to_bits(), - borrow_index: bank.borrow_index.to_bits(), - }) + emit_token_balance_log(ctx.accounts.account.key(), &bank, token_position); } Ok(()) 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 4d3624ac02..e89338feab 100644 --- a/programs/mango-v4/src/instructions/token_conditional_swap_start.rs +++ b/programs/mango-v4/src/instructions/token_conditional_swap_start.rs @@ -5,7 +5,8 @@ use crate::accounts_ix::*; use crate::error::*; use crate::health::*; use crate::i80f48::ClampToInt; -use crate::logs::{emit_stack, TokenBalanceLog, TokenConditionalSwapStartLog}; +use crate::logs::emit_token_balance_log; +use crate::logs::{emit_stack, TokenConditionalSwapStartLog}; use crate::state::*; #[allow(clippy::too_many_arguments)] @@ -98,22 +99,8 @@ pub fn token_conditional_swap_start( health_cache .adjust_token_balance(sell_bank, liqee_sell_post_balance - liqee_sell_pre_balance)?; - emit_stack(TokenBalanceLog { - mango_group: *group_pk, - mango_account: liqee_key, - token_index: sell_token_index, - indexed_position: liqee_sell_token.indexed_position.to_bits(), - deposit_index: sell_bank.deposit_index.to_bits(), - borrow_index: sell_bank.borrow_index.to_bits(), - }); - emit_stack(TokenBalanceLog { - mango_group: *group_pk, - mango_account: liqor_key, - token_index: sell_token_index, - indexed_position: liqor_sell_token.indexed_position.to_bits(), - deposit_index: sell_bank.deposit_index.to_bits(), - borrow_index: sell_bank.borrow_index.to_bits(), - }); + emit_token_balance_log(liqee_key, sell_bank, liqee_sell_token); + emit_token_balance_log(liqor_key, sell_bank, liqor_sell_token); emit_stack(TokenConditionalSwapStartLog { mango_group: *group_pk, mango_account: liqee_key, diff --git a/programs/mango-v4/src/instructions/token_conditional_swap_trigger.rs b/programs/mango-v4/src/instructions/token_conditional_swap_trigger.rs index 1d25846b42..84ebc7b48e 100644 --- a/programs/mango-v4/src/instructions/token_conditional_swap_trigger.rs +++ b/programs/mango-v4/src/instructions/token_conditional_swap_trigger.rs @@ -5,8 +5,9 @@ use crate::accounts_ix::*; use crate::error::*; use crate::health::*; use crate::i80f48::ClampToInt; +use crate::logs::emit_token_balance_log; use crate::logs::{ - emit_stack, LoanOriginationFeeInstruction, TokenBalanceLog, TokenConditionalSwapCancelLog, + emit_stack, LoanOriginationFeeInstruction, TokenConditionalSwapCancelLog, TokenConditionalSwapTriggerLogV3, WithdrawLoanLog, }; use crate::state::*; @@ -306,8 +307,8 @@ fn action( let post_liqee_buy_token = liqee_buy_token.native(&buy_bank); let post_liqor_buy_token = liqor_buy_token.native(&buy_bank); - let liqee_buy_indexed_position = liqee_buy_token.indexed_position; - let liqor_buy_indexed_position = liqor_buy_token.indexed_position; + emit_token_balance_log(liqee_key, buy_bank, liqee_buy_token); + emit_token_balance_log(liqor_key, buy_bank, liqor_buy_token); let (liqee_sell_token, liqee_sell_raw_index) = liqee.token_position_mut(tcs.sell_token_index)?; @@ -327,8 +328,8 @@ fn action( let post_liqee_sell_token = liqee_sell_token.native(&sell_bank); let post_liqor_sell_token = liqor_sell_token.native(&sell_bank); - let liqee_sell_indexed_position = liqee_sell_token.indexed_position; - let liqor_sell_indexed_position = liqor_sell_token.indexed_position; + emit_token_balance_log(liqee_key, sell_bank, liqee_sell_token); + emit_token_balance_log(liqor_key, sell_bank, liqor_sell_token); // With a scanning account retriever, it's safe to deactivate inactive token positions immediately. // Liqee positions can only be deactivated if the tcs is closed (see below). @@ -341,43 +342,6 @@ fn action( // Log info - // liqee buy token - emit_stack(TokenBalanceLog { - mango_group: liqee.fixed.group, - mango_account: liqee_key, - token_index: tcs.buy_token_index, - indexed_position: liqee_buy_indexed_position.to_bits(), - deposit_index: buy_bank.deposit_index.to_bits(), - borrow_index: buy_bank.borrow_index.to_bits(), - }); - // liqee sell token - emit_stack(TokenBalanceLog { - mango_group: liqee.fixed.group, - mango_account: liqee_key, - token_index: tcs.sell_token_index, - indexed_position: liqee_sell_indexed_position.to_bits(), - deposit_index: sell_bank.deposit_index.to_bits(), - borrow_index: sell_bank.borrow_index.to_bits(), - }); - // liqor buy token - emit_stack(TokenBalanceLog { - mango_group: liqee.fixed.group, - mango_account: liqor_key, - token_index: tcs.buy_token_index, - indexed_position: liqor_buy_indexed_position.to_bits(), - deposit_index: buy_bank.deposit_index.to_bits(), - borrow_index: buy_bank.borrow_index.to_bits(), - }); - // liqor sell token - emit_stack(TokenBalanceLog { - mango_group: liqee.fixed.group, - mango_account: liqor_key, - token_index: tcs.sell_token_index, - indexed_position: liqor_sell_indexed_position.to_bits(), - deposit_index: sell_bank.deposit_index.to_bits(), - borrow_index: sell_bank.borrow_index.to_bits(), - }); - if buy_transfer.has_loan() { emit_stack(WithdrawLoanLog { mango_group: liqee.fixed.group, diff --git a/programs/mango-v4/src/instructions/token_deposit.rs b/programs/mango-v4/src/instructions/token_deposit.rs index 1155cd7d9b..3d7f2a4f96 100644 --- a/programs/mango-v4/src/instructions/token_deposit.rs +++ b/programs/mango-v4/src/instructions/token_deposit.rs @@ -83,6 +83,7 @@ impl<'a, 'info> DepositCommon<'a, 'info> { Clock::get()?.unix_timestamp.try_into().unwrap(), )? }; + emit_token_balance_log(self.account.key(), &bank, position); // Transfer the actual tokens token::transfer(self.transfer_ctx(), amount_i80f48.to_num::())?; @@ -107,14 +108,6 @@ impl<'a, 'info> DepositCommon<'a, 'info> { let amount_usd = (amount_i80f48 * unsafe_oracle_price).to_num::(); account.fixed.net_deposits += amount_usd; - emit_stack(TokenBalanceLog { - mango_group: self.group.key(), - mango_account: self.account.key(), - token_index, - indexed_position: indexed_position.to_bits(), - deposit_index: bank.deposit_index.to_bits(), - borrow_index: bank.borrow_index.to_bits(), - }); drop(bank); // 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 1937f4dc73..ef3c65d9d9 100644 --- a/programs/mango-v4/src/instructions/token_force_close_borrows_with_token.rs +++ b/programs/mango-v4/src/instructions/token_force_close_borrows_with_token.rs @@ -1,7 +1,8 @@ use crate::accounts_ix::*; use crate::error::*; use crate::health::*; -use crate::logs::{emit_stack, TokenBalanceLog, TokenForceCloseBorrowsWithTokenLogV2}; +use crate::logs::emit_token_balance_log; +use crate::logs::{emit_stack, TokenForceCloseBorrowsWithTokenLogV2}; use crate::state::*; use anchor_lang::prelude::*; use fixed::types::I80F48; @@ -112,18 +113,18 @@ pub fn token_force_close_borrows_with_token( // Apply the balance changes to the liqor and liqee accounts let liqee_liab_active = liab_bank.deposit_with_dusting(liqee_liab_position, liab_transfer, now_ts)?; - let liqee_liab_indexed_position = liqee_liab_position.indexed_position; + emit_token_balance_log(liqee_key, liab_bank, liqee_liab_position); let liqor_liab_withdraw_result = liab_bank.withdraw_with_fee(liqor_liab_position, liab_transfer, now_ts)?; - let liqor_liab_indexed_position = liqor_liab_position.indexed_position; + emit_token_balance_log(liqor_key, liab_bank, liqor_liab_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_to_liqor, now_ts)?; - let liqor_asset_indexed_position = liqor_asset_position.indexed_position; + emit_token_balance_log(liqor_key, asset_bank, liqor_asset_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( @@ -131,7 +132,7 @@ pub fn token_force_close_borrows_with_token( asset_transfer_from_liqee, now_ts, )?; - let liqee_asset_indexed_position = liqee_asset_position.indexed_position; + emit_token_balance_log(liqee_key, asset_bank, liqee_asset_position); let liqee_assets_native_after = liqee_asset_position.native(asset_bank); msg!( @@ -140,43 +141,6 @@ pub fn token_force_close_borrows_with_token( asset_transfer_from_liqee, ); - // liqee asset - emit_stack(TokenBalanceLog { - mango_group: liqee.fixed.group, - mango_account: liqee_key, - token_index: asset_token_index, - indexed_position: liqee_asset_indexed_position.to_bits(), - deposit_index: asset_bank.deposit_index.to_bits(), - borrow_index: asset_bank.borrow_index.to_bits(), - }); - // liqee liab - emit_stack(TokenBalanceLog { - mango_group: liqee.fixed.group, - mango_account: liqee_key, - token_index: liab_token_index, - indexed_position: liqee_liab_indexed_position.to_bits(), - deposit_index: liab_bank.deposit_index.to_bits(), - borrow_index: liab_bank.borrow_index.to_bits(), - }); - // liqor asset - emit_stack(TokenBalanceLog { - mango_group: liqee.fixed.group, - mango_account: liqor_key, - token_index: asset_token_index, - indexed_position: liqor_asset_indexed_position.to_bits(), - deposit_index: asset_bank.deposit_index.to_bits(), - borrow_index: asset_bank.borrow_index.to_bits(), - }); - // liqor liab - emit_stack(TokenBalanceLog { - mango_group: liqee.fixed.group, - mango_account: liqor_key, - token_index: liab_token_index, - indexed_position: liqor_liab_indexed_position.to_bits(), - deposit_index: liab_bank.deposit_index.to_bits(), - borrow_index: liab_bank.borrow_index.to_bits(), - }); - emit_stack(TokenForceCloseBorrowsWithTokenLogV2 { mango_group: liqee.fixed.group, liqee: liqee_key, diff --git a/programs/mango-v4/src/instructions/token_force_withdraw.rs b/programs/mango-v4/src/instructions/token_force_withdraw.rs index 1479ce9e32..8ad834a8af 100644 --- a/programs/mango-v4/src/instructions/token_force_withdraw.rs +++ b/programs/mango-v4/src/instructions/token_force_withdraw.rs @@ -1,12 +1,13 @@ use crate::accounts_zerocopy::AccountInfoRef; use crate::error::*; +use crate::logs::emit_token_balance_log; use crate::state::*; use anchor_lang::prelude::*; use anchor_spl::token; use fixed::types::I80F48; use crate::accounts_ix::*; -use crate::logs::{emit_stack, ForceWithdrawLog, TokenBalanceLog}; +use crate::logs::{emit_stack, ForceWithdrawLog}; pub fn token_force_withdraw(ctx: Context) -> Result<()> { let group = ctx.accounts.group.load()?; @@ -61,14 +62,7 @@ pub fn token_force_withdraw(ctx: Context) -> Result<()> { amount, )?; - emit_stack(TokenBalanceLog { - mango_group: ctx.accounts.group.key(), - mango_account: ctx.accounts.account.key(), - token_index, - indexed_position: position.indexed_position.to_bits(), - deposit_index: bank.deposit_index.to_bits(), - borrow_index: bank.borrow_index.to_bits(), - }); + emit_token_balance_log(ctx.accounts.account.key(), &bank, position); // Get the oracle price, even if stale or unconfident: We want to allow force withdraws // even if the oracle is bad. diff --git a/programs/mango-v4/src/instructions/token_liq_bankruptcy.rs b/programs/mango-v4/src/instructions/token_liq_bankruptcy.rs index 8cc5e86388..46f34213d9 100644 --- a/programs/mango-v4/src/instructions/token_liq_bankruptcy.rs +++ b/programs/mango-v4/src/instructions/token_liq_bankruptcy.rs @@ -5,12 +5,12 @@ use fixed::types::I80F48; use crate::accounts_zerocopy::*; use crate::error::*; use crate::health::*; +use crate::logs::emit_token_balance_log; use crate::state::*; use crate::accounts_ix::*; use crate::logs::{ - emit_stack, LoanOriginationFeeInstruction, TokenBalanceLog, TokenLiqBankruptcyLog, - WithdrawLoanLog, + emit_stack, LoanOriginationFeeInstruction, TokenLiqBankruptcyLog, WithdrawLoanLog, }; pub fn token_liq_bankruptcy( @@ -128,24 +128,13 @@ pub fn token_liq_bankruptcy( require_keys_eq!(quote_bank.vault, ctx.accounts.quote_vault.key()); require_keys_eq!(quote_bank.mint, ctx.accounts.insurance_vault.mint); - let quote_deposit_index = quote_bank.deposit_index; - let quote_borrow_index = quote_bank.borrow_index; - // credit the liqor let (liqor_quote, liqor_quote_raw_token_index, _) = liqor.ensure_token_position(INSURANCE_TOKEN_INDEX)?; let liqor_quote_active = quote_bank.deposit(liqor_quote, insurance_transfer_i80f48, now_ts)?; - // liqor quote - emit_stack(TokenBalanceLog { - mango_group: ctx.accounts.group.key(), - mango_account: ctx.accounts.liqor.key(), - token_index: INSURANCE_TOKEN_INDEX, - indexed_position: liqor_quote.indexed_position.to_bits(), - deposit_index: quote_deposit_index.to_bits(), - borrow_index: quote_borrow_index.to_bits(), - }); + emit_token_balance_log(ctx.accounts.liqor.key(), quote_bank, liqor_quote); // transfer liab from liqee to liqor let (liqor_liab, liqor_liab_raw_token_index, _) = @@ -153,15 +142,7 @@ pub fn token_liq_bankruptcy( let liqor_liab_withdraw_result = liab_bank.withdraw_with_fee(liqor_liab, liab_transfer, now_ts)?; - // liqor liab - emit_stack(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(), - }); + emit_token_balance_log(ctx.accounts.liqor.key(), liab_bank, liqor_liab); // Check liqor's health if !liqor.fixed.is_in_health_region() { @@ -256,17 +237,9 @@ pub fn token_liq_bankruptcy( require_eq!(liqee_liab.indexed_position, I80F48::ZERO); } - // liqee liab - emit_stack(TokenBalanceLog { - mango_group: ctx.accounts.group.key(), - mango_account: ctx.accounts.liqee.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(), - }); - let liab_bank = bank_ais[0].load::()?; + emit_token_balance_log(ctx.accounts.liqee.key(), &liab_bank, liqee_liab); + let end_liab_native = liqee_liab.native(&liab_bank); liqee_health_cache.adjust_token_balance(&liab_bank, end_liab_native - initial_liab_native)?; 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 c064e2216f..ed7fb63cd1 100644 --- a/programs/mango-v4/src/instructions/token_liq_with_token.rs +++ b/programs/mango-v4/src/instructions/token_liq_with_token.rs @@ -4,9 +4,9 @@ use fixed::types::I80F48; use crate::accounts_ix::*; use crate::error::*; use crate::health::*; +use crate::logs::emit_token_balance_log; use crate::logs::{ - emit_stack, LoanOriginationFeeInstruction, TokenBalanceLog, TokenLiqWithTokenLogV2, - WithdrawLoanLog, + emit_stack, LoanOriginationFeeInstruction, TokenLiqWithTokenLogV2, WithdrawLoanLog, }; use crate::state::*; @@ -241,20 +241,20 @@ pub(crate) fn liquidation_action( 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, now_ts)?; - let liqee_liab_indexed_position = liqee_liab_position.indexed_position; + emit_token_balance_log(liqee_key, liab_bank, liqee_liab_position); let (liqor_liab_position, liqor_liab_raw_index, _) = liqor.ensure_token_position(liab_token_index)?; let liqor_liab_withdraw_result = liab_bank.withdraw_with_fee(liqor_liab_position, liab_transfer, now_ts)?; - let liqor_liab_indexed_position = liqor_liab_position.indexed_position; + emit_token_balance_log(liqor_key, liab_bank, liqor_liab_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_to_liqor, now_ts)?; - let liqor_asset_indexed_position = liqor_asset_position.indexed_position; + emit_token_balance_log(liqor_key, asset_bank, liqor_asset_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( @@ -262,7 +262,7 @@ pub(crate) fn liquidation_action( asset_transfer_from_liqee, now_ts, )?; - let liqee_asset_indexed_position = liqee_asset_position.indexed_position; + emit_token_balance_log(liqee_key, asset_bank, liqee_asset_position); let liqee_assets_native_after = liqee_asset_position.native(asset_bank); // Update the health cache @@ -277,43 +277,6 @@ pub(crate) fn liquidation_action( asset_transfer_from_liqee, ); - // liqee asset - emit_stack(TokenBalanceLog { - mango_group: liqee.fixed.group, - mango_account: liqee_key, - token_index: asset_token_index, - indexed_position: liqee_asset_indexed_position.to_bits(), - deposit_index: asset_bank.deposit_index.to_bits(), - borrow_index: asset_bank.borrow_index.to_bits(), - }); - // liqee liab - emit_stack(TokenBalanceLog { - mango_group: liqee.fixed.group, - mango_account: liqee_key, - token_index: liab_token_index, - indexed_position: liqee_liab_indexed_position.to_bits(), - deposit_index: liab_bank.deposit_index.to_bits(), - borrow_index: liab_bank.borrow_index.to_bits(), - }); - // liqor asset - emit_stack(TokenBalanceLog { - mango_group: liqee.fixed.group, - mango_account: liqor_key, - token_index: asset_token_index, - indexed_position: liqor_asset_indexed_position.to_bits(), - deposit_index: asset_bank.deposit_index.to_bits(), - borrow_index: asset_bank.borrow_index.to_bits(), - }); - // liqor liab - emit_stack(TokenBalanceLog { - mango_group: liqee.fixed.group, - mango_account: liqor_key, - token_index: liab_token_index, - indexed_position: liqor_liab_indexed_position.to_bits(), - deposit_index: liab_bank.deposit_index.to_bits(), - borrow_index: liab_bank.borrow_index.to_bits(), - }); - if liqor_liab_withdraw_result .loan_origination_fee .is_positive() diff --git a/programs/mango-v4/src/instructions/token_withdraw.rs b/programs/mango-v4/src/instructions/token_withdraw.rs index a03f34b1f7..e683df1143 100644 --- a/programs/mango-v4/src/instructions/token_withdraw.rs +++ b/programs/mango-v4/src/instructions/token_withdraw.rs @@ -1,6 +1,7 @@ use crate::accounts_zerocopy::*; use crate::error::*; use crate::health::*; +use crate::logs::emit_token_balance_log; use crate::state::*; use crate::util::clock_now; use anchor_lang::prelude::*; @@ -9,9 +10,7 @@ use anchor_spl::token; use fixed::types::I80F48; use crate::accounts_ix::*; -use crate::logs::{ - emit_stack, LoanOriginationFeeInstruction, TokenBalanceLog, WithdrawLoanLog, WithdrawLog, -}; +use crate::logs::{emit_stack, LoanOriginationFeeInstruction, WithdrawLoanLog, WithdrawLog}; const DELEGATE_WITHDRAW_MAX: i64 = 100_000; // $0.1 @@ -84,6 +83,7 @@ pub fn token_withdraw(ctx: Context, amount: u64, allow_borrow: bo amount_i80f48, Clock::get()?.unix_timestamp.try_into().unwrap(), )?; + emit_token_balance_log(ctx.accounts.account.key(), &bank, position); let native_position_after = position.native(&bank); // Avoid getting in trouble because of the mutable bank account borrow later @@ -108,15 +108,6 @@ pub fn token_withdraw(ctx: Context, amount: u64, allow_borrow: bo amount, )?; - emit_stack(TokenBalanceLog { - mango_group: ctx.accounts.group.key(), - mango_account: ctx.accounts.account.key(), - token_index, - indexed_position: position.indexed_position.to_bits(), - deposit_index: bank.deposit_index.to_bits(), - borrow_index: bank.borrow_index.to_bits(), - }); - // Update the net deposits - adjust by price so different tokens are on the same basis (in USD terms) let amount_usd = (amount_i80f48 * unsafe_oracle_state.price).to_num::(); account.fixed.net_deposits -= amount_usd; diff --git a/programs/mango-v4/src/logs.rs b/programs/mango-v4/src/logs.rs index 9032b3bc68..b832f97500 100644 --- a/programs/mango-v4/src/logs.rs +++ b/programs/mango-v4/src/logs.rs @@ -1,6 +1,6 @@ use crate::{ accounts_ix::FlashLoanType, - state::{OracleType, PerpMarket, PerpPosition}, + state::{Bank, OracleType, PerpMarket, PerpPosition, TokenPosition}, }; use anchor_lang::prelude::*; use borsh::BorshSerialize; @@ -53,6 +53,18 @@ pub struct PerpBalanceLog { pub short_funding: i128, // I80F48 } +pub fn emit_token_balance_log(mango_account: Pubkey, bank: &Bank, token_position: &TokenPosition) { + assert_eq!(bank.token_index, token_position.token_index); + emit_stack(TokenBalanceLog { + mango_group: bank.group, + mango_account, + token_index: bank.token_index, + indexed_position: token_position.indexed_position.to_bits(), // TODO ### oops, what if indexed_position is no-lending? + deposit_index: bank.deposit_index.to_bits(), + borrow_index: bank.borrow_index.to_bits(), + }); +} + #[event] pub struct TokenBalanceLog { pub mango_group: Pubkey, From 4fedfbe9bf1a7377224d2ff1214663993546bb8a Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Thu, 7 Mar 2024 14:22:15 +0100 Subject: [PATCH 05/23] some limits may be broken now --- programs/mango-v4/src/state/bank.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/programs/mango-v4/src/state/bank.rs b/programs/mango-v4/src/state/bank.rs index bb45ba2c85..8c3182bc3c 100644 --- a/programs/mango-v4/src/state/bank.rs +++ b/programs/mango-v4/src/state/bank.rs @@ -474,6 +474,7 @@ impl Bank { self.borrow_index * self.indexed_borrows } + // TODO: this now only tracks lendable deposits - need to audit all uses #[inline(always)] pub fn native_deposits(&self) -> I80F48 { self.deposit_index * self.indexed_deposits From 5c429d79132a1c8cfd7e6128f469b9452eeaa39e Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Thu, 7 Mar 2024 14:39:15 +0100 Subject: [PATCH 06/23] token balance log v2 --- programs/mango-v4/src/logs.rs | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/programs/mango-v4/src/logs.rs b/programs/mango-v4/src/logs.rs index b832f97500..5a40789bfe 100644 --- a/programs/mango-v4/src/logs.rs +++ b/programs/mango-v4/src/logs.rs @@ -55,13 +55,24 @@ pub struct PerpBalanceLog { pub fn emit_token_balance_log(mango_account: Pubkey, bank: &Bank, token_position: &TokenPosition) { assert_eq!(bank.token_index, token_position.token_index); - emit_stack(TokenBalanceLog { + let allow_lending = token_position.allow_lending(); + emit_stack(TokenBalanceLogV2 { mango_group: bank.group, mango_account, token_index: bank.token_index, - indexed_position: token_position.indexed_position.to_bits(), // TODO ### oops, what if indexed_position is no-lending? + indexed_position: if allow_lending { + token_position.indexed_position.to_bits() + } else { + 0 + }, deposit_index: bank.deposit_index.to_bits(), borrow_index: bank.borrow_index.to_bits(), + native_position: if !allow_lending { + token_position.indexed_position.to_num::() + } else { + 0 + }, + allow_lending: token_position.allow_lending(), }); } @@ -75,6 +86,18 @@ pub struct TokenBalanceLog { pub borrow_index: i128, // I80F48 } +#[event] +pub struct TokenBalanceLogV2 { + pub mango_group: Pubkey, + pub mango_account: Pubkey, + pub token_index: u16, // IDL doesn't support usize + pub indexed_position: i128, // on client convert i128 to I80F48 easily by passing in the BN to I80F48 ctor + pub deposit_index: i128, // I80F48 + pub borrow_index: i128, // I80F48 + pub native_position: u64, + pub allow_lending: bool, +} + #[derive(AnchorSerialize, AnchorDeserialize)] pub struct FlashLoanTokenDetail { pub token_index: u16, From 358f3cfc560be7b9085fae40d33c5f2831ead566 Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Tue, 12 Mar 2024 12:09:10 +0100 Subject: [PATCH 07/23] don't overload indexed_position field --- programs/mango-v4/src/logs.rs | 13 ++-------- programs/mango-v4/src/state/bank.rs | 25 +++++++++++++------ programs/mango-v4/src/state/mango_account.rs | 5 ++-- .../src/state/mango_account_components.rs | 11 +++++--- 4 files changed, 30 insertions(+), 24 deletions(-) diff --git a/programs/mango-v4/src/logs.rs b/programs/mango-v4/src/logs.rs index 5a40789bfe..fd6bde7785 100644 --- a/programs/mango-v4/src/logs.rs +++ b/programs/mango-v4/src/logs.rs @@ -55,23 +55,14 @@ pub struct PerpBalanceLog { pub fn emit_token_balance_log(mango_account: Pubkey, bank: &Bank, token_position: &TokenPosition) { assert_eq!(bank.token_index, token_position.token_index); - let allow_lending = token_position.allow_lending(); emit_stack(TokenBalanceLogV2 { mango_group: bank.group, mango_account, token_index: bank.token_index, - indexed_position: if allow_lending { - token_position.indexed_position.to_bits() - } else { - 0 - }, + indexed_position: token_position.indexed_position.to_bits(), deposit_index: bank.deposit_index.to_bits(), borrow_index: bank.borrow_index.to_bits(), - native_position: if !allow_lending { - token_position.indexed_position.to_num::() - } else { - 0 - }, + native_position: token_position.unlendable_deposit, allow_lending: token_position.allow_lending(), }); } diff --git a/programs/mango-v4/src/state/bank.rs b/programs/mango-v4/src/state/bank.rs index 8c3182bc3c..888dd8627a 100644 --- a/programs/mango-v4/src/state/bank.rs +++ b/programs/mango-v4/src/state/bank.rs @@ -568,15 +568,20 @@ impl Bank { now_ts: u64, ) -> Result { if position.allow_lending() { + assert!(position.unlendable_deposit == 0); let opening_indexed_position = position.indexed_position; let result = self.deposit_internal(position, native_amount, allow_dusting, now_ts)?; self.update_cumulative_interest(position, opening_indexed_position); Ok(result) } else { + assert!(position.indexed_position.is_zero()); let deposit_amount = native_amount.floor(); - self.unlendable_deposits += deposit_amount.to_num::(); self.dust += native_amount - deposit_amount; - position.indexed_position += deposit_amount; + + let deposit_amount_u64 = deposit_amount.to_num::(); + position.unlendable_deposit += deposit_amount_u64; + self.unlendable_deposits += deposit_amount_u64; + Ok(true) } } @@ -719,6 +724,7 @@ impl Bank { now_ts: u64, ) -> Result { if position.allow_lending() { + assert!(position.unlendable_deposit == 0); let opening_indexed_position = position.indexed_position; let res = self.withdraw_internal( position, @@ -730,12 +736,15 @@ impl Bank { self.update_cumulative_interest(position, opening_indexed_position); res } else { + assert!(position.indexed_position.is_zero()); + // TODO: might there be trouble by rounding up here? let withdraw_amount = native_amount.ceil(); self.dust += withdraw_amount - native_amount; - self.unlendable_deposits -= withdraw_amount.to_num::(); - position.indexed_position -= withdraw_amount; - require_gte!(position.indexed_position, I80F48::ZERO); // TODO: error + + let withdraw_amount_u64 = withdraw_amount.to_num::(); + self.unlendable_deposits -= withdraw_amount_u64; + position.unlendable_deposit -= withdraw_amount_u64; Ok(WithdrawResult { position_is_active: true, loan_amount: I80F48::ZERO, @@ -1387,8 +1396,9 @@ mod tests { cumulative_deposit_interest: 0.0, cumulative_borrow_interest: 0.0, previous_index: I80F48::ZERO, + unlendable_deposit: 0, padding: Default::default(), - reserved: [0; 128], + reserved: [0; 120], }; account.indexed_position = indexed(I80F48::from_num(start), &bank); @@ -1515,8 +1525,9 @@ mod tests { cumulative_deposit_interest: 0.0, cumulative_borrow_interest: 0.0, previous_index: I80F48::ZERO, + unlendable_deposit: 0, padding: Default::default(), - reserved: [0; 128], + reserved: [0; 120], }; // diff --git a/programs/mango-v4/src/state/mango_account.rs b/programs/mango-v4/src/state/mango_account.rs index c9bbf13108..6c954d1910 100644 --- a/programs/mango-v4/src/state/mango_account.rs +++ b/programs/mango-v4/src/state/mango_account.rs @@ -1016,8 +1016,9 @@ impl< cumulative_deposit_interest: 0.0, cumulative_borrow_interest: 0.0, previous_index: I80F48::ZERO, + unlendable_deposit: 0, padding: Default::default(), - reserved: [0; 128], + reserved: [0; 120], }; } Ok((v, raw_index, bank_index)) @@ -1162,7 +1163,7 @@ impl< perp_position.market_index = perp_market_index; let settle_token_position = self.ensure_token_position(settle_token_index)?.0; - // no-lending positions can't have negative balances can't work with perps: + // no-lending positions can't have negative balances and can't work with perps: // settlement must be able to move the position arbitrarily require_msg_typed!( settle_token_position.allow_lending(), diff --git a/programs/mango-v4/src/state/mango_account_components.rs b/programs/mango-v4/src/state/mango_account_components.rs index 1e7becb6f6..1688073e07 100644 --- a/programs/mango-v4/src/state/mango_account_components.rs +++ b/programs/mango-v4/src/state/mango_account_components.rs @@ -54,13 +54,15 @@ pub struct TokenPosition { // Cumulative borrow interest in token native units pub cumulative_borrow_interest: f64, + pub unlendable_deposit: u64, + #[derivative(Debug = "ignore")] - pub reserved: [u8; 128], + pub reserved: [u8; 120], } const_assert_eq!( size_of::(), - 16 + 2 + 2 + 4 + 16 + 8 + 8 + 128 + 16 + 2 + 2 + 4 + 16 + 8 + 8 + 8 + 120 ); const_assert_eq!(size_of::(), 184); const_assert_eq!(size_of::() % 8, 0); @@ -75,8 +77,9 @@ impl Default for TokenPosition { cumulative_deposit_interest: 0.0, cumulative_borrow_interest: 0.0, previous_index: I80F48::ZERO, + unlendable_deposit: 0, padding: Default::default(), - reserved: [0; 128], + reserved: [0; 120], } } } @@ -98,7 +101,7 @@ impl TokenPosition { self.indexed_position * bank.borrow_index } } else { - self.indexed_position + I80F48::from(self.unlendable_deposit) } } From be25e07e8d89d5614dbba9382fa2367b359e8b42 Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Tue, 12 Mar 2024 13:01:40 +0100 Subject: [PATCH 08/23] bank: distinguish lendable and unlendable deposits --- .../src/instructions/token_liq_bankruptcy.rs | 2 +- programs/mango-v4/src/logs.rs | 2 +- programs/mango-v4/src/state/bank.rs | 42 +++++++++++-------- programs/mango-v4/src/state/mango_account.rs | 2 +- .../src/state/mango_account_components.rs | 6 +-- .../tests/cases/test_bankrupt_tokens.rs | 4 +- programs/mango-v4/tests/cases/test_basic.rs | 6 +-- .../cases/test_token_update_index_and_rate.rs | 4 +- 8 files changed, 38 insertions(+), 30 deletions(-) diff --git a/programs/mango-v4/src/instructions/token_liq_bankruptcy.rs b/programs/mango-v4/src/instructions/token_liq_bankruptcy.rs index 46f34213d9..a6a0d395eb 100644 --- a/programs/mango-v4/src/instructions/token_liq_bankruptcy.rs +++ b/programs/mango-v4/src/instructions/token_liq_bankruptcy.rs @@ -220,7 +220,7 @@ pub fn token_liq_bankruptcy( bank.deposit_index = new_deposit_index; // credit liqee on each bank where we can offset borrows - let amount_for_bank = amount_to_credit.min(bank.native_borrows()); + let amount_for_bank = amount_to_credit.min(bank.borrows()); if amount_for_bank.is_positive() { // enable dusting, because each deposit() is allowed to round up. thus multiple deposit // could bring the total position slightly above zero otherwise diff --git a/programs/mango-v4/src/logs.rs b/programs/mango-v4/src/logs.rs index fd6bde7785..1d94c1eabc 100644 --- a/programs/mango-v4/src/logs.rs +++ b/programs/mango-v4/src/logs.rs @@ -62,7 +62,7 @@ pub fn emit_token_balance_log(mango_account: Pubkey, bank: &Bank, token_position indexed_position: token_position.indexed_position.to_bits(), deposit_index: bank.deposit_index.to_bits(), borrow_index: bank.borrow_index.to_bits(), - native_position: token_position.unlendable_deposit, + native_position: token_position.unlendable_deposits, allow_lending: token_position.allow_lending(), }); } diff --git a/programs/mango-v4/src/state/bank.rs b/programs/mango-v4/src/state/bank.rs index 888dd8627a..0122b6aca2 100644 --- a/programs/mango-v4/src/state/bank.rs +++ b/programs/mango-v4/src/state/bank.rs @@ -470,16 +470,25 @@ impl Bank { } #[inline(always)] - pub fn native_borrows(&self) -> I80F48 { + pub fn borrows(&self) -> I80F48 { self.borrow_index * self.indexed_borrows } - // TODO: this now only tracks lendable deposits - need to audit all uses #[inline(always)] - pub fn native_deposits(&self) -> I80F48 { + pub fn lendable_deposits(&self) -> I80F48 { self.deposit_index * self.indexed_deposits } + #[inline(always)] + pub fn unlendable_deposits(&self) -> u64 { + self.unlendable_deposits + } + + #[inline(always)] + pub fn deposits(&self) -> I80F48 { + self.lendable_deposits() + I80F48::from(self.unlendable_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 { @@ -515,14 +524,14 @@ impl Bank { /// Prevent borrowing away the full bank vault. /// Keep some in reserve to satisfy non-borrow withdraws. fn enforce_max_utilization(&self, max_utilization: I80F48) -> Result<()> { - let bank_native_deposits = self.native_deposits(); - let bank_native_borrows = self.native_borrows(); + let bank_lendable_deposits = self.lendable_deposits(); + let bank_borrows = self.borrows(); - if bank_native_borrows > max_utilization * bank_native_deposits { + if bank_borrows > max_utilization * bank_lendable_deposits { return err!(MangoError::BankBorrowLimitReached).with_context(|| { format!( "deposits {}, borrows {}, max utilization {}", - bank_native_deposits, bank_native_borrows, max_utilization, + bank_lendable_deposits, bank_borrows, max_utilization, ) }); }; @@ -568,7 +577,7 @@ impl Bank { now_ts: u64, ) -> Result { if position.allow_lending() { - assert!(position.unlendable_deposit == 0); + assert!(position.unlendable_deposits == 0); let opening_indexed_position = position.indexed_position; let result = self.deposit_internal(position, native_amount, allow_dusting, now_ts)?; self.update_cumulative_interest(position, opening_indexed_position); @@ -579,7 +588,7 @@ impl Bank { self.dust += native_amount - deposit_amount; let deposit_amount_u64 = deposit_amount.to_num::(); - position.unlendable_deposit += deposit_amount_u64; + position.unlendable_deposits += deposit_amount_u64; self.unlendable_deposits += deposit_amount_u64; Ok(true) @@ -724,7 +733,7 @@ impl Bank { now_ts: u64, ) -> Result { if position.allow_lending() { - assert!(position.unlendable_deposit == 0); + assert!(position.unlendable_deposits == 0); let opening_indexed_position = position.indexed_position; let res = self.withdraw_internal( position, @@ -744,7 +753,7 @@ impl Bank { let withdraw_amount_u64 = withdraw_amount.to_num::(); self.unlendable_deposits -= withdraw_amount_u64; - position.unlendable_deposit -= withdraw_amount_u64; + position.unlendable_deposits -= withdraw_amount_u64; Ok(WithdrawResult { position_is_active: true, loan_amount: I80F48::ZERO, @@ -1022,7 +1031,7 @@ impl Bank { // Intentionally does not use remaining_deposits_until_limit(): That function // returns slightly less than the true limit to make sure depositing that amount // will not cause a limit overrun. - let deposits = self.native_deposits(); + let deposits = self.deposits(); let serum = I80F48::from(self.potential_serum_tokens); let total = deposits + serum; let remaining = I80F48::from(self.deposit_limit) - total; @@ -1294,8 +1303,7 @@ impl Bank { if self.deposit_weight_scale_start_quote == f64::MAX { return self.init_asset_weight; } - let all_deposits = - self.native_deposits().to_num::() + self.potential_serum_tokens as f64; + let all_deposits = self.deposits().to_num::() + self.potential_serum_tokens as f64; let deposits_quote = all_deposits * price.to_num::(); if deposits_quote <= self.deposit_weight_scale_start_quote { self.init_asset_weight @@ -1311,7 +1319,7 @@ impl Bank { if self.borrow_weight_scale_start_quote == f64::MAX { return self.init_liab_weight; } - let borrows_quote = self.native_borrows().to_num::() * price.to_num::(); + let borrows_quote = self.borrows().to_num::() * price.to_num::(); if borrows_quote <= self.borrow_weight_scale_start_quote { self.init_liab_weight } else if self.borrow_weight_scale_start_quote == 0.0 { @@ -1396,7 +1404,7 @@ mod tests { cumulative_deposit_interest: 0.0, cumulative_borrow_interest: 0.0, previous_index: I80F48::ZERO, - unlendable_deposit: 0, + unlendable_deposits: 0, padding: Default::default(), reserved: [0; 120], }; @@ -1525,7 +1533,7 @@ mod tests { cumulative_deposit_interest: 0.0, cumulative_borrow_interest: 0.0, previous_index: I80F48::ZERO, - unlendable_deposit: 0, + unlendable_deposits: 0, padding: Default::default(), reserved: [0; 120], }; diff --git a/programs/mango-v4/src/state/mango_account.rs b/programs/mango-v4/src/state/mango_account.rs index 6c954d1910..812105d626 100644 --- a/programs/mango-v4/src/state/mango_account.rs +++ b/programs/mango-v4/src/state/mango_account.rs @@ -1016,7 +1016,7 @@ impl< cumulative_deposit_interest: 0.0, cumulative_borrow_interest: 0.0, previous_index: I80F48::ZERO, - unlendable_deposit: 0, + unlendable_deposits: 0, padding: Default::default(), reserved: [0; 120], }; diff --git a/programs/mango-v4/src/state/mango_account_components.rs b/programs/mango-v4/src/state/mango_account_components.rs index 1688073e07..3b5ed6e469 100644 --- a/programs/mango-v4/src/state/mango_account_components.rs +++ b/programs/mango-v4/src/state/mango_account_components.rs @@ -54,7 +54,7 @@ pub struct TokenPosition { // Cumulative borrow interest in token native units pub cumulative_borrow_interest: f64, - pub unlendable_deposit: u64, + pub unlendable_deposits: u64, #[derivative(Debug = "ignore")] pub reserved: [u8; 120], @@ -77,7 +77,7 @@ impl Default for TokenPosition { cumulative_deposit_interest: 0.0, cumulative_borrow_interest: 0.0, previous_index: I80F48::ZERO, - unlendable_deposit: 0, + unlendable_deposits: 0, padding: Default::default(), reserved: [0; 120], } @@ -101,7 +101,7 @@ impl TokenPosition { self.indexed_position * bank.borrow_index } } else { - I80F48::from(self.unlendable_deposit) + I80F48::from(self.unlendable_deposits) } } diff --git a/programs/mango-v4/tests/cases/test_bankrupt_tokens.rs b/programs/mango-v4/tests/cases/test_bankrupt_tokens.rs index c9ba251bbb..89347c4402 100644 --- a/programs/mango-v4/tests/cases/test_bankrupt_tokens.rs +++ b/programs/mango-v4/tests/cases/test_bankrupt_tokens.rs @@ -240,8 +240,8 @@ async fn test_bankrupt_tokens_socialize_loss() -> Result<(), TransportError> { // both bank's borrows were completely wiped: no one else borrowed let borrow1_bank0: Bank = solana.get_account(borrow_token1.bank).await; let borrow1_bank1: Bank = solana.get_account(borrow_token1.bank).await; - assert_eq!(borrow1_bank0.native_borrows(), 0); - assert_eq!(borrow1_bank1.native_borrows(), 0); + assert_eq!(borrow1_bank0.borrows(), 0); + assert_eq!(borrow1_bank1.borrows(), 0); send_tx( solana, diff --git a/programs/mango-v4/tests/cases/test_basic.rs b/programs/mango-v4/tests/cases/test_basic.rs index ee774c080b..fdf818f6f2 100644 --- a/programs/mango-v4/tests/cases/test_basic.rs +++ b/programs/mango-v4/tests/cases/test_basic.rs @@ -157,7 +157,7 @@ async fn test_basic() -> Result<(), TransportError> { deposit_amount as i64 ); let bank_data: Bank = solana.get_account(bank).await; - assert!(bank_data.native_deposits() - I80F48::from_num(deposit_amount) < dust_threshold); + assert!(bank_data.deposits() - I80F48::from_num(deposit_amount) < dust_threshold); let account_data: MangoAccount = solana.get_account(account).await; // Assumes oracle price of 1 @@ -204,7 +204,7 @@ async fn test_basic() -> Result<(), TransportError> { ); let bank_data: Bank = solana.get_account(bank).await; assert!( - bank_data.native_deposits() - I80F48::from_num(start_amount - withdraw_amount) + bank_data.deposits() - I80F48::from_num(start_amount - withdraw_amount) < dust_threshold ); @@ -233,7 +233,7 @@ async fn test_basic() -> Result<(), TransportError> { send_tx( solana, TokenWithdrawInstruction { - amount: bank_data.native_deposits().to_num(), + amount: bank_data.deposits().to_num(), allow_borrow: false, account, owner, diff --git a/programs/mango-v4/tests/cases/test_token_update_index_and_rate.rs b/programs/mango-v4/tests/cases/test_token_update_index_and_rate.rs index 242125c429..ac3a190f95 100644 --- a/programs/mango-v4/tests/cases/test_token_update_index_and_rate.rs +++ b/programs/mango-v4/tests/cases/test_token_update_index_and_rate.rs @@ -79,12 +79,12 @@ async fn test_token_update_index_and_rate() -> Result<(), TransportError> { let fee_change = 5000.0 * loan_fee_rate * diff_ts / year; assert_eq_fixed_f64!( - bank_after.native_borrows() - bank_before.native_borrows(), + bank_after.borrows() - bank_before.borrows(), interest_change, 0.1 ); assert_eq_fixed_f64!( - bank_after.native_deposits() - bank_before.native_deposits(), + bank_after.deposits() - bank_before.deposits(), interest_change, 0.1 ); From 659f093b34ba2eef64c47c3c9973fd90d91b8a0e Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Tue, 12 Mar 2024 13:55:18 +0100 Subject: [PATCH 09/23] simple tests --- programs/mango-v4/src/error.rs | 2 + programs/mango-v4/src/lib.rs | 15 ++ programs/mango-v4/src/state/bank.rs | 7 +- programs/mango-v4/tests/cases/mod.rs | 1 + .../mango-v4/tests/cases/test_unlendable.rs | 192 ++++++++++++++++++ .../tests/program_test/mango_client.rs | 80 ++++++++ 6 files changed, 296 insertions(+), 1 deletion(-) create mode 100644 programs/mango-v4/tests/cases/test_unlendable.rs diff --git a/programs/mango-v4/src/error.rs b/programs/mango-v4/src/error.rs index 2ea3e91f46..b174a30adb 100644 --- a/programs/mango-v4/src/error.rs +++ b/programs/mango-v4/src/error.rs @@ -155,6 +155,8 @@ pub enum MangoError { TokenPositionBalanceNotZero, #[msg("cannot have a lending and no-lending token position at the same time")] TokenPositionWithDifferentSettingAlreadyExists, + #[msg("cannot borrow from unlendable token position")] + UnlendableTokenPositionCannotBeNegative, } impl MangoError { diff --git a/programs/mango-v4/src/lib.rs b/programs/mango-v4/src/lib.rs index b1f4b91024..b6920ca75f 100644 --- a/programs/mango-v4/src/lib.rs +++ b/programs/mango-v4/src/lib.rs @@ -492,6 +492,21 @@ pub mod mango_v4 { Ok(()) } + pub fn token_create_position( + ctx: Context, + allow_lending: bool, + ) -> Result<()> { + #[cfg(feature = "enable-gpl")] + instructions::token_create_position(ctx, allow_lending)?; + Ok(()) + } + + pub fn token_close_position(ctx: Context) -> Result<()> { + #[cfg(feature = "enable-gpl")] + instructions::token_close_position(ctx)?; + Ok(()) + } + pub fn token_deposit(ctx: Context, amount: u64, reduce_only: bool) -> Result<()> { #[cfg(feature = "enable-gpl")] instructions::token_deposit(ctx, amount, reduce_only)?; diff --git a/programs/mango-v4/src/state/bank.rs b/programs/mango-v4/src/state/bank.rs index 0122b6aca2..bf4997eec6 100644 --- a/programs/mango-v4/src/state/bank.rs +++ b/programs/mango-v4/src/state/bank.rs @@ -752,8 +752,13 @@ impl Bank { self.dust += withdraw_amount - native_amount; let withdraw_amount_u64 = withdraw_amount.to_num::(); - self.unlendable_deposits -= withdraw_amount_u64; + require_gte!( + position.unlendable_deposits, + withdraw_amount_u64, + MangoError::UnlendableTokenPositionCannotBeNegative + ); position.unlendable_deposits -= withdraw_amount_u64; + self.unlendable_deposits -= withdraw_amount_u64; Ok(WithdrawResult { position_is_active: true, loan_amount: I80F48::ZERO, diff --git a/programs/mango-v4/tests/cases/mod.rs b/programs/mango-v4/tests/cases/mod.rs index 79aba39d86..e6460dfb21 100644 --- a/programs/mango-v4/tests/cases/mod.rs +++ b/programs/mango-v4/tests/cases/mod.rs @@ -40,3 +40,4 @@ mod test_serum; mod test_stale_oracles; mod test_token_conditional_swap; mod test_token_update_index_and_rate; +mod test_unlendable; diff --git a/programs/mango-v4/tests/cases/test_unlendable.rs b/programs/mango-v4/tests/cases/test_unlendable.rs new file mode 100644 index 0000000000..148a3e6413 --- /dev/null +++ b/programs/mango-v4/tests/cases/test_unlendable.rs @@ -0,0 +1,192 @@ +use super::*; + +#[tokio::test] +async fn test_unlendable() -> Result<(), TransportError> { + let context = TestContext::new().await; + let solana = &context.solana.clone(); + + let admin = TestKeypair::new(); + let owner = context.users[0].key; + let payer = context.users[1].key; + let mints = &context.mints[0..2]; + let payer_mint0_account = context.users[1].token_accounts[0]; + + // + // SETUP: Create a group, account, register a token (mint0) + // + + let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig { + admin, + payer, + mints: mints.to_vec(), + ..mango_setup::GroupWithTokensConfig::default() + } + .create(solana) + .await; + let bank = tokens[0].bank; + + let account = send_tx( + solana, + AccountCreateInstruction { + account_num: 0, + token_count: 6, + serum3_count: 3, + perp_count: 3, + perp_oo_count: 3, + token_conditional_swap_count: 3, + group, + owner, + payer, + }, + ) + .await + .unwrap() + .account; + + // + // TEST: opening and closing + // + + send_tx( + solana, + TokenCreatePositionInstruction { + account, + owner, + bank, + allow_lending: true, + }, + ) + .await + .unwrap(); + + send_tx_expect_error!( + solana, + TokenCreatePositionInstruction { + account, + owner, + bank, + allow_lending: false, + }, + MangoError::TokenPositionWithDifferentSettingAlreadyExists, + ); + + send_tx( + solana, + TokenClosePositionInstruction { + account, + owner, + bank, + }, + ) + .await + .unwrap(); + + send_tx_expect_error!( + solana, + TokenClosePositionInstruction { + account, + owner, + bank, + }, + MangoError::TokenPositionDoesNotExist, + ); + + send_tx( + solana, + TokenCreatePositionInstruction { + account, + owner, + bank, + allow_lending: false, + }, + ) + .await + .unwrap(); + + send_tx_expect_error!( + solana, + TokenCreatePositionInstruction { + account, + owner, + bank, + allow_lending: true, + }, + MangoError::TokenPositionWithDifferentSettingAlreadyExists, + ); + + // + // TEST: Deposit and withdraw + // + + send_tx( + solana, + TokenDepositInstruction { + amount: 100, + reduce_only: false, + account, + owner, + token_account: payer_mint0_account, + token_authority: payer.clone(), + bank_index: 0, + }, + ) + .await + .unwrap(); + let account_data: MangoAccount = solana.get_account(account).await; + assert_eq!(account_data.tokens[0].unlendable_deposits, 100); + + // provides health + let maint_health = account_maint_health(solana, account).await; + assert_eq_f64!(maint_health, 80.0, 1e-4); + + send_tx( + solana, + TokenWithdrawInstruction { + amount: 50, + allow_borrow: false, + account, + owner, + token_account: payer_mint0_account, + bank_index: 0, + }, + ) + .await + .unwrap(); + let account_data: MangoAccount = solana.get_account(account).await; + assert_eq!(account_data.tokens[0].unlendable_deposits, 50); + + send_tx_expect_error!( + solana, + TokenWithdrawInstruction { + amount: 51, + allow_borrow: true, + account, + owner, + token_account: payer_mint0_account, + bank_index: 0, + }, + MangoError::UnlendableTokenPositionCannotBeNegative + ); + + send_tx( + solana, + TokenWithdrawInstruction { + amount: 50, + allow_borrow: false, + account, + owner, + token_account: payer_mint0_account, + bank_index: 0, + }, + ) + .await + .unwrap(); + + let account_data: MangoAccount = solana.get_account(account).await; + assert_eq!(account_data.tokens[0].unlendable_deposits, 0); + + // not auto-closed + assert!(account_data.tokens[0].is_active()); + + Ok(()) +} diff --git a/programs/mango-v4/tests/program_test/mango_client.rs b/programs/mango-v4/tests/program_test/mango_client.rs index 6fc9040220..c87b2f788f 100644 --- a/programs/mango-v4/tests/program_test/mango_client.rs +++ b/programs/mango-v4/tests/program_test/mango_client.rs @@ -776,6 +776,86 @@ impl ClientInstruction for FlashLoanEndInstruction { } } +#[derive(Clone)] +pub struct TokenCreatePositionInstruction { + pub account: Pubkey, + pub owner: TestKeypair, + pub bank: Pubkey, + pub allow_lending: bool, +} +#[async_trait::async_trait(?Send)] +impl ClientInstruction for TokenCreatePositionInstruction { + type Accounts = mango_v4::accounts::TokenCreateOrClosePosition; + type Instruction = mango_v4::instruction::TokenCreatePosition; + async fn to_instruction( + &self, + account_loader: &(impl ClientAccountLoader + 'async_trait), + ) -> (Self::Accounts, instruction::Instruction) { + let program_id = mango_v4::id(); + let instruction = Self::Instruction { + allow_lending: self.allow_lending, + }; + + let account = account_loader + .load_mango_account(&self.account) + .await + .unwrap(); + + let accounts = Self::Accounts { + group: account.fixed.group, + account: self.account, + owner: self.owner.pubkey(), + bank: self.bank, + }; + + let instruction = make_instruction(program_id, &accounts, &instruction); + (accounts, instruction) + } + + fn signers(&self) -> Vec { + vec![self.owner] + } +} + +#[derive(Clone)] +pub struct TokenClosePositionInstruction { + pub account: Pubkey, + pub owner: TestKeypair, + pub bank: Pubkey, +} +#[async_trait::async_trait(?Send)] +impl ClientInstruction for TokenClosePositionInstruction { + type Accounts = mango_v4::accounts::TokenCreateOrClosePosition; + type Instruction = mango_v4::instruction::TokenClosePosition; + async fn to_instruction( + &self, + account_loader: &(impl ClientAccountLoader + 'async_trait), + ) -> (Self::Accounts, instruction::Instruction) { + let program_id = mango_v4::id(); + let instruction = Self::Instruction {}; + + // load accounts, find PDAs, find remainingAccounts + let account = account_loader + .load_mango_account(&self.account) + .await + .unwrap(); + + let accounts = Self::Accounts { + group: account.fixed.group, + account: self.account, + owner: self.owner.pubkey(), + bank: self.bank, + }; + + let instruction = make_instruction(program_id, &accounts, &instruction); + (accounts, instruction) + } + + fn signers(&self) -> Vec { + vec![self.owner] + } +} + #[derive(Clone)] pub struct TokenWithdrawInstruction { pub amount: u64, From 799c7ee1a333de07d902e25f700e7d986fbbcf0a Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Tue, 12 Mar 2024 15:09:20 +0100 Subject: [PATCH 10/23] liq tests --- .../src/instructions/token_liq_with_token.rs | 11 +- .../mango-v4/tests/cases/test_unlendable.rs | 187 +++++++++++++++++- 2 files changed, 195 insertions(+), 3 deletions(-) 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 ed7fb63cd1..d9e061c3f2 100644 --- a/programs/mango-v4/src/instructions/token_liq_with_token.rs +++ b/programs/mango-v4/src/instructions/token_liq_with_token.rs @@ -226,8 +226,14 @@ pub(crate) fn liquidation_action( // The amount of asset native tokens we will give up for them let asset_transfer_base = liab_transfer * liab_oracle_price / asset_oracle_price; - let asset_transfer_to_liqor = asset_transfer_base * fee_factor_liqor; - let asset_transfer_from_liqee = asset_transfer_base * fee_factor_total; + let mut asset_transfer_to_liqor = asset_transfer_base * fee_factor_liqor; + let mut asset_transfer_from_liqee = asset_transfer_base * fee_factor_total; + + // Converting max asset to liab and back to asset can have introduced rounding errors, ensure + // the transfered amounts are guaranteed < max + assert!(asset_transfer_from_liqee < max_asset_transfer + I80F48::ONE); + asset_transfer_to_liqor = asset_transfer_to_liqor.min(max_asset_transfer); + asset_transfer_from_liqee = asset_transfer_from_liqee.min(max_asset_transfer); let asset_liquidation_fee = asset_transfer_from_liqee - asset_transfer_to_liqor; asset_bank.collected_fees_native += asset_liquidation_fee; @@ -308,6 +314,7 @@ pub(crate) fn liquidation_action( // Check liqee health again let liqee_liq_end_health = liqee_health_cache.health(HealthType::LiquidationEnd); + msg!("liqee liq end health: {}", liqee_liq_end_health); liqee .fixed .maybe_recover_from_being_liquidated(liqee_liq_end_health); diff --git a/programs/mango-v4/tests/cases/test_unlendable.rs b/programs/mango-v4/tests/cases/test_unlendable.rs index 148a3e6413..560a394509 100644 --- a/programs/mango-v4/tests/cases/test_unlendable.rs +++ b/programs/mango-v4/tests/cases/test_unlendable.rs @@ -1,7 +1,7 @@ use super::*; #[tokio::test] -async fn test_unlendable() -> Result<(), TransportError> { +async fn test_unlendable_basic() -> Result<(), TransportError> { let context = TestContext::new().await; let solana = &context.solana.clone(); @@ -190,3 +190,188 @@ async fn test_unlendable() -> Result<(), TransportError> { Ok(()) } + +#[tokio::test] +async fn test_unlendable_liq() -> Result<(), TransportError> { + let context = TestContext::new().await; + let solana = &context.solana.clone(); + + let admin = TestKeypair::new(); + let owner = context.users[0].key; + let payer = context.users[1].key; + let mints = &context.mints[0..2]; + let payer_mint0_account = context.users[1].token_accounts[0]; + let payer_mint1_account = context.users[1].token_accounts[1]; + + // + // SETUP: Create a group, account, register a token (mint0) + // + + 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; + + // Drop loan origination to simplify + send_tx( + solana, + TokenEdit { + group, + admin, + mint: tokens[1].mint.pubkey, + fallback_oracle: Pubkey::default(), + options: mango_v4::instruction::TokenEdit { + loan_origination_fee_rate_opt: Some(0.0), + ..token_edit_instruction_default() + }, + }, + ) + .await + .unwrap(); + + // funding for vaults + create_funded_account( + &solana, + group, + owner, + 1, + &context.users[1], + mints, + 1_000_000, + 0, + ) + .await; + + let liqor = create_funded_account( + &solana, + group, + owner, + 2, + &context.users[1], + &mints[0..1], + 1_000_000, + 0, + ) + .await; + + // + // SETUP: an account with unlendable deposits backing a borrow + // + let account = send_tx( + solana, + AccountCreateInstruction { + account_num: 0, + token_count: 6, + serum3_count: 3, + perp_count: 3, + perp_oo_count: 3, + token_conditional_swap_count: 3, + group, + owner, + payer, + }, + ) + .await + .unwrap() + .account; + + send_tx( + solana, + TokenCreatePositionInstruction { + account, + owner, + bank: tokens[0].bank, + allow_lending: false, + }, + ) + .await + .unwrap(); + + send_tx( + solana, + TokenDepositInstruction { + amount: 1000, + reduce_only: false, + account, + owner, + token_account: payer_mint0_account, + token_authority: payer.clone(), + bank_index: 0, + }, + ) + .await + .unwrap(); + + set_bank_stub_oracle_price(solana, group, &tokens[1], admin, 0.1).await; + send_tx( + solana, + TokenWithdrawInstruction { + amount: 1000, + allow_borrow: true, + account, + owner, + token_account: payer_mint1_account, + bank_index: 0, + }, + ) + .await + .unwrap(); + + // First liquidation until init > -1 + + set_bank_stub_oracle_price(solana, group, &tokens[1], admin, 0.9).await; + send_tx( + solana, + TokenLiqWithTokenInstruction { + liqee: account, + liqor, + liqor_owner: owner, + asset_token_index: tokens[0].index, + liab_token_index: tokens[1].index, + asset_bank_index: 0, + liab_bank_index: 0, + max_liab_transfer: I80F48::from_num(10000.0), + }, + ) + .await + .unwrap(); + + let account_data: MangoAccount = solana.get_account(account).await; + assert_eq!(account_data.tokens[0].unlendable_deposits, 1000 - 753); + assert_eq_f64!( + account_position_f64(solana, account, tokens[1].bank).await, + -1000.0 + 752.2 / (0.9 * 1.02 * 1.02), + 0.1 + ); + assert_eq_f64!(account_init_health(solana, account).await, -0.5, 0.5); + + // Second liquidation to bankruptcy + + set_bank_stub_oracle_price(solana, group, &tokens[1], admin, 1.5).await; + send_tx( + solana, + TokenLiqWithTokenInstruction { + liqee: account, + liqor, + liqor_owner: owner, + asset_token_index: tokens[0].index, + liab_token_index: tokens[1].index, + asset_bank_index: 0, + liab_bank_index: 0, + max_liab_transfer: I80F48::from_num(10000.0), + }, + ) + .await + .unwrap(); + + let account_data: MangoAccount = solana.get_account(account).await; + assert_eq!(account_data.tokens[0].unlendable_deposits, 0); + assert!(account_data.tokens[1].indexed_position < 0); + + Ok(()) +} From 11738dfe1ecf02eb88eec0269c9af0d6df27031a Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Tue, 12 Mar 2024 16:06:45 +0100 Subject: [PATCH 11/23] comments --- .../src/state/mango_account_components.rs | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/programs/mango-v4/src/state/mango_account_components.rs b/programs/mango-v4/src/state/mango_account_components.rs index 3b5ed6e469..824286f714 100644 --- a/programs/mango-v4/src/state/mango_account_components.rs +++ b/programs/mango-v4/src/state/mango_account_components.rs @@ -15,15 +15,11 @@ pub const FREE_ORDER_SLOT: PerpMarketIndex = PerpMarketIndex::MAX; #[derive(AnchorDeserialize, AnchorSerialize, Derivative, PartialEq)] #[derivative(Debug)] pub struct TokenPosition { - // TODO: Why did we have deposits and borrows as two different values - // if only one of them was allowed to be != 0 at a time? - // todo: maybe we want to split collateral and lending? - // todo: see https://github.com/blockworks-foundation/mango-v4/issues/1 - // todo: how does ftx do this? - /// The deposit_index (if positive) or borrow_index (if negative) scaled position + /// The token position, scaled with the deposit_index (if positive) or borrow_index (if negative) + /// to get the lendable/borrowed native token amount pub indexed_position: I80F48, - /// index into Group.tokens + /// index the token is registered with, same as in Bank and MintInfo pub token_index: TokenIndex, /// incremented when a market requires this position to stay alive @@ -31,14 +27,11 @@ pub struct TokenPosition { /// set to 1 when these deposits may not be lent out /// - /// This has wide ranging consequences: - /// - only deposits possible, no borrows (TODO: that means no perps because settlement may cause borrows?) - /// - not accounted for in Bank.indexedDeposits - /// - instead tracked in Bank.unlendableDeposits (to ensure the vault always has them) - /// - indexed_position becomes stores straight token-native amount, fractional will always be 0 - /// - /// TODO: completely borks logging of indexed_position in many instructions - /// TODO: this means all fractional deposits/withdraws are dusted?! + /// This has consequences: + /// - only deposits possible, no borrows (also implying no perps with that settle token) + /// - not accounted for in Bank.indexed_deposits, + /// - instead tracked in Bank.unlendable_deposits (to ensure the vault always has them) + /// - indexed_position stays 0, instead use unlendable_deposits a straight native token amount pub disable_lending: u8, #[derivative(Debug = "ignore")] @@ -54,6 +47,9 @@ pub struct TokenPosition { // Cumulative borrow interest in token native units pub cumulative_borrow_interest: f64, + /// deposited unlendable native token amount + /// + /// When this is set, indexed_position is always zero pub unlendable_deposits: u64, #[derivative(Debug = "ignore")] From 9bb85d70ce69964acc773e1bf3277fccc56f29fd Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Thu, 14 Mar 2024 10:37:54 +0100 Subject: [PATCH 12/23] fixes, disable tcs on unlendable --- programs/mango-v4/src/error.rs | 2 ++ .../token_conditional_swap_create.rs | 16 ++++++++++++++++ .../mango-v4/src/instructions/token_deposit.rs | 4 +--- programs/mango-v4/src/state/bank.rs | 2 +- .../src/state/mango_account_components.rs | 4 ++++ 5 files changed, 24 insertions(+), 4 deletions(-) diff --git a/programs/mango-v4/src/error.rs b/programs/mango-v4/src/error.rs index b174a30adb..9a9cd9a4cf 100644 --- a/programs/mango-v4/src/error.rs +++ b/programs/mango-v4/src/error.rs @@ -157,6 +157,8 @@ pub enum MangoError { TokenPositionWithDifferentSettingAlreadyExists, #[msg("cannot borrow from unlendable token position")] UnlendableTokenPositionCannotBeNegative, + #[msg("token conditional swaps currently don't support unlendable positions")] + TokenConditionalSwapUnsupportedUnlendablePosition, } impl MangoError { diff --git a/programs/mango-v4/src/instructions/token_conditional_swap_create.rs b/programs/mango-v4/src/instructions/token_conditional_swap_create.rs index 454c4a232c..b3313e5470 100644 --- a/programs/mango-v4/src/instructions/token_conditional_swap_create.rs +++ b/programs/mango-v4/src/instructions/token_conditional_swap_create.rs @@ -1,6 +1,7 @@ use anchor_lang::prelude::*; use crate::accounts_ix::*; +use crate::error::*; use crate::logs::{emit_stack, TokenConditionalSwapCreateLogV3}; use crate::state::*; @@ -23,10 +24,25 @@ pub fn token_conditional_swap_create( .ensure_token_position(token_conditional_swap.buy_token_index)? .0; buy_pos.increment_in_use(); + + // The complication with unlendable positions is about withdraws that may fail if the token + // balance goes negative. This could go wrong with tcs start incentives or withdraws during + // execution. If the rounding goes wrong even slightly, it could break even when no borrowing + // was intended or strictly necessary. + // Thus TCS is currently disabled with these token positions until it's well tested. + require!( + buy_pos.allow_lending(), + MangoError::TokenConditionalSwapUnsupportedUnlendablePosition + ); + let sell_pos = account .ensure_token_position(token_conditional_swap.sell_token_index)? .0; sell_pos.increment_in_use(); + require!( + sell_pos.allow_lending(), + MangoError::TokenConditionalSwapUnsupportedUnlendablePosition + ); } let id = account.fixed.next_token_conditional_swap_id; diff --git a/programs/mango-v4/src/instructions/token_deposit.rs b/programs/mango-v4/src/instructions/token_deposit.rs index 3d7f2a4f96..51de125309 100644 --- a/programs/mango-v4/src/instructions/token_deposit.rs +++ b/programs/mango-v4/src/instructions/token_deposit.rs @@ -88,8 +88,6 @@ impl<'a, 'info> DepositCommon<'a, 'info> { // Transfer the actual tokens token::transfer(self.transfer_ctx(), amount_i80f48.to_num::())?; - let indexed_position = position.indexed_position; - // Get the oracle price, even if stale or unconfident: We want to allow users // to deposit to close borrows or do other fixes even if the oracle is bad. let oracle_ref = &AccountInfoRef::borrow(self.oracle.as_ref())?; @@ -100,7 +98,7 @@ impl<'a, 'info> DepositCommon<'a, 'info> { let unsafe_oracle_price = unsafe_oracle_state.price; // If increasing total deposits, check deposit limits - if indexed_position > 0 { + if position.has_deposits() { bank.check_deposit_and_oo_limit()?; } diff --git a/programs/mango-v4/src/state/bank.rs b/programs/mango-v4/src/state/bank.rs index bf4997eec6..f3ec2335f8 100644 --- a/programs/mango-v4/src/state/bank.rs +++ b/programs/mango-v4/src/state/bank.rs @@ -943,7 +943,7 @@ impl Bank { let target_is_active = if !target_amount.is_zero() { let active = self.deposit(target, target_amount, now_ts)?; require!( - target.indexed_position <= 0 || !self.are_deposits_reduce_only(), + !target.has_deposits() || !self.are_deposits_reduce_only(), MangoError::TokenInReduceOnlyMode ); active diff --git a/programs/mango-v4/src/state/mango_account_components.rs b/programs/mango-v4/src/state/mango_account_components.rs index 824286f714..a78989fa0a 100644 --- a/programs/mango-v4/src/state/mango_account_components.rs +++ b/programs/mango-v4/src/state/mango_account_components.rs @@ -127,6 +127,10 @@ impl TokenPosition { pub fn allow_lending(&self) -> bool { self.disable_lending == 0 } + + pub fn has_deposits(&self) -> bool { + self.indexed_position > 0 || self.unlendable_deposits > 0 + } } #[zero_copy] From de57a96a99c7ce76a7b369c8fc7451416f64c7b8 Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Thu, 14 Mar 2024 10:58:46 +0100 Subject: [PATCH 13/23] fix tests by reducing cu --- .../src/instructions/token_liq_with_token.rs | 13 +++++++++---- programs/mango-v4/src/state/mango_account.rs | 4 ++-- programs/mango-v4/tests/cases/test_liq_tokens.rs | 11 +++-------- 3 files changed, 14 insertions(+), 14 deletions(-) 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 d9e061c3f2..2e8d12de09 100644 --- a/programs/mango-v4/src/instructions/token_liq_with_token.rs +++ b/programs/mango-v4/src/instructions/token_liq_with_token.rs @@ -231,7 +231,9 @@ pub(crate) fn liquidation_action( // Converting max asset to liab and back to asset can have introduced rounding errors, ensure // the transfered amounts are guaranteed < max - assert!(asset_transfer_from_liqee < max_asset_transfer + I80F48::ONE); + // The intuition here is: + // asset_to_liab = asset_oracle_price / liab_oracle_price / fee_factor_total + // min(max_asset_transfer * asset_to_liab, max_liab_transfer) / asset_to_liab <= max_asset_transfer asset_transfer_to_liqor = asset_transfer_to_liqor.min(max_asset_transfer); asset_transfer_from_liqee = asset_transfer_from_liqee.min(max_asset_transfer); @@ -279,8 +281,8 @@ pub(crate) fn liquidation_action( msg!( "liquidated {} liab for {} asset", - liab_transfer, - asset_transfer_from_liqee, + liab_transfer.to_num::(), + asset_transfer_from_liqee.to_num::(), ); if liqor_liab_withdraw_result @@ -314,7 +316,10 @@ pub(crate) fn liquidation_action( // Check liqee health again let liqee_liq_end_health = liqee_health_cache.health(HealthType::LiquidationEnd); - msg!("liqee liq end health: {}", liqee_liq_end_health); + msg!( + "liqee liq end health: {}", + liqee_liq_end_health.to_num::() + ); liqee .fixed .maybe_recover_from_being_liquidated(liqee_liq_end_health); diff --git a/programs/mango-v4/src/state/mango_account.rs b/programs/mango-v4/src/state/mango_account.rs index 812105d626..13e3454dda 100644 --- a/programs/mango-v4/src/state/mango_account.rs +++ b/programs/mango-v4/src/state/mango_account.rs @@ -1402,7 +1402,7 @@ impl< pub fn check_health_pre(&mut self, health_cache: &HealthCache) -> Result { let pre_init_health = health_cache.health(HealthType::Init); - msg!("pre_init_health: {}", pre_init_health); + msg!("pre_init_health: {}", pre_init_health.to_num::()); self.check_health_pre_checks(health_cache, pre_init_health)?; Ok(pre_init_health) } @@ -1434,7 +1434,7 @@ impl< pre_init_health: I80F48, ) -> Result { let post_init_health = health_cache.health(HealthType::Init); - msg!("post_init_health: {}", post_init_health); + msg!("post_init_health: {}", post_init_health.to_num::()); self.check_health_post_checks(pre_init_health, post_init_health)?; Ok(post_init_health) } diff --git a/programs/mango-v4/tests/cases/test_liq_tokens.rs b/programs/mango-v4/tests/cases/test_liq_tokens.rs index c20c5eeee6..4ba1896df0 100644 --- a/programs/mango-v4/tests/cases/test_liq_tokens.rs +++ b/programs/mango-v4/tests/cases/test_liq_tokens.rs @@ -165,7 +165,7 @@ async fn test_liq_tokens_force_cancel() -> Result<(), TransportError> { #[tokio::test] async fn test_liq_tokens_with_token() -> Result<(), TransportError> { let mut test_builder = TestContextBuilder::new(); - test_builder.test().set_compute_max_units(85_000); // LiqTokenWithToken needs 79k + test_builder.test().set_compute_max_units(85_000); // LiqTokenWithToken needs 84k let context = test_builder.start_default().await; let solana = &context.solana.clone(); @@ -346,7 +346,7 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> { ) .await .unwrap(); - let res = send_tx( + send_tx_expect_error!( solana, TokenLiqWithTokenInstruction { liqee: account, @@ -358,12 +358,7 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> { liab_bank_index: 0, max_liab_transfer: I80F48::from_num(10000.0), }, - ) - .await; - assert_mango_error( - &res, - MangoError::TokenAssetLiquidationDisabled.into(), - "liquidation disabled".to_string(), + MangoError::TokenAssetLiquidationDisabled ); send_tx( solana, From a2b17f2d08140449aae1536ad29cc39b3e7f5f0f Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Thu, 14 Mar 2024 11:02:35 +0100 Subject: [PATCH 14/23] update idl --- mango_v4.json | 201 ++++++++++++++++++- ts/client/src/mango_v4.ts | 402 +++++++++++++++++++++++++++++++++++++- 2 files changed, 588 insertions(+), 15 deletions(-) diff --git a/mango_v4.json b/mango_v4.json index d041fa22fa..c6ac78e267 100644 --- a/mango_v4.json +++ b/mango_v4.json @@ -1921,6 +1921,75 @@ } ] }, + { + "name": "tokenCreatePosition", + "accounts": [ + { + "name": "group", + "isMut": false, + "isSigner": false + }, + { + "name": "account", + "isMut": true, + "isSigner": false, + "relations": [ + "group" + ] + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "bank", + "isMut": true, + "isSigner": false, + "relations": [ + "group" + ] + } + ], + "args": [ + { + "name": "allowLending", + "type": "bool" + } + ] + }, + { + "name": "tokenClosePosition", + "accounts": [ + { + "name": "group", + "isMut": false, + "isSigner": false + }, + { + "name": "account", + "isMut": true, + "isSigner": false, + "relations": [ + "group" + ] + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "bank", + "isMut": true, + "isSigner": false, + "relations": [ + "group" + ] + } + ], + "args": [] + }, { "name": "tokenDeposit", "accounts": [ @@ -7651,12 +7720,25 @@ ], "type": "f32" }, + { + "name": "padding2", + "type": { + "array": [ + "u8", + 4 + ] + } + }, + { + "name": "unlendableDeposits", + "type": "u64" + }, { "name": "reserved", "type": { "array": [ "u8", - 1900 + 1888 ] } } @@ -9163,7 +9245,8 @@ { "name": "indexedPosition", "docs": [ - "The deposit_index (if positive) or borrow_index (if negative) scaled position" + "The token position, scaled with the deposit_index (if positive) or borrow_index (if negative)", + "to get the lendable/borrowed native token amount" ], "type": { "defined": "I80F48" @@ -9172,7 +9255,7 @@ { "name": "tokenIndex", "docs": [ - "index into Group.tokens" + "index the token is registered with, same as in Bank and MintInfo" ], "type": "u16" }, @@ -9183,12 +9266,25 @@ ], "type": "u16" }, + { + "name": "disableLending", + "docs": [ + "set to 1 when these deposits may not be lent out", + "", + "This has consequences:", + "- only deposits possible, no borrows (also implying no perps with that settle token)", + "- not accounted for in Bank.indexed_deposits,", + "- instead tracked in Bank.unlendable_deposits (to ensure the vault always has them)", + "- indexed_position stays 0, instead use unlendable_deposits a straight native token amount" + ], + "type": "u8" + }, { "name": "padding", "type": { "array": [ "u8", - 4 + 3 ] } }, @@ -9206,12 +9302,21 @@ "name": "cumulativeBorrowInterest", "type": "f64" }, + { + "name": "unlendableDeposits", + "docs": [ + "deposited unlendable native token amount", + "", + "When this is set, indexed_position is always zero" + ], + "type": "u64" + }, { "name": "reserved", "type": { "array": [ "u8", - 128 + 120 ] } } @@ -11008,6 +11113,12 @@ }, { "name": "TokenForceWithdraw" + }, + { + "name": "TokenCreatePosition" + }, + { + "name": "TokenClosePosition" } ] } @@ -11458,6 +11569,51 @@ } ] }, + { + "name": "TokenBalanceLogV2", + "fields": [ + { + "name": "mangoGroup", + "type": "publicKey", + "index": false + }, + { + "name": "mangoAccount", + "type": "publicKey", + "index": false + }, + { + "name": "tokenIndex", + "type": "u16", + "index": false + }, + { + "name": "indexedPosition", + "type": "i128", + "index": false + }, + { + "name": "depositIndex", + "type": "i128", + "index": false + }, + { + "name": "borrowIndex", + "type": "i128", + "index": false + }, + { + "name": "nativePosition", + "type": "u64", + "index": false + }, + { + "name": "allowLending", + "type": "bool", + "index": false + } + ] + }, { "name": "FlashLoanLog", "fields": [ @@ -14350,6 +14506,41 @@ "code": 6069, "name": "TokenAssetLiquidationDisabled", "msg": "the asset does not allow liquidation" + }, + { + "code": 6070, + "name": "BorrowsRequireHealthAccountBank", + "msg": "for borrows the bank must be in the health account list" + }, + { + "code": 6071, + "name": "PerpSettleTokenPositionMustSupportBorrows", + "msg": "perp settle token position does not support borrows" + }, + { + "code": 6072, + "name": "TokenPositionIsInUse", + "msg": "the token position is still in use by another position" + }, + { + "code": 6073, + "name": "TokenPositionBalanceNotZero", + "msg": "the token position has a non-zero balance" + }, + { + "code": 6074, + "name": "TokenPositionWithDifferentSettingAlreadyExists", + "msg": "cannot have a lending and no-lending token position at the same time" + }, + { + "code": 6075, + "name": "UnlendableTokenPositionCannotBeNegative", + "msg": "cannot borrow from unlendable token position" + }, + { + "code": 6076, + "name": "TokenConditionalSwapUnsupportedUnlendablePosition", + "msg": "token conditional swaps currently don't support unlendable positions" } ] } \ No newline at end of file diff --git a/ts/client/src/mango_v4.ts b/ts/client/src/mango_v4.ts index bcce269fba..ab641f5679 100644 --- a/ts/client/src/mango_v4.ts +++ b/ts/client/src/mango_v4.ts @@ -1921,6 +1921,75 @@ export type MangoV4 = { } ] }, + { + "name": "tokenCreatePosition", + "accounts": [ + { + "name": "group", + "isMut": false, + "isSigner": false + }, + { + "name": "account", + "isMut": true, + "isSigner": false, + "relations": [ + "group" + ] + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "bank", + "isMut": true, + "isSigner": false, + "relations": [ + "group" + ] + } + ], + "args": [ + { + "name": "allowLending", + "type": "bool" + } + ] + }, + { + "name": "tokenClosePosition", + "accounts": [ + { + "name": "group", + "isMut": false, + "isSigner": false + }, + { + "name": "account", + "isMut": true, + "isSigner": false, + "relations": [ + "group" + ] + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "bank", + "isMut": true, + "isSigner": false, + "relations": [ + "group" + ] + } + ], + "args": [] + }, { "name": "tokenDeposit", "accounts": [ @@ -7651,12 +7720,25 @@ export type MangoV4 = { ], "type": "f32" }, + { + "name": "padding2", + "type": { + "array": [ + "u8", + 4 + ] + } + }, + { + "name": "unlendableDeposits", + "type": "u64" + }, { "name": "reserved", "type": { "array": [ "u8", - 1900 + 1888 ] } } @@ -9163,7 +9245,8 @@ export type MangoV4 = { { "name": "indexedPosition", "docs": [ - "The deposit_index (if positive) or borrow_index (if negative) scaled position" + "The token position, scaled with the deposit_index (if positive) or borrow_index (if negative)", + "to get the lendable/borrowed native token amount" ], "type": { "defined": "I80F48" @@ -9172,7 +9255,7 @@ export type MangoV4 = { { "name": "tokenIndex", "docs": [ - "index into Group.tokens" + "index the token is registered with, same as in Bank and MintInfo" ], "type": "u16" }, @@ -9183,12 +9266,25 @@ export type MangoV4 = { ], "type": "u16" }, + { + "name": "disableLending", + "docs": [ + "set to 1 when these deposits may not be lent out", + "", + "This has consequences:", + "- only deposits possible, no borrows (also implying no perps with that settle token)", + "- not accounted for in Bank.indexed_deposits,", + "- instead tracked in Bank.unlendable_deposits (to ensure the vault always has them)", + "- indexed_position stays 0, instead use unlendable_deposits a straight native token amount" + ], + "type": "u8" + }, { "name": "padding", "type": { "array": [ "u8", - 4 + 3 ] } }, @@ -9206,12 +9302,21 @@ export type MangoV4 = { "name": "cumulativeBorrowInterest", "type": "f64" }, + { + "name": "unlendableDeposits", + "docs": [ + "deposited unlendable native token amount", + "", + "When this is set, indexed_position is always zero" + ], + "type": "u64" + }, { "name": "reserved", "type": { "array": [ "u8", - 128 + 120 ] } } @@ -11008,6 +11113,12 @@ export type MangoV4 = { }, { "name": "TokenForceWithdraw" + }, + { + "name": "TokenCreatePosition" + }, + { + "name": "TokenClosePosition" } ] } @@ -11458,6 +11569,51 @@ export type MangoV4 = { } ] }, + { + "name": "TokenBalanceLogV2", + "fields": [ + { + "name": "mangoGroup", + "type": "publicKey", + "index": false + }, + { + "name": "mangoAccount", + "type": "publicKey", + "index": false + }, + { + "name": "tokenIndex", + "type": "u16", + "index": false + }, + { + "name": "indexedPosition", + "type": "i128", + "index": false + }, + { + "name": "depositIndex", + "type": "i128", + "index": false + }, + { + "name": "borrowIndex", + "type": "i128", + "index": false + }, + { + "name": "nativePosition", + "type": "u64", + "index": false + }, + { + "name": "allowLending", + "type": "bool", + "index": false + } + ] + }, { "name": "FlashLoanLog", "fields": [ @@ -14350,6 +14506,41 @@ export type MangoV4 = { "code": 6069, "name": "TokenAssetLiquidationDisabled", "msg": "the asset does not allow liquidation" + }, + { + "code": 6070, + "name": "BorrowsRequireHealthAccountBank", + "msg": "for borrows the bank must be in the health account list" + }, + { + "code": 6071, + "name": "PerpSettleTokenPositionMustSupportBorrows", + "msg": "perp settle token position does not support borrows" + }, + { + "code": 6072, + "name": "TokenPositionIsInUse", + "msg": "the token position is still in use by another position" + }, + { + "code": 6073, + "name": "TokenPositionBalanceNotZero", + "msg": "the token position has a non-zero balance" + }, + { + "code": 6074, + "name": "TokenPositionWithDifferentSettingAlreadyExists", + "msg": "cannot have a lending and no-lending token position at the same time" + }, + { + "code": 6075, + "name": "UnlendableTokenPositionCannotBeNegative", + "msg": "cannot borrow from unlendable token position" + }, + { + "code": 6076, + "name": "TokenConditionalSwapUnsupportedUnlendablePosition", + "msg": "token conditional swaps currently don't support unlendable positions" } ] }; @@ -16277,6 +16468,75 @@ export const IDL: MangoV4 = { } ] }, + { + "name": "tokenCreatePosition", + "accounts": [ + { + "name": "group", + "isMut": false, + "isSigner": false + }, + { + "name": "account", + "isMut": true, + "isSigner": false, + "relations": [ + "group" + ] + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "bank", + "isMut": true, + "isSigner": false, + "relations": [ + "group" + ] + } + ], + "args": [ + { + "name": "allowLending", + "type": "bool" + } + ] + }, + { + "name": "tokenClosePosition", + "accounts": [ + { + "name": "group", + "isMut": false, + "isSigner": false + }, + { + "name": "account", + "isMut": true, + "isSigner": false, + "relations": [ + "group" + ] + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "bank", + "isMut": true, + "isSigner": false, + "relations": [ + "group" + ] + } + ], + "args": [] + }, { "name": "tokenDeposit", "accounts": [ @@ -22007,12 +22267,25 @@ export const IDL: MangoV4 = { ], "type": "f32" }, + { + "name": "padding2", + "type": { + "array": [ + "u8", + 4 + ] + } + }, + { + "name": "unlendableDeposits", + "type": "u64" + }, { "name": "reserved", "type": { "array": [ "u8", - 1900 + 1888 ] } } @@ -23519,7 +23792,8 @@ export const IDL: MangoV4 = { { "name": "indexedPosition", "docs": [ - "The deposit_index (if positive) or borrow_index (if negative) scaled position" + "The token position, scaled with the deposit_index (if positive) or borrow_index (if negative)", + "to get the lendable/borrowed native token amount" ], "type": { "defined": "I80F48" @@ -23528,7 +23802,7 @@ export const IDL: MangoV4 = { { "name": "tokenIndex", "docs": [ - "index into Group.tokens" + "index the token is registered with, same as in Bank and MintInfo" ], "type": "u16" }, @@ -23539,12 +23813,25 @@ export const IDL: MangoV4 = { ], "type": "u16" }, + { + "name": "disableLending", + "docs": [ + "set to 1 when these deposits may not be lent out", + "", + "This has consequences:", + "- only deposits possible, no borrows (also implying no perps with that settle token)", + "- not accounted for in Bank.indexed_deposits,", + "- instead tracked in Bank.unlendable_deposits (to ensure the vault always has them)", + "- indexed_position stays 0, instead use unlendable_deposits a straight native token amount" + ], + "type": "u8" + }, { "name": "padding", "type": { "array": [ "u8", - 4 + 3 ] } }, @@ -23562,12 +23849,21 @@ export const IDL: MangoV4 = { "name": "cumulativeBorrowInterest", "type": "f64" }, + { + "name": "unlendableDeposits", + "docs": [ + "deposited unlendable native token amount", + "", + "When this is set, indexed_position is always zero" + ], + "type": "u64" + }, { "name": "reserved", "type": { "array": [ "u8", - 128 + 120 ] } } @@ -25364,6 +25660,12 @@ export const IDL: MangoV4 = { }, { "name": "TokenForceWithdraw" + }, + { + "name": "TokenCreatePosition" + }, + { + "name": "TokenClosePosition" } ] } @@ -25814,6 +26116,51 @@ export const IDL: MangoV4 = { } ] }, + { + "name": "TokenBalanceLogV2", + "fields": [ + { + "name": "mangoGroup", + "type": "publicKey", + "index": false + }, + { + "name": "mangoAccount", + "type": "publicKey", + "index": false + }, + { + "name": "tokenIndex", + "type": "u16", + "index": false + }, + { + "name": "indexedPosition", + "type": "i128", + "index": false + }, + { + "name": "depositIndex", + "type": "i128", + "index": false + }, + { + "name": "borrowIndex", + "type": "i128", + "index": false + }, + { + "name": "nativePosition", + "type": "u64", + "index": false + }, + { + "name": "allowLending", + "type": "bool", + "index": false + } + ] + }, { "name": "FlashLoanLog", "fields": [ @@ -28706,6 +29053,41 @@ export const IDL: MangoV4 = { "code": 6069, "name": "TokenAssetLiquidationDisabled", "msg": "the asset does not allow liquidation" + }, + { + "code": 6070, + "name": "BorrowsRequireHealthAccountBank", + "msg": "for borrows the bank must be in the health account list" + }, + { + "code": 6071, + "name": "PerpSettleTokenPositionMustSupportBorrows", + "msg": "perp settle token position does not support borrows" + }, + { + "code": 6072, + "name": "TokenPositionIsInUse", + "msg": "the token position is still in use by another position" + }, + { + "code": 6073, + "name": "TokenPositionBalanceNotZero", + "msg": "the token position has a non-zero balance" + }, + { + "code": 6074, + "name": "TokenPositionWithDifferentSettingAlreadyExists", + "msg": "cannot have a lending and no-lending token position at the same time" + }, + { + "code": 6075, + "name": "UnlendableTokenPositionCannotBeNegative", + "msg": "cannot borrow from unlendable token position" + }, + { + "code": 6076, + "name": "TokenConditionalSwapUnsupportedUnlendablePosition", + "msg": "token conditional swaps currently don't support unlendable positions" } ] }; From 8924dfbdbf1aff602d7b7d8bcc2af587bb49308a Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Thu, 14 Mar 2024 11:15:07 +0100 Subject: [PATCH 15/23] ts: token positions --- ts/client/src/accounts/mangoAccount.ts | 26 ++++++++++++++++++++------ ts/client/src/numbers/I80F48.ts | 4 ++++ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/ts/client/src/accounts/mangoAccount.ts b/ts/client/src/accounts/mangoAccount.ts index c8d4dbad27..2316687c79 100644 --- a/ts/client/src/accounts/mangoAccount.ts +++ b/ts/client/src/accounts/mangoAccount.ts @@ -1201,6 +1201,8 @@ export class TokenPosition { I80F48.from(dto.previousIndex), dto.cumulativeDepositInterest, dto.cumulativeBorrowInterest, + dto.disableLending != 0, + dto.unlendableDeposits, ); } @@ -1211,22 +1213,32 @@ export class TokenPosition { public previousIndex: I80F48, public cumulativeDepositInterest: number, public cumulativeBorrowInterest: number, + public disableLending: boolean, + public unlendableDeposits: BN, ) {} public isActive(): boolean { return this.tokenIndex !== TokenPosition.TokenIndexUnset; } + public allowLending(): boolean { + return !this.disableLending; + } + /** * * @param bank * @returns native balance */ public balance(bank: Bank): I80F48 { - if (this.indexedPosition.isPos()) { - return bank.depositIndex.mul(this.indexedPosition); + if (this.allowLending()) { + if (this.indexedPosition.isPos()) { + return bank.depositIndex.mul(this.indexedPosition); + } else { + return bank.borrowIndex.mul(this.indexedPosition); + } } else { - return bank.borrowIndex.mul(this.indexedPosition); + return I80F48.fromU64(this.unlendableDeposits); } } @@ -1248,10 +1260,10 @@ export class TokenPosition { * @returns native borrows, 0 if position has deposits */ public borrows(bank: Bank): I80F48 { - if (this.indexedPosition && this.indexedPosition.gt(ZERO_I80F48())) { - return ZERO_I80F48(); + if (this.indexedPosition && this.indexedPosition.lt(ZERO_I80F48())) { + return this.balance(bank).abs(); } - return this.balance(bank).abs(); + return ZERO_I80F48(); } /** @@ -1312,6 +1324,8 @@ export class TokenPositionDto { public previousIndex: I80F48Dto, public cumulativeDepositInterest: number, public cumulativeBorrowInterest: number, + public disableLending: number, + public unlendableDeposits: BN, ) {} } diff --git a/ts/client/src/numbers/I80F48.ts b/ts/client/src/numbers/I80F48.ts index d683ae6a5f..303e432257 100644 --- a/ts/client/src/numbers/I80F48.ts +++ b/ts/client/src/numbers/I80F48.ts @@ -36,6 +36,10 @@ export class I80F48 { return new I80F48(dto.val); } + // Note: this is equivalent to I80F48::from_bits(), meaning that + // new I80F48(1) is not ONE_I80F48 but the smallest representable + // positive number! + // Use fromU64(n) to represent the integer n. constructor(data: BN) { if (data.lt(I80F48.MIN_BN) || data.gt(I80F48.MAX_BN)) { throw new Error('Number out of range'); From 37ae6879efa292878453b757f4c075ab415a8a8e Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Thu, 14 Mar 2024 11:21:04 +0100 Subject: [PATCH 16/23] ts: bank deposits need to audit all uses still --- programs/mango-v4/src/state/bank.rs | 1 + ts/client/src/accounts/bank.ts | 13 ++++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/programs/mango-v4/src/state/bank.rs b/programs/mango-v4/src/state/bank.rs index f3ec2335f8..c4594309bf 100644 --- a/programs/mango-v4/src/state/bank.rs +++ b/programs/mango-v4/src/state/bank.rs @@ -234,6 +234,7 @@ pub struct Bank { #[derivative(Debug = "ignore")] pub padding2: [u8; 4], + /// The sum of native tokens in unlendable token positions pub unlendable_deposits: u64, #[derivative(Debug = "ignore")] diff --git a/ts/client/src/accounts/bank.ts b/ts/client/src/accounts/bank.ts index 986c06cb75..6b6e94235f 100644 --- a/ts/client/src/accounts/bank.ts +++ b/ts/client/src/accounts/bank.ts @@ -152,6 +152,7 @@ export class Bank implements BankForHealth { collectedLiquidationFees: I80F48Dto; collectedCollateralFees: I80F48Dto; collateralFeePerDay: number; + unlendableDeposits: BN; }, ): Bank { return new Bank( @@ -220,6 +221,7 @@ export class Bank implements BankForHealth { obj.collectedCollateralFees, obj.collateralFeePerDay, obj.forceWithdraw == 1, + obj.unlendableDeposits, ); } @@ -289,6 +291,7 @@ export class Bank implements BankForHealth { collectedCollateralFees: I80F48Dto, public collateralFeePerDay: number, public forceWithdraw: boolean, + public unlendableDeposts: BN, ) { this.name = utf8.decode(new Uint8Array(name)).split('\x00')[0]; this.oracleConfig = { @@ -515,10 +518,18 @@ export class Bank implements BankForHealth { return this._oracleProvider; } - nativeDeposits(): I80F48 { + nativeLendableDeposits(): I80F48 { return this.indexedDeposits.mul(this.depositIndex); } + nativeUnlendableDeposits(): I80F48 { + return I80F48.fromU64(this.unlendableDeposts); + } + + nativeDeposits(): I80F48 { + return this.nativeUnlendableDeposits().add(this.nativeLendableDeposits()); + } + nativeBorrows(): I80F48 { return this.indexedBorrows.mul(this.borrowIndex); } From 7084a8bfc9e63b24ad7df0438544bfad8b406c97 Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Thu, 14 Mar 2024 12:06:25 +0100 Subject: [PATCH 17/23] ts: deposit split into lendable and unlendable --- ts/client/scripts/liqtest/README.md | 1 + ts/client/src/accounts/bank.ts | 78 ++++++++++++++------- ts/client/src/accounts/mangoAccount.spec.ts | 24 ++++++- ts/client/src/accounts/mangoAccount.ts | 53 +++++++++----- ts/client/src/numbers/I80F48.ts | 19 +++-- 5 files changed, 128 insertions(+), 47 deletions(-) diff --git a/ts/client/scripts/liqtest/README.md b/ts/client/scripts/liqtest/README.md index 847fcfdaff..f1889404d3 100644 --- a/ts/client/scripts/liqtest/README.md +++ b/ts/client/scripts/liqtest/README.md @@ -51,6 +51,7 @@ This creates a bunch of to-be-liquidated accounts as well as a LIQOR account. Run the liquidator on the group with the liqor account. Since devnet doesn't have any jupiter, run with + ``` JUPITER_VERSION=mock TCS_MODE=borrow-buy diff --git a/ts/client/src/accounts/bank.ts b/ts/client/src/accounts/bank.ts index 06f22b5a2b..3c9226b485 100644 --- a/ts/client/src/accounts/bank.ts +++ b/ts/client/src/accounts/bank.ts @@ -547,14 +547,14 @@ export class Bank implements BankForHealth { */ getBorrowRateWithoutUpkeepRate(): I80F48 { const totalBorrows = this.nativeBorrows(); - const totalDeposits = this.nativeDeposits(); + const lendableDeposits = this.nativeLendableDeposits(); - if (totalDeposits.isZero() || totalBorrows.isZero()) { + if (lendableDeposits.isZero() || totalBorrows.isZero()) { return ZERO_I80F48(); } const utilization = totalBorrows - .div(totalDeposits) + .div(lendableDeposits) .max(ZERO_I80F48()) .min(ONE_I80F48()); const scaling = I80F48.fromNumber( @@ -599,15 +599,15 @@ export class Bank implements BankForHealth { getDepositRate(): I80F48 { const borrowRate = this.getBorrowRate(); const totalBorrows = this.nativeBorrows(); - const totalDeposits = this.nativeDeposits(); + const lendableDeposits = this.nativeLendableDeposits(); - if (totalDeposits.isZero() && totalBorrows.isZero()) { + if (lendableDeposits.isZero() && totalBorrows.isZero()) { return ZERO_I80F48(); - } else if (totalDeposits.isZero()) { + } else if (lendableDeposits.isZero()) { return this.maxRate; } - const utilization = totalBorrows.div(totalDeposits); + const utilization = totalBorrows.div(lendableDeposits); return utilization.mul(borrowRate); } @@ -633,26 +633,56 @@ export class Bank implements BankForHealth { return toUiDecimals(this.getNetBorrowLimitPerWindow(), this.mintDecimals); } - getMaxWithdraw(vaultBalance: BN, userDeposits = ZERO_I80F48()): I80F48 { + /** + * The maximum someone with `userDeposits` could withdraw from the bank + * if they had infinite health. + */ + getMaxWithdraw( + vaultBalance: BN, + userDeposits = ZERO_I80F48(), + userPositionWithLending: boolean, + ): I80F48 { userDeposits = userDeposits.max(ZERO_I80F48()); + let vaultBalanceI80F48 = I80F48.fromU64(vaultBalance); + + // It may not be possible to fully withdraw userDeposits - check for that and return early + let availableWithdraws; + if (userPositionWithLending) { + availableWithdraws = this.nativeLendableDeposits() + .sub(this.nativeBorrows()) + .min(vaultBalanceI80F48); + } else { + availableWithdraws = vaultBalanceI80F48; + } + if (availableWithdraws.lte(userDeposits)) { + return availableWithdraws.max(ZERO_I80F48()); + } + if (!userPositionWithLending) { + return userDeposits; + } - // any borrow must respect the minVaultToDepositsRatio - const minVaultBalanceRequired = this.nativeDeposits().mul( - I80F48.fromNumber(this.minVaultToDepositsRatio), - ); - const maxBorrowFromVault = I80F48.fromI64(vaultBalance) - .sub(minVaultBalanceRequired) + // Now we know all userDeposits are withdrawn. How much can be borrowed? + + const maxBorrowByVault = vaultBalanceI80F48 + .sub(userDeposits) + .max(ZERO_I80F48()); + + const maxUtilization = I80F48.fromNumber(1 - this.minVaultToDepositsRatio); + const maxBorrowByUtilization = this.nativeLendableDeposits() + .sub(userDeposits) + .mul(maxUtilization) + .sub(this.nativeBorrows()) .max(ZERO_I80F48()); - // User deposits can exceed maxWithdrawFromVault - let maxBorrow = maxBorrowFromVault.sub(userDeposits).max(ZERO_I80F48()); - // any borrow must respect the limit left in window - maxBorrow = maxBorrow.min(this.getBorrowLimitLeftInWindow()); - // borrows would be applied a fee - maxBorrow = maxBorrow.div(ONE_I80F48().add(this.loanOriginationFeeRate)); - - // user deposits can always be withdrawn - // even if vaults can be depleted - return maxBorrow.add(userDeposits).min(I80F48.fromI64(vaultBalance)); + + const maxBorrow = maxBorrowByVault + .min(maxBorrowByUtilization) + .min(this.getBorrowLimitLeftInWindow()); + + const maxBorrowAsk = maxBorrow.div( + ONE_I80F48().add(this.loanOriginationFeeRate), + ); + + return maxBorrowAsk.add(userDeposits); } getTimeToNextBorrowLimitWindowStartsTs(): number { diff --git a/ts/client/src/accounts/mangoAccount.spec.ts b/ts/client/src/accounts/mangoAccount.spec.ts index 949c90ad08..3731612736 100644 --- a/ts/client/src/accounts/mangoAccount.spec.ts +++ b/ts/client/src/accounts/mangoAccount.spec.ts @@ -115,10 +115,28 @@ describe('maxWithdraw', () => { new Map(), ); protoAccount.tokens.push( - new TokenPosition(ZERO_I80F48(), 0 as TokenIndex, 0, ZERO_I80F48(), 0, 0, false, new BN(0)), + new TokenPosition( + ZERO_I80F48(), + 0 as TokenIndex, + 0, + ZERO_I80F48(), + 0, + 0, + false, + new BN(0), + ), ); protoAccount.tokens.push( - new TokenPosition(ZERO_I80F48(), 1 as TokenIndex, 0, ZERO_I80F48(), 0, 0, false, new BN(0)), + new TokenPosition( + ZERO_I80F48(), + 1 as TokenIndex, + 0, + ZERO_I80F48(), + 0, + 0, + false, + new BN(0), + ), ); const protoBank = { @@ -150,7 +168,7 @@ describe('maxWithdraw', () => { borrowIndex: I80F48.fromNumber(1000000), indexedDeposits: I80F48.fromNumber(0), indexedBorrows: I80F48.fromNumber(0), - nativeDeposits() { + nativeLendableDeposits() { return this.depositIndex.mul(this.indexedDeposits); }, nativeBorrows() { diff --git a/ts/client/src/accounts/mangoAccount.ts b/ts/client/src/accounts/mangoAccount.ts index 0c11f7aaa3..f173c19bea 100644 --- a/ts/client/src/accounts/mangoAccount.ts +++ b/ts/client/src/accounts/mangoAccount.ts @@ -565,12 +565,14 @@ export class MangoAccount { // To do that, we first get an upper bound that the search can start with. const existingTokenDeposits = tp ? tp.deposits(tokenBank) : ZERO_I80F48(); + const allowLending = tp ? tp.allowLending() : true; const lowerBoundBorrowHealthFactor = tokenBank .getLiabPrice() .mul(tokenBank.scaledInitLiabWeight(tokenBank.getLiabPrice())); - const upperBound = existingTokenDeposits.add( - initHealth.div(lowerBoundBorrowHealthFactor), - ); + let upperBound = existingTokenDeposits; + if (allowLending) { + upperBound.iadd(initHealth.div(lowerBoundBorrowHealthFactor)); + } // Step 2: Find the maximum withdraw amount @@ -591,23 +593,36 @@ export class MangoAccount { .sub(borrowCost); // Update the bank and the scaled weights - mutTokenBank.indexedDeposits = tokenBank.indexedDeposits.sub( - withdrawOfDepositsAmount.div(tokenBank.depositIndex), - ); - mutTokenBank.indexedBorrows = tokenBank.indexedBorrows.add( - borrowCost.div(tokenBank.borrowIndex), - ); - if (mutTokenBank.nativeBorrows().gt(mutTokenBank.nativeDeposits())) { - return invalidHealthValue; - } - if (borrowAmount.isPos()) { + if (allowLending) { + mutTokenBank.indexedDeposits = tokenBank.indexedDeposits.sub( + withdrawOfDepositsAmount.div(tokenBank.depositIndex), + ); + mutTokenBank.indexedBorrows = tokenBank.indexedBorrows.add( + borrowCost.div(tokenBank.borrowIndex), + ); if ( - mutTokenBank - .nativeBorrows() - .gt(mutTokenBank.nativeDeposits().mul(maxBorrowUtilization)) + mutTokenBank.nativeBorrows().gt(mutTokenBank.nativeLendableDeposits()) ) { return invalidHealthValue; } + if (borrowAmount.isPos()) { + if ( + mutTokenBank + .nativeBorrows() + .gt( + mutTokenBank.nativeLendableDeposits().mul(maxBorrowUtilization), + ) + ) { + return invalidHealthValue; + } + } + } else { + mutTokenBank.unlendableDeposts = tokenBank.unlendableDeposts.sub( + withdrawOfDepositsAmount.ceil().toBN(), + ); + if (borrowAmount.isPos()) { + return invalidHealthValue; + } } mutTi.initScaledAssetWeight = mutTokenBank.scaledInitAssetWeight( tokenBank.getAssetPrice(), @@ -715,9 +730,11 @@ export class MangoAccount { ), ); const sourceBalance = this.getEffectiveTokenBalance(group, sourceBank); + const sourceAllowsLending = this.getToken(sourceBank.tokenIndex)?.allowLending() ?? true; const maxWithdrawNative = sourceBank.getMaxWithdraw( group.getTokenVaultBalanceByMint(sourceBank.mint), sourceBalance, + sourceAllowsLending, ); maxSource = maxSource.min(maxWithdrawNative); @@ -873,9 +890,11 @@ export class MangoAccount { let quoteAmount = nativeAmount.div(quoteBank.price); const quoteBalance = this.getEffectiveTokenBalance(group, quoteBank); + const quoteAllowsLending = this.getToken(quoteBank.tokenIndex)?.allowLending() ?? true; const maxWithdrawNative = quoteBank.getMaxWithdraw( group.getTokenVaultBalanceByMint(quoteBank.mint), quoteBalance, + quoteAllowsLending, ); quoteAmount = quoteAmount.min(maxWithdrawNative); @@ -928,9 +947,11 @@ export class MangoAccount { let baseAmount = nativeAmount.div(baseBank.price); const baseBalance = this.getEffectiveTokenBalance(group, baseBank); + const baseAllowsLending = this.getToken(baseBank.tokenIndex)?.allowLending() ?? true; const maxWithdrawNative = baseBank.getMaxWithdraw( group.getTokenVaultBalanceByMint(baseBank.mint), baseBalance, + baseAllowsLending, ); baseAmount = baseAmount.min(maxWithdrawNative); diff --git a/ts/client/src/numbers/I80F48.ts b/ts/client/src/numbers/I80F48.ts index 303e432257..30c260913b 100644 --- a/ts/client/src/numbers/I80F48.ts +++ b/ts/client/src/numbers/I80F48.ts @@ -36,10 +36,12 @@ export class I80F48 { return new I80F48(dto.val); } - // Note: this is equivalent to I80F48::from_bits(), meaning that - // new I80F48(1) is not ONE_I80F48 but the smallest representable - // positive number! - // Use fromU64(n) to represent the integer n. + /** + * Note: this is equivalent to I80F48::from_bits(), meaning that + * new I80F48(1) is not ONE_I80F48 but the smallest representable + * positive number! + * Use fromU64(n) to represent the integer n. + */ constructor(data: BN) { if (data.lt(I80F48.MIN_BN) || data.gt(I80F48.MAX_BN)) { throw new Error('Number out of range'); @@ -91,6 +93,15 @@ export class I80F48 { ): string { return this.toNumber().toLocaleString(locales, options); } + /** + * The integer part as a BN + */ + toBN(): BN { + return this.data.div(I80F48.MULTIPLIER_BN); + } + /** + * The integer part as a Big + */ toBig(): Big { return new Big(this.data.toString()).div(I80F48.MULTIPLIER_BIG); } From 8792a8fe7eac5cdec47260e974c8252dda55fe1d Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Fri, 15 Mar 2024 12:04:06 +0100 Subject: [PATCH 18/23] rs: fix max_swap and max_borrow for unlendable token positions --- programs/mango-v4/src/health/client.rs | 66 ++++++++++++++++++- .../mango-v4/tests/cases/test_health_check.rs | 2 +- 2 files changed, 64 insertions(+), 4 deletions(-) diff --git a/programs/mango-v4/src/health/client.rs b/programs/mango-v4/src/health/client.rs index c7bad10092..ea298cea59 100644 --- a/programs/mango-v4/src/health/client.rs +++ b/programs/mango-v4/src/health/client.rs @@ -24,6 +24,7 @@ impl HealthCache { /// Errors: /// - If there are no existing token positions for the source or target index. /// - If the withdraw fails due to the net borrow limit. + /// - If the withdraw fails due to borrows on a position with no lending fn cache_after_swap( &self, account: &MangoAccountValue, @@ -196,7 +197,7 @@ impl HealthCache { } let cache_after_swap = |amount: I80F48| -> Result> { - ignore_net_borrow_limit_errors(self.cache_after_swap( + ignore_limit_errors(self.cache_after_swap( account, source_bank, source_oracle_price, @@ -456,7 +457,7 @@ impl HealthCache { Ok(resulting_cache) }; let fn_value_after_borrow = |amount: I80F48| -> Result { - Ok(ignore_net_borrow_limit_errors(cache_after_borrow(amount))? + Ok(ignore_limit_errors(cache_after_borrow(amount))? .as_ref() .map(target_fn) .unwrap_or(I80F48::MIN)) @@ -628,12 +629,18 @@ fn find_maximum( } } -fn ignore_net_borrow_limit_errors(maybe_cache: Result) -> Result> { +fn ignore_limit_errors(maybe_cache: Result) -> Result> { // Special case net borrow errors: We want to be able to find a good // swap amount even if the max swap is limited by the net borrow limit. if maybe_cache.is_anchor_error_with_code(MangoError::BankNetBorrowsLimitReached.error_code()) { return Ok(None); } + // Same for withdrawing from no-lending positions + if maybe_cache + .is_anchor_error_with_code(MangoError::UnlendableTokenPositionCannotBeNegative.error_code()) + { + return Ok(None); + } maybe_cache.map(|c| Some(c)) } @@ -1730,4 +1737,57 @@ mod tests { assert!(leverage_eq(&health_cache, 2.0)); } + + #[test] + fn test_max_no_lending() { + let buffer = MangoAccount::default_for_tests().try_to_vec().unwrap(); + let mut account = MangoAccountValue::from_bytes(&buffer).unwrap(); + let tp0 = account.ensure_token_position(0).unwrap().0; + tp0.disable_lending = 1; + tp0.unlendable_deposits = 100; + account.ensure_token_position(1).unwrap(); + + let group = Pubkey::new_unique(); + let (mut bank0, _) = mock_bank_and_oracle(group, 0, 1.0, 0.0, 0.0); + let (mut bank1, _) = mock_bank_and_oracle(group, 1, 2.0, 0.2, 0.2); + let bank0_data = bank0.data(); + bank0_data.unlendable_deposits = 1000; // assume other accounts also deposited + let bank1_data = bank1.data(); + + let health_cache = HealthCache { + token_infos: vec![ + TokenInfo { + token_index: 0, + balance_spot: I80F48::from(100), + ..default_token_info(0.0, 1.0) + }, + TokenInfo { + token_index: 1, + balance_spot: I80F48::from(100), + ..default_token_info(0.2, 2.0) + }, + ], + serum3_infos: vec![], + perp_infos: vec![], + being_liquidated: false, + }; + + let max_swap = health_cache + .max_swap_source_for_health_ratio_with_limits( + &account, + bank0_data, + I80F48::from_num(1.0), + bank1_data, + I80F48::from_num(0.5), + I80F48::from_num(1.0), + ) + .unwrap(); + assert_eq!(max_swap, 100); + + let max_borrow = health_cache + .max_borrow_for_health_ratio(&account, bank0_data, I80F48::from_num(1.0)) + .unwrap(); + assert!(max_borrow < 100); + assert!(max_borrow > 99); + } } diff --git a/programs/mango-v4/tests/cases/test_health_check.rs b/programs/mango-v4/tests/cases/test_health_check.rs index b4624218b6..2363da1bcf 100644 --- a/programs/mango-v4/tests/cases/test_health_check.rs +++ b/programs/mango-v4/tests/cases/test_health_check.rs @@ -3,7 +3,7 @@ use crate::cases::{ HealthCheckInstruction, TestContext, TestKeypair, TokenWithdrawInstruction, }; use crate::send_tx_expect_error; -use mango_v4::accounts_ix::{HealthCheck, HealthCheckKind}; +use mango_v4::accounts_ix::HealthCheckKind; use mango_v4::error::MangoError; use solana_sdk::transport::TransportError; From 68ebe1d3472a798e23d633b93f97d9775fde0682 Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Mon, 18 Mar 2024 09:18:59 +0100 Subject: [PATCH 19/23] flash loan: fix vault amount --- programs/mango-v4/src/instructions/flash_loan.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/programs/mango-v4/src/instructions/flash_loan.rs b/programs/mango-v4/src/instructions/flash_loan.rs index 8e72ad5970..e720ffba74 100644 --- a/programs/mango-v4/src/instructions/flash_loan.rs +++ b/programs/mango-v4/src/instructions/flash_loan.rs @@ -258,7 +258,7 @@ struct TokenVaultChange { bank_index: usize, raw_token_index: usize, amount: I80F48, - vault_balance: u64, + after_vault_balance: u64, } pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>( @@ -361,12 +361,14 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>( max_swap_fee_rate = max_swap_fee_rate.max(bank.flash_loan_swap_fee_rate); + let vault = Account::::try_from(vault_ai)?; + changes.push(TokenVaultChange { token_index: bank.token_index, bank_index: i, raw_token_index, amount: change, - vault_balance: token_account.amount, + after_vault_balance: vault.amount, }); } @@ -494,7 +496,7 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>( // Verify that the no-lending amount on the vault remains. If the acting account // itself had a no-lending position, the unlendable_deposits has already been reduced. require_gte!( - change.vault_balance, + change.after_vault_balance, bank.unlendable_deposits, MangoError::InsufficentBankVaultFunds ); From c07a1da8ca2167099801052e4a8b8b1f660c370f Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Mon, 18 Mar 2024 09:20:25 +0100 Subject: [PATCH 20/23] rename token balance log field --- programs/mango-v4/src/instructions/token_liq_with_token.rs | 2 +- programs/mango-v4/src/logs.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) 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 2e8d12de09..96a482d264 100644 --- a/programs/mango-v4/src/instructions/token_liq_with_token.rs +++ b/programs/mango-v4/src/instructions/token_liq_with_token.rs @@ -230,7 +230,7 @@ pub(crate) fn liquidation_action( let mut asset_transfer_from_liqee = asset_transfer_base * fee_factor_total; // Converting max asset to liab and back to asset can have introduced rounding errors, ensure - // the transfered amounts are guaranteed < max + // the transfered amounts are guaranteed <= max // The intuition here is: // asset_to_liab = asset_oracle_price / liab_oracle_price / fee_factor_total // min(max_asset_transfer * asset_to_liab, max_liab_transfer) / asset_to_liab <= max_asset_transfer diff --git a/programs/mango-v4/src/logs.rs b/programs/mango-v4/src/logs.rs index 1d94c1eabc..38aeba492e 100644 --- a/programs/mango-v4/src/logs.rs +++ b/programs/mango-v4/src/logs.rs @@ -62,7 +62,7 @@ pub fn emit_token_balance_log(mango_account: Pubkey, bank: &Bank, token_position indexed_position: token_position.indexed_position.to_bits(), deposit_index: bank.deposit_index.to_bits(), borrow_index: bank.borrow_index.to_bits(), - native_position: token_position.unlendable_deposits, + unlendable_deposits: token_position.unlendable_deposits, allow_lending: token_position.allow_lending(), }); } @@ -85,7 +85,7 @@ pub struct TokenBalanceLogV2 { pub indexed_position: i128, // on client convert i128 to I80F48 easily by passing in the BN to I80F48 ctor pub deposit_index: i128, // I80F48 pub borrow_index: i128, // I80F48 - pub native_position: u64, + pub unlendable_deposits: u64, pub allow_lending: bool, } From 2ea729982824989c59411e38e9194caa4d7e633c Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Mon, 18 Mar 2024 09:21:27 +0100 Subject: [PATCH 21/23] comment --- programs/mango-v4/src/state/bank.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/programs/mango-v4/src/state/bank.rs b/programs/mango-v4/src/state/bank.rs index c4594309bf..fdc762b31e 100644 --- a/programs/mango-v4/src/state/bank.rs +++ b/programs/mango-v4/src/state/bank.rs @@ -748,7 +748,8 @@ impl Bank { } else { assert!(position.indexed_position.is_zero()); - // TODO: might there be trouble by rounding up here? + // Note: this rounds up native_amount, withdrawing a fraction of a token more + // than desired and dusting the difference! let withdraw_amount = native_amount.ceil(); self.dust += withdraw_amount - native_amount; From a8a1c97856eae0c3526db73ea20ddf8e5d551400 Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Mon, 18 Mar 2024 09:21:59 +0100 Subject: [PATCH 22/23] fix TokenPos size computation --- programs/mango-v4/src/state/mango_account_components.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/programs/mango-v4/src/state/mango_account_components.rs b/programs/mango-v4/src/state/mango_account_components.rs index a78989fa0a..de635be3b6 100644 --- a/programs/mango-v4/src/state/mango_account_components.rs +++ b/programs/mango-v4/src/state/mango_account_components.rs @@ -58,7 +58,7 @@ pub struct TokenPosition { const_assert_eq!( size_of::(), - 16 + 2 + 2 + 4 + 16 + 8 + 8 + 8 + 120 + 16 + 2 + 2 + 1 + 3 + 16 + 8 + 8 + 8 + 120 ); const_assert_eq!(size_of::(), 184); const_assert_eq!(size_of::() % 8, 0); From f962294dc2af58272236b830a4efdfc7b7c6d9c9 Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Mon, 18 Mar 2024 09:58:25 +0100 Subject: [PATCH 23/23] ts vault use fixes --- ts/client/src/accounts/group.ts | 24 +++++++++++++++++- ts/client/src/accounts/mangoAccount.spec.ts | 15 +++++++----- ts/client/src/accounts/mangoAccount.ts | 27 ++++++++++----------- 3 files changed, 45 insertions(+), 21 deletions(-) diff --git a/ts/client/src/accounts/group.ts b/ts/client/src/accounts/group.ts index b486d14326..33d60f74b1 100644 --- a/ts/client/src/accounts/group.ts +++ b/ts/client/src/accounts/group.ts @@ -597,7 +597,7 @@ export class Group { const totalAmount = new BN(0); for (const bank of banks) { const amount = this.vaultAmountsMap.get(bank.vault.toBase58()); - if (!amount) { + if (amount === undefined) { throw new Error( `Vault balance not found for bank ${bank.name} ${bank.bankNum}!`, ); @@ -608,6 +608,28 @@ export class Group { return totalAmount; } + /** + * If allowLending is true, the withdrawer has a lendable position (and can withdraw less + * than the full vault amount) + */ + public getTokenVaultWithdrawableByBank( + bank: Bank, + allowLending: boolean, + ): BN { + const amount = this.vaultAmountsMap.get(bank.vault.toBase58()); + if (amount === undefined) { + throw new Error( + `Vault balance not found for bank ${bank.name} ${bank.bankNum}!`, + ); + } + + if (allowLending) { + return amount.sub(bank.unlendableDeposts); + } else { + return amount; + } + } + /** * * @param mintPk diff --git a/ts/client/src/accounts/mangoAccount.spec.ts b/ts/client/src/accounts/mangoAccount.spec.ts index 3731612736..443a608b78 100644 --- a/ts/client/src/accounts/mangoAccount.spec.ts +++ b/ts/client/src/accounts/mangoAccount.spec.ts @@ -194,9 +194,9 @@ describe('maxWithdraw', () => { getFirstBankForPerpSettlement() { return bank0; }, - vaultAmountsMap: new Map([ - [bank0.vault.toBase58(), new BN(vaultAmount)], - ]), + getTokenVaultWithdrawableByBank(bank: Bank, allowLending: boolean): BN { + return new BN(vaultAmount); + }, } as any as Group; } @@ -281,10 +281,11 @@ describe('maxWithdraw', () => { it('pure borrow limited utilization', (done) => { const [group, bank0, bank1, account] = setup(1000000); + account.tokens[0].disableLending = false; const other = deepClone(account); deposit(bank0, other, 50); deposit(bank1, account, 100); - expect(maxWithdraw(group, account)).equal(44); // due to origination fees! + expect(maxWithdraw(group, account)).equal(44); // not 45 due to origination fees! bank0.loanOriginationFeeRate = ZERO_I80F48(); expect(maxWithdraw(group, account)).equal(45); @@ -306,8 +307,10 @@ describe('maxWithdraw', () => { const [group, bank0, bank1, account] = setup(1000000); bank0.scaledInitAssetWeight = function (price) { const startScale = I80F48.fromNumber(50); - if (this.nativeDeposits().gt(startScale)) { - return this.initAssetWeight.div(this.nativeDeposits().div(startScale)); + if (this.nativeLendableDeposits().gt(startScale)) { + return this.initAssetWeight.div( + this.nativeLendableDeposits().div(startScale), + ); } return this.initAssetWeight; }; diff --git a/ts/client/src/accounts/mangoAccount.ts b/ts/client/src/accounts/mangoAccount.ts index f173c19bea..2b7b685ed8 100644 --- a/ts/client/src/accounts/mangoAccount.ts +++ b/ts/client/src/accounts/mangoAccount.ts @@ -571,7 +571,7 @@ export class MangoAccount { .mul(tokenBank.scaledInitLiabWeight(tokenBank.getLiabPrice())); let upperBound = existingTokenDeposits; if (allowLending) { - upperBound.iadd(initHealth.div(lowerBoundBorrowHealthFactor)); + upperBound = upperBound.add(initHealth.div(lowerBoundBorrowHealthFactor)); } // Step 2: Find the maximum withdraw amount @@ -669,13 +669,9 @@ export class MangoAccount { } // Step 5: also limit by vault funds - const vaultAmount = group.vaultAmountsMap.get(tokenBank.vault.toBase58()); - if (!vaultAmount) { - throw new Error( - `No vault amount found for ${tokenBank.name} vault ${tokenBank.vault}!`, - ); - } - const vaultLimit = I80F48.fromU64(vaultAmount); + const vaultLimit = I80F48.fromU64( + group.getTokenVaultWithdrawableByBank(tokenBank, allowLending), + ); return amount.min(vaultLimit).max(ZERO_I80F48()); } @@ -730,9 +726,10 @@ export class MangoAccount { ), ); const sourceBalance = this.getEffectiveTokenBalance(group, sourceBank); - const sourceAllowsLending = this.getToken(sourceBank.tokenIndex)?.allowLending() ?? true; + const sourceAllowsLending = + this.getToken(sourceBank.tokenIndex)?.allowLending() ?? true; const maxWithdrawNative = sourceBank.getMaxWithdraw( - group.getTokenVaultBalanceByMint(sourceBank.mint), + group.getTokenVaultWithdrawableByBank(sourceBank, sourceAllowsLending), sourceBalance, sourceAllowsLending, ); @@ -890,9 +887,10 @@ export class MangoAccount { let quoteAmount = nativeAmount.div(quoteBank.price); const quoteBalance = this.getEffectiveTokenBalance(group, quoteBank); - const quoteAllowsLending = this.getToken(quoteBank.tokenIndex)?.allowLending() ?? true; + const quoteAllowsLending = + this.getToken(quoteBank.tokenIndex)?.allowLending() ?? true; const maxWithdrawNative = quoteBank.getMaxWithdraw( - group.getTokenVaultBalanceByMint(quoteBank.mint), + group.getTokenVaultWithdrawableByBank(quoteBank, quoteAllowsLending), quoteBalance, quoteAllowsLending, ); @@ -947,9 +945,10 @@ export class MangoAccount { let baseAmount = nativeAmount.div(baseBank.price); const baseBalance = this.getEffectiveTokenBalance(group, baseBank); - const baseAllowsLending = this.getToken(baseBank.tokenIndex)?.allowLending() ?? true; + const baseAllowsLending = + this.getToken(baseBank.tokenIndex)?.allowLending() ?? true; const maxWithdrawNative = baseBank.getMaxWithdraw( - group.getTokenVaultBalanceByMint(baseBank.mint), + group.getTokenVaultWithdrawableByBank(baseBank, baseAllowsLending), baseBalance, baseAllowsLending, );