diff --git a/mango_v4.json b/mango_v4.json index cc44fefe8f..a9b5c71f9a 100644 --- a/mango_v4.json +++ b/mango_v4.json @@ -1981,6 +1981,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": [ @@ -7711,12 +7780,28 @@ ], "type": "f32" }, + { + "name": "padding2", + "type": { + "array": [ + "u8", + 4 + ] + } + }, + { + "name": "unlendableDeposits", + "docs": [ + "The sum of native tokens in unlendable token positions" + ], + "type": "u64" + }, { "name": "reserved", "type": { "array": [ "u8", - 1900 + 1888 ] } } @@ -9218,7 +9303,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" @@ -9227,7 +9313,7 @@ { "name": "tokenIndex", "docs": [ - "index into Group.tokens" + "index the token is registered with, same as in Bank and MintInfo" ], "type": "u16" }, @@ -9238,12 +9324,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 ] } }, @@ -9261,12 +9360,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 ] } } @@ -11090,6 +11198,12 @@ }, { "name": "HealthCheck" + }, + { + "name": "TokenCreatePosition" + }, + { + "name": "TokenClosePosition" } ] } @@ -11540,6 +11654,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": [ @@ -14447,6 +14606,36 @@ "code": 6072, "name": "InvalidHealth", "msg": "invalid health" + }, + { + "code": 6073, + "name": "PerpSettleTokenPositionMustSupportBorrows", + "msg": "perp settle token position does not support borrows" + }, + { + "code": 6074, + "name": "TokenPositionIsInUse", + "msg": "the token position is still in use by another position" + }, + { + "code": 6075, + "name": "TokenPositionBalanceNotZero", + "msg": "the token position has a non-zero balance" + }, + { + "code": 6076, + "name": "TokenPositionWithDifferentSettingAlreadyExists", + "msg": "cannot have a lending and no-lending token position at the same time" + }, + { + "code": 6077, + "name": "UnlendableTokenPositionCannotBeNegative", + "msg": "cannot borrow from unlendable token position" + }, + { + "code": 6078, + "name": "TokenConditionalSwapUnsupportedUnlendablePosition", + "msg": "token conditional swaps currently don't support unlendable positions" } ] } \ No newline at end of file diff --git a/programs/mango-v4/src/accounts_ix/mod.rs b/programs/mango-v4/src/accounts_ix/mod.rs index 4256824a8e..8db83e82ac 100644 --- a/programs/mango-v4/src/accounts_ix/mod.rs +++ b/programs/mango-v4/src/accounts_ix/mod.rs @@ -66,6 +66,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::*; @@ -146,6 +147,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 bac49d63c6..47b42857a2 100644 --- a/programs/mango-v4/src/error.rs +++ b/programs/mango-v4/src/error.rs @@ -151,6 +151,18 @@ pub enum MangoError { InvalidSequenceNumber, #[msg("invalid health")] InvalidHealth, + #[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, + #[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/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/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/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..e720ffba74 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; @@ -257,6 +258,7 @@ struct TokenVaultChange { bank_index: usize, raw_token_index: usize, amount: I80F48, + after_vault_balance: u64, } pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>( @@ -359,11 +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, + after_vault_balance: vault.amount, }); } @@ -487,6 +492,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.after_vault_balance, + bank.unlendable_deposits, + MangoError::InsufficentBankVaultFunds + ); + } + bank.flash_loan_approved_amount = 0; bank.flash_loan_token_account_initial = u64::MAX; @@ -502,14 +517,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/ix_gate_set.rs b/programs/mango-v4/src/instructions/ix_gate_set.rs index 8fdd0b8531..4cc8542671 100644 --- a/programs/mango-v4/src/instructions/ix_gate_set.rs +++ b/programs/mango-v4/src/instructions/ix_gate_set.rs @@ -98,6 +98,8 @@ pub fn ix_gate_set(ctx: Context, ix_gate: u128) -> Result<()> { log_if_changed(&group, ix_gate, IxGate::TokenForceWithdraw); log_if_changed(&group, ix_gate, IxGate::SequenceCheck); log_if_changed(&group, ix_gate, IxGate::HealthCheck); + log_if_changed(&group, ix_gate, IxGate::TokenCreatePosition); + log_if_changed(&group, ix_gate, IxGate::TokenClosePosition); group.ix_gate = ix_gate; diff --git a/programs/mango-v4/src/instructions/mod.rs b/programs/mango-v4/src/instructions/mod.rs index 1f91a7b53a..e922688f1c 100644 --- a/programs/mango-v4/src/instructions/mod.rs +++ b/programs/mango-v4/src/instructions/mod.rs @@ -53,10 +53,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::*; @@ -124,10 +126,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/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 80d43fb18a..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, }; @@ -331,6 +332,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. { @@ -530,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 @@ -539,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 { @@ -560,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_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_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_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_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/instructions/token_deposit.rs b/programs/mango-v4/src/instructions/token_deposit.rs index 1155cd7d9b..51de125309 100644 --- a/programs/mango-v4/src/instructions/token_deposit.rs +++ b/programs/mango-v4/src/instructions/token_deposit.rs @@ -83,12 +83,11 @@ 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::())?; - 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())?; @@ -99,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()?; } @@ -107,14 +106,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 70eade859b..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()?; @@ -36,11 +37,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 ) }); } @@ -60,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..a6a0d395eb 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() { @@ -239,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 @@ -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..96a482d264 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::*; @@ -226,8 +226,16 @@ 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 + // 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); let asset_liquidation_fee = asset_transfer_from_liqee - asset_transfer_to_liqor; asset_bank.collected_fees_native += asset_liquidation_fee; @@ -241,20 +249,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 +270,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 @@ -273,47 +281,10 @@ 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::(), ); - // 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() @@ -345,6 +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.to_num::() + ); liqee .fixed .maybe_recover_from_being_liquidated(liqee_liq_end_health); 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 7f32940f50..97c3388458 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 @@ -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 ) }); } @@ -107,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/lib.rs b/programs/mango-v4/src/lib.rs index 533434b148..747851e35b 100644 --- a/programs/mango-v4/src/lib.rs +++ b/programs/mango-v4/src/lib.rs @@ -508,6 +508,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/logs.rs b/programs/mango-v4/src/logs.rs index 9032b3bc68..38aeba492e 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,20 @@ 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(TokenBalanceLogV2 { + mango_group: bank.group, + mango_account, + 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(), + unlendable_deposits: token_position.unlendable_deposits, + allow_lending: token_position.allow_lending(), + }); +} + #[event] pub struct TokenBalanceLog { pub mango_group: Pubkey, @@ -63,6 +77,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 unlendable_deposits: u64, + pub allow_lending: bool, +} + #[derive(AnchorSerialize, AnchorDeserialize)] pub struct FlashLoanTokenDetail { pub token_index: u16, diff --git a/programs/mango-v4/src/state/bank.rs b/programs/mango-v4/src/state/bank.rs index cfc6aea3d0..fdc762b31e 100644 --- a/programs/mango-v4/src/state/bank.rs +++ b/programs/mango-v4/src/state/bank.rs @@ -232,7 +232,13 @@ pub struct Bank { pub collateral_fee_per_day: f32, #[derivative(Debug = "ignore")] - pub reserved: [u8; 1900], + pub padding2: [u8; 4], + + /// The sum of native tokens in unlendable token positions + pub unlendable_deposits: u64, + + #[derivative(Debug = "ignore")] + pub reserved: [u8; 1888], } const_assert_eq!( size_of::(), @@ -271,7 +277,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 +330,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 +391,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], } } @@ -461,15 +471,25 @@ impl Bank { } #[inline(always)] - pub fn native_borrows(&self) -> I80F48 { + pub fn borrows(&self) -> I80F48 { self.borrow_index * self.indexed_borrows } #[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 { @@ -505,14 +525,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, ) }); }; @@ -534,7 +554,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 +567,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 +577,23 @@ 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() { + 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); + Ok(result) + } else { + assert!(position.indexed_position.is_zero()); + let deposit_amount = native_amount.floor(); + self.dust += native_amount - deposit_amount; + + let deposit_amount_u64 = deposit_amount.to_num::(); + position.unlendable_deposits += deposit_amount_u64; + self.unlendable_deposits += deposit_amount_u64; + + Ok(true) + } } /// Internal function to deposit funds @@ -572,6 +605,7 @@ impl Bank { now_ts: u64, ) -> Result { require_gte!(native_amount, 0); + assert!(position.allow_lending()); let native_position = position.native(self); @@ -646,7 +680,7 @@ impl Bank { position, native_amount, false, - !position.is_in_use(), + position.can_auto_close(), now_ts, )? .position_is_active; @@ -664,7 +698,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 +715,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 +733,40 @@ 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() { + assert!(position.unlendable_deposits == 0); + 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 { + assert!(position.indexed_position.is_zero()); + + // 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; + + let withdraw_amount_u64 = withdraw_amount.to_num::(); + 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, + loan_origination_fee: I80F48::ZERO, + }) + } } /// Internal function to withdraw funds @@ -789,7 +853,7 @@ impl Bank { position, loan_origination_fee, false, - !position.is_in_use(), + position.can_auto_close(), now_ts, )? .position_is_active; @@ -804,7 +868,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); @@ -881,7 +945,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 @@ -974,7 +1038,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; @@ -998,6 +1062,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) @@ -1242,8 +1310,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 @@ -1259,7 +1326,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 { @@ -1340,11 +1407,13 @@ 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, + unlendable_deposits: 0, padding: Default::default(), - reserved: [0; 128], + reserved: [0; 120], }; account.indexed_position = indexed(I80F48::from_num(start), &bank); @@ -1467,11 +1536,13 @@ 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, + unlendable_deposits: 0, padding: Default::default(), - reserved: [0; 128], + reserved: [0; 120], }; // diff --git a/programs/mango-v4/src/state/group.rs b/programs/mango-v4/src/state/group.rs index 61f61cde2e..cabab7b89b 100644 --- a/programs/mango-v4/src/state/group.rs +++ b/programs/mango-v4/src/state/group.rs @@ -248,6 +248,8 @@ pub enum IxGate { TokenForceWithdraw = 72, SequenceCheck = 73, HealthCheck = 74, + TokenCreatePosition = 75, + TokenClosePosition = 76, // NOTE: Adding new variants requires matching changes in ts and the ix_gate_set instruction. } diff --git a/programs/mango-v4/src/state/mango_account.rs b/programs/mango-v4/src/state/mango_account.rs index c146b23239..21cd35ce4a 100644 --- a/programs/mango-v4/src/state/mango_account.rs +++ b/programs/mango-v4/src/state/mango_account.rs @@ -1011,11 +1011,13 @@ 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, + unlendable_deposits: 0, padding: Default::default(), - reserved: [0; 128], + reserved: [0; 120], }; } Ok((v, raw_index, bank_index)) @@ -1160,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 and 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(); } } @@ -1392,7 +1401,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) } @@ -1424,7 +1433,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/src/state/mango_account_components.rs b/programs/mango-v4/src/state/mango_account_components.rs index 987f0e8a7d..de635be3b6 100644 --- a/programs/mango-v4/src/state/mango_account_components.rs +++ b/programs/mango-v4/src/state/mango_account_components.rs @@ -15,22 +15,27 @@ 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 pub in_use_count: u16, + /// 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 + 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 @@ -42,13 +47,18 @@ 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")] - pub reserved: [u8; 128], + pub reserved: [u8; 120], } const_assert_eq!( size_of::(), - 16 + 2 + 2 + 4 + 16 + 8 + 8 + 128 + 16 + 2 + 2 + 1 + 3 + 16 + 8 + 8 + 8 + 120 ); const_assert_eq!(size_of::(), 184); const_assert_eq!(size_of::() % 8, 0); @@ -59,11 +69,13 @@ 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, + unlendable_deposits: 0, padding: Default::default(), - reserved: [0; 128], + reserved: [0; 120], } } } @@ -78,28 +90,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 + I80F48::from(self.unlendable_deposits) } } #[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 +123,14 @@ 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 + } + + pub fn has_deposits(&self) -> bool { + self.indexed_position > 0 || self.unlendable_deposits > 0 + } } #[zero_copy] diff --git a/programs/mango-v4/tests/cases/mod.rs b/programs/mango-v4/tests/cases/mod.rs index bf15b1ac0a..3b0f6c961a 100644 --- a/programs/mango-v4/tests/cases/mod.rs +++ b/programs/mango-v4/tests/cases/mod.rs @@ -42,3 +42,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_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 c2ba99091f..1bb0b11afb 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_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; 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, 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 ); 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..560a394509 --- /dev/null +++ b/programs/mango-v4/tests/cases/test_unlendable.rs @@ -0,0 +1,377 @@ +use super::*; + +#[tokio::test] +async fn test_unlendable_basic() -> 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(()) +} + +#[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(()) +} diff --git a/programs/mango-v4/tests/program_test/mango_client.rs b/programs/mango-v4/tests/program_test/mango_client.rs index 2efa8967a4..783179f171 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, 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 d4c8c44371..3c9226b485 100644 --- a/ts/client/src/accounts/bank.ts +++ b/ts/client/src/accounts/bank.ts @@ -151,6 +151,7 @@ export class Bank implements BankForHealth { collectedLiquidationFees: I80F48Dto; collectedCollateralFees: I80F48Dto; collateralFeePerDay: number; + unlendableDeposits: BN; }, ): Bank { return new Bank( @@ -219,6 +220,7 @@ export class Bank implements BankForHealth { obj.collectedCollateralFees, obj.collateralFeePerDay, obj.forceWithdraw == 1, + obj.unlendableDeposits, ); } @@ -288,6 +290,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 = { @@ -514,10 +517,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); } @@ -536,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( @@ -588,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); } @@ -622,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/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 ddb25a9f26..443a608b78 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), + 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), + 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() { @@ -176,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; } @@ -263,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); @@ -288,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 7d0c16b45f..2b7b685ed8 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 = upperBound.add(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(), @@ -654,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()); } @@ -715,9 +726,12 @@ 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), + group.getTokenVaultWithdrawableByBank(sourceBank, sourceAllowsLending), sourceBalance, + sourceAllowsLending, ); maxSource = maxSource.min(maxWithdrawNative); @@ -873,9 +887,12 @@ 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), + group.getTokenVaultWithdrawableByBank(quoteBank, quoteAllowsLending), quoteBalance, + quoteAllowsLending, ); quoteAmount = quoteAmount.min(maxWithdrawNative); @@ -928,9 +945,12 @@ 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), + group.getTokenVaultWithdrawableByBank(baseBank, baseAllowsLending), baseBalance, + baseAllowsLending, ); baseAmount = baseAmount.min(maxWithdrawNative); @@ -1270,6 +1290,8 @@ export class TokenPosition { I80F48.from(dto.previousIndex), dto.cumulativeDepositInterest, dto.cumulativeBorrowInterest, + dto.disableLending != 0, + dto.unlendableDeposits, ); } @@ -1280,22 +1302,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); } } @@ -1317,10 +1349,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(); } /** @@ -1381,6 +1413,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/mango_v4.ts b/ts/client/src/mango_v4.ts index f294f383ba..a5ae8796f8 100644 --- a/ts/client/src/mango_v4.ts +++ b/ts/client/src/mango_v4.ts @@ -1981,6 +1981,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": [ @@ -7711,12 +7780,28 @@ export type MangoV4 = { ], "type": "f32" }, + { + "name": "padding2", + "type": { + "array": [ + "u8", + 4 + ] + } + }, + { + "name": "unlendableDeposits", + "docs": [ + "The sum of native tokens in unlendable token positions" + ], + "type": "u64" + }, { "name": "reserved", "type": { "array": [ "u8", - 1900 + 1888 ] } } @@ -9218,7 +9303,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" @@ -9227,7 +9313,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" }, @@ -9238,12 +9324,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 ] } }, @@ -9261,12 +9360,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 ] } } @@ -11090,6 +11198,12 @@ export type MangoV4 = { }, { "name": "HealthCheck" + }, + { + "name": "TokenCreatePosition" + }, + { + "name": "TokenClosePosition" } ] } @@ -11540,6 +11654,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": [ @@ -14447,6 +14606,36 @@ export type MangoV4 = { "code": 6072, "name": "InvalidHealth", "msg": "invalid health" + }, + { + "code": 6073, + "name": "PerpSettleTokenPositionMustSupportBorrows", + "msg": "perp settle token position does not support borrows" + }, + { + "code": 6074, + "name": "TokenPositionIsInUse", + "msg": "the token position is still in use by another position" + }, + { + "code": 6075, + "name": "TokenPositionBalanceNotZero", + "msg": "the token position has a non-zero balance" + }, + { + "code": 6076, + "name": "TokenPositionWithDifferentSettingAlreadyExists", + "msg": "cannot have a lending and no-lending token position at the same time" + }, + { + "code": 6077, + "name": "UnlendableTokenPositionCannotBeNegative", + "msg": "cannot borrow from unlendable token position" + }, + { + "code": 6078, + "name": "TokenConditionalSwapUnsupportedUnlendablePosition", + "msg": "token conditional swaps currently don't support unlendable positions" } ] }; @@ -16434,6 +16623,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": [ @@ -22164,12 +22422,28 @@ export const IDL: MangoV4 = { ], "type": "f32" }, + { + "name": "padding2", + "type": { + "array": [ + "u8", + 4 + ] + } + }, + { + "name": "unlendableDeposits", + "docs": [ + "The sum of native tokens in unlendable token positions" + ], + "type": "u64" + }, { "name": "reserved", "type": { "array": [ "u8", - 1900 + 1888 ] } } @@ -23671,7 +23945,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" @@ -23680,7 +23955,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" }, @@ -23691,12 +23966,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 ] } }, @@ -23714,12 +24002,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 ] } } @@ -25543,6 +25840,12 @@ export const IDL: MangoV4 = { }, { "name": "HealthCheck" + }, + { + "name": "TokenCreatePosition" + }, + { + "name": "TokenClosePosition" } ] } @@ -25993,6 +26296,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": [ @@ -28900,6 +29248,36 @@ export const IDL: MangoV4 = { "code": 6072, "name": "InvalidHealth", "msg": "invalid health" + }, + { + "code": 6073, + "name": "PerpSettleTokenPositionMustSupportBorrows", + "msg": "perp settle token position does not support borrows" + }, + { + "code": 6074, + "name": "TokenPositionIsInUse", + "msg": "the token position is still in use by another position" + }, + { + "code": 6075, + "name": "TokenPositionBalanceNotZero", + "msg": "the token position has a non-zero balance" + }, + { + "code": 6076, + "name": "TokenPositionWithDifferentSettingAlreadyExists", + "msg": "cannot have a lending and no-lending token position at the same time" + }, + { + "code": 6077, + "name": "UnlendableTokenPositionCannotBeNegative", + "msg": "cannot borrow from unlendable token position" + }, + { + "code": 6078, + "name": "TokenConditionalSwapUnsupportedUnlendablePosition", + "msg": "token conditional swaps currently don't support unlendable positions" } ] }; diff --git a/ts/client/src/numbers/I80F48.ts b/ts/client/src/numbers/I80F48.ts index d683ae6a5f..30c260913b 100644 --- a/ts/client/src/numbers/I80F48.ts +++ b/ts/client/src/numbers/I80F48.ts @@ -36,6 +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. + */ constructor(data: BN) { if (data.lt(I80F48.MIN_BN) || data.gt(I80F48.MAX_BN)) { throw new Error('Number out of range'); @@ -87,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); }