From 704dfcaa27ae9efb79f587ba8b5d586e99883e16 Mon Sep 17 00:00:00 2001 From: Nicholas Clarke Date: Fri, 19 Aug 2022 18:50:54 -0700 Subject: [PATCH] Clarkeni/loan fee logging (#180) Logging for loan origination fees and token bankruptcy --- anchor | 2 +- .../src/instructions/liq_token_bankruptcy.rs | 75 ++++- .../src/instructions/liq_token_with_token.rs | 299 +++++++++--------- .../serum3_liq_force_cancel_orders.rs | 44 ++- .../src/instructions/serum3_place_order.rs | 48 ++- .../src/instructions/serum3_settle_funds.rs | 41 ++- .../token_update_index_and_rate.rs | 4 +- .../src/instructions/token_withdraw.rs | 21 +- programs/mango-v4/src/logs.rs | 38 ++- programs/mango-v4/src/state/bank.rs | 36 ++- programs/mango-v4/tests/program_test/mod.rs | 2 +- ts/client/src/mango_v4.ts | 252 +++++++++++++++ 12 files changed, 663 insertions(+), 199 deletions(-) diff --git a/anchor b/anchor index b52f23614..9e546f52d 160000 --- a/anchor +++ b/anchor @@ -1 +1 @@ -Subproject commit b52f23614601652a99ec6c27aec77bd327363b31 +Subproject commit 9e546f52d967e95fea4ae105f4bc7bf3720b9464 diff --git a/programs/mango-v4/src/instructions/liq_token_bankruptcy.rs b/programs/mango-v4/src/instructions/liq_token_bankruptcy.rs index b620268d2..c65282b34 100644 --- a/programs/mango-v4/src/instructions/liq_token_bankruptcy.rs +++ b/programs/mango-v4/src/instructions/liq_token_bankruptcy.rs @@ -10,6 +10,11 @@ use crate::state::ScanningAccountRetriever; use crate::state::*; use crate::util::checked_math as cm; +use crate::logs::{ + LiquidateTokenBankruptcyLog, LoanOriginationFeeInstruction, TokenBalanceLog, + WithdrawLoanOriginationFeeLog, +}; + // Remaining accounts: // - all banks for liab_mint_info (writable) // - merged health accounts for liqor+liqee @@ -98,7 +103,8 @@ pub fn liq_token_bankruptcy( let (liab_bank, liab_price, opt_quote_bank_and_price) = account_retriever.banks_mut_and_oracles(liab_token_index, QUOTE_TOKEN_INDEX)?; - let liab_deposit_index = liab_bank.deposit_index; + let mut liab_deposit_index = liab_bank.deposit_index; + let liab_borrow_index = liab_bank.borrow_index; let (liqee_liab, liqee_raw_token_index) = liqee.token_position_mut(liab_token_index)?; let initial_liab_native = liqee_liab.native(&liab_bank); let mut remaining_liab_loss = -initial_liab_native; @@ -151,26 +157,52 @@ pub fn liq_token_bankruptcy( )?; // move quote assets into liqor and withdraw liab assets - if let Some((quote_bank, _)) = opt_quote_bank_and_price { + if let Some((quote_bank, quote_price)) = opt_quote_bank_and_price { // account constraint #2 a) 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(QUOTE_TOKEN_INDEX)?; let liqor_quote_active = quote_bank.deposit(liqor_quote, insurance_transfer_i80f48)?; + let liqor_quote_indexed_position = liqor_quote.indexed_position; // transfer liab from liqee to liqor let (liqor_liab, liqor_liab_raw_token_index, _) = liqor.ensure_token_position(liab_token_index)?; - let liqor_liab_active = liab_bank.withdraw_with_fee(liqor_liab, liab_transfer)?; + let (liqor_liab_active, loan_origination_fee) = + liab_bank.withdraw_with_fee(liqor_liab, liab_transfer)?; // Check liqor's health let liqor_health = compute_health(&liqor.borrow(), HealthType::Init, &account_retriever)?; require!(liqor_health >= 0, MangoError::HealthMustBePositive); + // liqor quote + emit!(TokenBalanceLog { + mango_group: ctx.accounts.group.key(), + mango_account: ctx.accounts.liqor.key(), + token_index: QUOTE_TOKEN_INDEX, + indexed_position: liqor_quote_indexed_position.to_bits(), + deposit_index: quote_deposit_index.to_bits(), + borrow_index: quote_borrow_index.to_bits(), + price: quote_price.to_bits(), + }); + + if loan_origination_fee.is_positive() { + emit!(WithdrawLoanOriginationFeeLog { + mango_group: ctx.accounts.group.key(), + mango_account: ctx.accounts.liqor.key(), + token_index: liab_token_index, + loan_origination_fee: loan_origination_fee.to_bits(), + instruction: LoanOriginationFeeInstruction::LiqTokenBankruptcy + }); + } + if !liqor_quote_active { liqor.deactivate_token_position(liqor_quote_raw_token_index); } @@ -191,6 +223,7 @@ pub fn liq_token_bankruptcy( // Socialize loss if there's more loss and noone else could use the // insurance fund to cover it. + let mut socialized_loss = I80F48::ZERO; if insurance_fund_exhausted && remaining_liab_loss.is_positive() { // find the total deposits let mut indexed_total_deposits = I80F48::ZERO; @@ -205,6 +238,8 @@ pub fn liq_token_bankruptcy( // Probably not. let new_deposit_index = cm!(liab_deposit_index - remaining_liab_loss / indexed_total_deposits); + liab_deposit_index = new_deposit_index; + socialized_loss = remaining_liab_loss; let mut amount_to_credit = remaining_liab_loss; for bank_ai in bank_ais.iter() { @@ -228,6 +263,28 @@ pub fn liq_token_bankruptcy( require_eq!(liqee_liab.indexed_position, I80F48::ZERO); } + // liqor liab + emit!(TokenBalanceLog { + mango_group: ctx.accounts.group.key(), + mango_account: ctx.accounts.liqor.key(), + token_index: liab_token_index, + indexed_position: liqee_liab.indexed_position.to_bits(), + deposit_index: liab_deposit_index.to_bits(), + borrow_index: liab_borrow_index.to_bits(), + price: liab_price.to_bits(), + }); + + // liqee liab + emit!(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(), + price: liab_price.to_bits(), + }); + let liab_bank = bank_ais[0].load::()?; let end_liab_native = liqee_liab.native(&liab_bank); liqee_health_cache @@ -243,5 +300,17 @@ pub fn liq_token_bankruptcy( liqee.deactivate_token_position(liqee_raw_token_index); } + emit!(LiquidateTokenBankruptcyLog { + mango_group: ctx.accounts.group.key(), + liqee: ctx.accounts.liqee.key(), + liqor: ctx.accounts.liqor.key(), + liab_token_index: liab_token_index, + initial_liab_native: initial_liab_native.to_bits(), + liab_price: liab_price.to_bits(), + insurance_token_index: QUOTE_TOKEN_INDEX, + insurance_transfer: insurance_transfer_i80f48.to_bits(), + socialized_loss: socialized_loss.to_bits() + }); + Ok(()) } diff --git a/programs/mango-v4/src/instructions/liq_token_with_token.rs b/programs/mango-v4/src/instructions/liq_token_with_token.rs index f34f8403d..46fa2d59b 100644 --- a/programs/mango-v4/src/instructions/liq_token_with_token.rs +++ b/programs/mango-v4/src/instructions/liq_token_with_token.rs @@ -3,7 +3,10 @@ use fixed::types::I80F48; use std::cmp::min; use crate::error::*; -use crate::logs::{LiquidateTokenAndTokenLog, TokenBalanceLog}; +use crate::logs::{ + LiquidateTokenAndTokenLog, LoanOriginationFeeInstruction, TokenBalanceLog, + WithdrawLoanOriginationFeeLog, +}; use crate::state::ScanningAccountRetriever; use crate::state::*; use crate::util::checked_math as cm; @@ -77,171 +80,168 @@ pub fn liq_token_with_token( // Transfer some liab_token from liqor to liqee and // transfer some asset_token from liqee to liqor. // - { - // Get the mut banks and oracle prices - // - // This must happen _after_ the health computation, since immutable borrows of - // the bank are not allowed at the same time. - let (asset_bank, asset_price, opt_liab_bank_and_price) = - account_retriever.banks_mut_and_oracles(asset_token_index, liab_token_index)?; - let (liab_bank, liab_price) = opt_liab_bank_and_price.unwrap(); - // The main complication here is that we can't keep the liqee_asset_position and liqee_liab_position - // borrows alive at the same time. Possibly adding get_mut_pair() would be helpful. - let (liqee_asset_position, liqee_asset_raw_index) = - liqee.token_position_and_raw_index(asset_token_index)?; - let liqee_asset_native = liqee_asset_position.native(asset_bank); - require!(liqee_asset_native.is_positive(), MangoError::SomeError); + // Get the mut banks and oracle prices + // + // This must happen _after_ the health computation, since immutable borrows of + // the bank are not allowed at the same time. + let (asset_bank, asset_price, opt_liab_bank_and_price) = + account_retriever.banks_mut_and_oracles(asset_token_index, liab_token_index)?; + let (liab_bank, liab_price) = opt_liab_bank_and_price.unwrap(); - let (liqee_liab_position, liqee_liab_raw_index) = - liqee.token_position_and_raw_index(liab_token_index)?; - let liqee_liab_native = liqee_liab_position.native(liab_bank); - require!(liqee_liab_native.is_negative(), MangoError::SomeError); + // The main complication here is that we can't keep the liqee_asset_position and liqee_liab_position + // borrows alive at the same time. Possibly adding get_mut_pair() would be helpful. + let (liqee_asset_position, liqee_asset_raw_index) = + liqee.token_position_and_raw_index(asset_token_index)?; + let liqee_asset_native = liqee_asset_position.native(asset_bank); + require!(liqee_asset_native.is_positive(), MangoError::SomeError); - // TODO why sum of both tokens liquidation fees? Add comment - let fee_factor = I80F48::ONE + asset_bank.liquidation_fee + liab_bank.liquidation_fee; - let liab_price_adjusted = liab_price * fee_factor; + let (liqee_liab_position, liqee_liab_raw_index) = + liqee.token_position_and_raw_index(liab_token_index)?; + let liqee_liab_native = liqee_liab_position.native(liab_bank); + require!(liqee_liab_native.is_negative(), MangoError::SomeError); - let init_asset_weight = asset_bank.init_asset_weight; - let init_liab_weight = liab_bank.init_liab_weight; + // TODO why sum of both tokens liquidation fees? Add comment + let fee_factor = I80F48::ONE + asset_bank.liquidation_fee + liab_bank.liquidation_fee; + let liab_price_adjusted = liab_price * fee_factor; - // How much asset would need to be exchanged to liab in order to bring health to 0? - // - // That means: what is x (unit: native liab tokens) such that - // init_health + x * ilw * lp - y * iaw * ap = 0 - // where - // ilw = init_liab_weight, lp = liab_price - // iap = init_asset_weight, ap = asset_price - // ff = fee_factor, lpa = lp * ff - // and the asset cost of getting x native units of liab is: - // y = x * lp / ap * ff = x * lpa / ap (native asset tokens) - // - // Result: x = -init_health / (lp * ilw - iaw * lpa) - let liab_needed = cm!(-init_health + let init_asset_weight = asset_bank.init_asset_weight; + let init_liab_weight = liab_bank.init_liab_weight; + + // How much asset would need to be exchanged to liab in order to bring health to 0? + // + // That means: what is x (unit: native liab tokens) such that + // init_health + x * ilw * lp - y * iaw * ap = 0 + // where + // ilw = init_liab_weight, lp = liab_price + // iap = init_asset_weight, ap = asset_price + // ff = fee_factor, lpa = lp * ff + // and the asset cost of getting x native units of liab is: + // y = x * lp / ap * ff = x * lpa / ap (native asset tokens) + // + // Result: x = -init_health / (lp * ilw - iaw * lpa) + let liab_needed = + cm!(-init_health / (liab_price * init_liab_weight - init_asset_weight * liab_price_adjusted)); - // How much liab can we get at most for the asset balance? - let liab_possible = cm!(liqee_asset_native * asset_price / liab_price_adjusted); + // How much liab can we get at most for the asset balance? + let liab_possible = cm!(liqee_asset_native * asset_price / liab_price_adjusted); - // The amount of liab native tokens we will transfer - let liab_transfer = min( - min(min(liab_needed, -liqee_liab_native), liab_possible), - max_liab_transfer, - ); + // The amount of liab native tokens we will transfer + let liab_transfer = min( + min(min(liab_needed, -liqee_liab_native), liab_possible), + max_liab_transfer, + ); - // The amount of asset native tokens we will give up for them - let asset_transfer = cm!(liab_transfer * liab_price_adjusted / asset_price); + // The amount of asset native tokens we will give up for them + let asset_transfer = cm!(liab_transfer * liab_price_adjusted / asset_price); - // During liquidation, we mustn't leave small positive balances in the liqee. Those - // could break bankruptcy-detection. Thus we dust them even if the token position - // is nominally in-use. + // During liquidation, we mustn't leave small positive balances in the liqee. Those + // could break bankruptcy-detection. Thus we dust them even if the token position + // is nominally in-use. - // Apply the balance changes to the liqor and liqee accounts - let liqee_liab_position = liqee.token_position_mut_by_raw_index(liqee_liab_raw_index); - let liqee_liab_active = - liab_bank.deposit_with_dusting(liqee_liab_position, liab_transfer)?; - let liqee_liab_position_indexed = liqee_liab_position.indexed_position; + // Apply the balance changes to the liqor and liqee accounts + let liqee_liab_position = liqee.token_position_mut_by_raw_index(liqee_liab_raw_index); + let liqee_liab_active = liab_bank.deposit_with_dusting(liqee_liab_position, liab_transfer)?; + let liqee_liab_position_indexed = liqee_liab_position.indexed_position; - let (liqor_liab_position, liqor_liab_raw_index, _) = - liqor.ensure_token_position(liab_token_index)?; - let liqor_liab_active = liab_bank.withdraw_with_fee(liqor_liab_position, liab_transfer)?; - let liqor_liab_position_indexed = liqor_liab_position.indexed_position; - let liqee_liab_native_after = liqee_liab_position.native(&liab_bank); + let (liqor_liab_position, liqor_liab_raw_index, _) = + liqor.ensure_token_position(liab_token_index)?; + let (liqor_liab_active, loan_origination_fee) = + liab_bank.withdraw_with_fee(liqor_liab_position, liab_transfer)?; + let liqor_liab_position_indexed = liqor_liab_position.indexed_position; + let liqee_liab_native_after = liqee_liab_position.native(&liab_bank); - let (liqor_asset_position, liqor_asset_raw_index, _) = - liqor.ensure_token_position(asset_token_index)?; - let liqor_asset_active = asset_bank.deposit(liqor_asset_position, asset_transfer)?; - let liqor_asset_position_indexed = liqor_asset_position.indexed_position; + let (liqor_asset_position, liqor_asset_raw_index, _) = + liqor.ensure_token_position(asset_token_index)?; + let liqor_asset_active = asset_bank.deposit(liqor_asset_position, asset_transfer)?; + let liqor_asset_position_indexed = liqor_asset_position.indexed_position; - let liqee_asset_position = liqee.token_position_mut_by_raw_index(liqee_asset_raw_index); - let liqee_asset_active = - asset_bank.withdraw_without_fee_with_dusting(liqee_asset_position, asset_transfer)?; - let liqee_asset_position_indexed = liqee_asset_position.indexed_position; - let liqee_assets_native_after = liqee_asset_position.native(&asset_bank); + let liqee_asset_position = liqee.token_position_mut_by_raw_index(liqee_asset_raw_index); + let liqee_asset_active = + asset_bank.withdraw_without_fee_with_dusting(liqee_asset_position, asset_transfer)?; + let liqee_asset_position_indexed = liqee_asset_position.indexed_position; + let liqee_assets_native_after = liqee_asset_position.native(&asset_bank); - // Update the health cache - liqee_health_cache.adjust_token_balance( - liab_token_index, - cm!(liqee_liab_native_after - liqee_liab_native), - )?; - liqee_health_cache.adjust_token_balance( - asset_token_index, - cm!(liqee_assets_native_after - liqee_asset_native), - )?; + // Update the health cache + liqee_health_cache.adjust_token_balance( + liab_token_index, + cm!(liqee_liab_native_after - liqee_liab_native), + )?; + liqee_health_cache.adjust_token_balance( + asset_token_index, + cm!(liqee_assets_native_after - liqee_asset_native), + )?; - msg!( - "liquidated {} liab for {} asset", - liab_transfer, - asset_transfer - ); + msg!( + "liquidated {} liab for {} asset", + liab_transfer, + asset_transfer + ); - emit!(LiquidateTokenAndTokenLog { - mango_group: ctx.accounts.group.key(), - liqee: ctx.accounts.liqee.key(), - liqor: ctx.accounts.liqor.key(), - asset_token_index, - liab_token_index, - asset_transfer: asset_transfer.to_bits(), - liab_transfer: liab_transfer.to_bits(), - asset_price: asset_price.to_bits(), - liab_price: liab_price.to_bits(), - // bankruptcy: - }); + // liqee asset + emit!(TokenBalanceLog { + mango_group: ctx.accounts.group.key(), + mango_account: ctx.accounts.liqee.key(), + token_index: asset_token_index, + indexed_position: liqee_asset_position_indexed.to_bits(), + deposit_index: asset_bank.deposit_index.to_bits(), + borrow_index: asset_bank.borrow_index.to_bits(), + price: asset_price.to_bits(), + }); + // liqee liab + emit!(TokenBalanceLog { + mango_group: ctx.accounts.group.key(), + mango_account: ctx.accounts.liqee.key(), + token_index: liab_token_index, + indexed_position: liqee_liab_position_indexed.to_bits(), + deposit_index: liab_bank.deposit_index.to_bits(), + borrow_index: liab_bank.borrow_index.to_bits(), + price: liab_price.to_bits(), + }); + // liqor asset + emit!(TokenBalanceLog { + mango_group: ctx.accounts.group.key(), + mango_account: ctx.accounts.liqor.key(), + token_index: asset_token_index, + indexed_position: liqor_asset_position_indexed.to_bits(), + deposit_index: asset_bank.deposit_index.to_bits(), + borrow_index: asset_bank.borrow_index.to_bits(), + price: asset_price.to_bits(), + }); + // liqor liab + emit!(TokenBalanceLog { + mango_group: ctx.accounts.group.key(), + mango_account: ctx.accounts.liqor.key(), + token_index: liab_token_index, + indexed_position: liqor_liab_position_indexed.to_bits(), + deposit_index: liab_bank.deposit_index.to_bits(), + borrow_index: liab_bank.borrow_index.to_bits(), + price: liab_price.to_bits(), + }); - // liqee asset - emit!(TokenBalanceLog { - mango_group: ctx.accounts.group.key(), - mango_account: ctx.accounts.liqee.key(), - token_index: asset_token_index, - indexed_position: liqee_asset_position_indexed.to_bits(), - deposit_index: asset_bank.deposit_index.to_bits(), - borrow_index: asset_bank.borrow_index.to_bits(), - price: asset_price.to_bits(), - }); - // liqee liab - emit!(TokenBalanceLog { - mango_group: ctx.accounts.group.key(), - mango_account: ctx.accounts.liqee.key(), - token_index: liab_token_index, - indexed_position: liqee_liab_position_indexed.to_bits(), - deposit_index: liab_bank.deposit_index.to_bits(), - borrow_index: liab_bank.borrow_index.to_bits(), - price: liab_price.to_bits(), - }); - // liqor asset - emit!(TokenBalanceLog { - mango_group: ctx.accounts.group.key(), - mango_account: ctx.accounts.liqor.key(), - token_index: asset_token_index, - indexed_position: liqor_asset_position_indexed.to_bits(), - deposit_index: asset_bank.deposit_index.to_bits(), - borrow_index: asset_bank.borrow_index.to_bits(), - price: asset_price.to_bits(), - }); - // liqor liab - emit!(TokenBalanceLog { + if loan_origination_fee.is_positive() { + emit!(WithdrawLoanOriginationFeeLog { mango_group: ctx.accounts.group.key(), mango_account: ctx.accounts.liqor.key(), token_index: liab_token_index, - indexed_position: liqor_liab_position_indexed.to_bits(), - deposit_index: liab_bank.deposit_index.to_bits(), - borrow_index: liab_bank.borrow_index.to_bits(), - price: liab_price.to_bits(), + loan_origination_fee: loan_origination_fee.to_bits(), + instruction: LoanOriginationFeeInstruction::LiqTokenWithToken }); + } - // Since we use a scanning account retriever, it's safe to deactivate inactive token positions - if !liqee_asset_active { - liqee.deactivate_token_position(liqee_asset_raw_index); - } - if !liqee_liab_active { - liqee.deactivate_token_position(liqee_liab_raw_index); - } - if !liqor_asset_active { - liqor.deactivate_token_position(liqor_asset_raw_index); - } - if !liqor_liab_active { - liqor.deactivate_token_position(liqor_liab_raw_index) - } + // Since we use a scanning account retriever, it's safe to deactivate inactive token positions + if !liqee_asset_active { + liqee.deactivate_token_position(liqee_asset_raw_index); + } + if !liqee_liab_active { + liqee.deactivate_token_position(liqee_liab_raw_index); + } + if !liqor_asset_active { + liqor.deactivate_token_position(liqor_asset_raw_index); + } + if !liqor_liab_active { + liqor.deactivate_token_position(liqor_liab_raw_index) } // Check liqee health again @@ -255,5 +255,18 @@ pub fn liq_token_with_token( .context("compute liqor health")?; require!(liqor_health >= 0, MangoError::HealthMustBePositive); + emit!(LiquidateTokenAndTokenLog { + mango_group: ctx.accounts.group.key(), + liqee: ctx.accounts.liqee.key(), + liqor: ctx.accounts.liqor.key(), + asset_token_index, + liab_token_index, + asset_transfer: asset_transfer.to_bits(), + liab_transfer: liab_transfer.to_bits(), + asset_price: asset_price.to_bits(), + liab_price: liab_price.to_bits(), + bankruptcy: !liqee_health_cache.has_liquidatable_assets() & liqee_init_health.is_negative() + }); + Ok(()) } diff --git a/programs/mango-v4/src/instructions/serum3_liq_force_cancel_orders.rs b/programs/mango-v4/src/instructions/serum3_liq_force_cancel_orders.rs index 9717d77e3..165f85ada 100644 --- a/programs/mango-v4/src/instructions/serum3_liq_force_cancel_orders.rs +++ b/programs/mango-v4/src/instructions/serum3_liq_force_cancel_orders.rs @@ -5,6 +5,8 @@ use crate::error::*; use crate::instructions::apply_vault_difference; use crate::state::*; +use crate::logs::{LoanOriginationFeeInstruction, WithdrawLoanOriginationFeeLog}; + #[derive(Accounts)] pub struct Serum3LiqForceCancelOrders<'info> { pub group: AccountLoader<'info, Group>, @@ -68,9 +70,9 @@ pub fn serum3_liq_force_cancel_orders( // // Validation // + let serum_market = ctx.accounts.serum_market.load()?; { let account = ctx.accounts.account.load()?; - let serum_market = ctx.accounts.serum_market.load()?; // Validate open_orders require!( @@ -138,16 +140,36 @@ pub fn serum3_liq_force_cancel_orders( let mut account = ctx.accounts.account.load_mut()?; let mut base_bank = ctx.accounts.base_bank.load_mut()?; let mut quote_bank = ctx.accounts.quote_bank.load_mut()?; - apply_vault_difference( - &mut account.borrow_mut(), - &mut base_bank, - after_base_vault, - before_base_vault, - &mut quote_bank, - after_quote_vault, - before_quote_vault, - )? - .deactivate_inactive_token_accounts(&mut account.borrow_mut()); + let (vault_difference_result, base_loan_origination_fee, quote_loan_origination_fee) = + apply_vault_difference( + &mut account.borrow_mut(), + &mut base_bank, + after_base_vault, + before_base_vault, + &mut quote_bank, + after_quote_vault, + before_quote_vault, + )?; + vault_difference_result.deactivate_inactive_token_accounts(&mut account.borrow_mut()); + + if base_loan_origination_fee.is_positive() { + emit!(WithdrawLoanOriginationFeeLog { + mango_group: ctx.accounts.group.key(), + mango_account: ctx.accounts.account.key(), + token_index: serum_market.base_token_index, + loan_origination_fee: base_loan_origination_fee.to_bits(), + instruction: LoanOriginationFeeInstruction::Serum3LiqForceCancelOrders + }); + } + if quote_loan_origination_fee.is_positive() { + emit!(WithdrawLoanOriginationFeeLog { + mango_group: ctx.accounts.group.key(), + mango_account: ctx.accounts.account.key(), + token_index: serum_market.quote_token_index, + loan_origination_fee: quote_loan_origination_fee.to_bits(), + instruction: LoanOriginationFeeInstruction::Serum3LiqForceCancelOrders + }); + } Ok(()) } diff --git a/programs/mango-v4/src/instructions/serum3_place_order.rs b/programs/mango-v4/src/instructions/serum3_place_order.rs index 2bff6d01c..3b787185d 100644 --- a/programs/mango-v4/src/instructions/serum3_place_order.rs +++ b/programs/mango-v4/src/instructions/serum3_place_order.rs @@ -12,6 +12,8 @@ use serum_dex::instruction::NewOrderInstructionV3; use serum_dex::matching::Side; use serum_dex::state::OpenOrders; +use crate::logs::{LoanOriginationFeeInstruction, WithdrawLoanOriginationFeeLog}; + /// For loan origination fees bookkeeping purposes pub struct OpenOrdersSlim { pub native_coin_free: u64, @@ -273,9 +275,10 @@ pub fn serum3_place_order( // Charge the difference in vault balances to the user's account let mut account = ctx.accounts.account.load_mut()?; - let vault_difference_result = { + let (vault_difference_result, base_loan_origination_fee, quote_loan_origination_fee) = { let mut base_bank = ctx.accounts.base_bank.load_mut()?; let mut quote_bank = ctx.accounts.quote_bank.load_mut()?; + apply_vault_difference( &mut account.borrow_mut(), &mut base_bank, @@ -298,6 +301,25 @@ pub fn serum3_place_order( vault_difference_result.deactivate_inactive_token_accounts(&mut account.borrow_mut()); + if base_loan_origination_fee.is_positive() { + emit!(WithdrawLoanOriginationFeeLog { + mango_group: ctx.accounts.group.key(), + mango_account: ctx.accounts.account.key(), + token_index: serum_market.base_token_index, + loan_origination_fee: base_loan_origination_fee.to_bits(), + instruction: LoanOriginationFeeInstruction::Serum3PlaceOrder + }); + } + if quote_loan_origination_fee.is_positive() { + emit!(WithdrawLoanOriginationFeeLog { + mango_group: ctx.accounts.group.key(), + mango_account: ctx.accounts.account.key(), + token_index: serum_market.quote_token_index, + loan_origination_fee: quote_loan_origination_fee.to_bits(), + instruction: LoanOriginationFeeInstruction::Serum3PlaceOrder + }); + } + Ok(()) } @@ -350,25 +372,31 @@ pub fn apply_vault_difference( quote_bank: &mut Bank, after_quote_vault: u64, before_quote_vault: u64, -) -> Result { +) -> Result<(VaultDifferenceResult, I80F48, I80F48)> { // TODO: Applying the loan origination fee here may be too early: it should only be // charged if an order executes and the loan materializes? Otherwise MMs that place // an order without having the funds will be charged for each place_order! let (base_position, base_raw_index) = account.token_position_mut(base_bank.token_index)?; let base_change = I80F48::from(after_base_vault) - I80F48::from(before_base_vault); - let base_active = base_bank.change_with_fee(base_position, base_change)?; + let (base_active, base_loan_origination_fee) = + base_bank.change_with_fee(base_position, base_change)?; let (quote_position, quote_raw_index) = account.token_position_mut(quote_bank.token_index)?; let quote_change = I80F48::from(after_quote_vault) - I80F48::from(before_quote_vault); - let quote_active = quote_bank.change_with_fee(quote_position, quote_change)?; + let (quote_active, quote_loan_origination_fee) = + quote_bank.change_with_fee(quote_position, quote_change)?; - Ok(VaultDifferenceResult { - base_raw_index, - base_active, - quote_raw_index, - quote_active, - }) + Ok(( + VaultDifferenceResult { + base_raw_index, + base_active, + quote_raw_index, + quote_active, + }, + base_loan_origination_fee, + quote_loan_origination_fee, + )) } fn cpi_place_order(ctx: &Serum3PlaceOrder, order: NewOrderInstructionV3) -> Result<()> { diff --git a/programs/mango-v4/src/instructions/serum3_settle_funds.rs b/programs/mango-v4/src/instructions/serum3_settle_funds.rs index 101727fc9..8b4d92d7a 100644 --- a/programs/mango-v4/src/instructions/serum3_settle_funds.rs +++ b/programs/mango-v4/src/instructions/serum3_settle_funds.rs @@ -10,6 +10,7 @@ use crate::serum3_cpi::load_open_orders_ref; use crate::state::*; use super::{apply_vault_difference, OpenOrdersReserved, OpenOrdersSlim}; +use crate::logs::{LoanOriginationFeeInstruction, WithdrawLoanOriginationFeeLog}; #[derive(Accounts)] pub struct Serum3SettleFunds<'info> { @@ -149,16 +150,36 @@ pub fn serum3_settle_funds(ctx: Context) -> Result<()> { let mut account = ctx.accounts.account.load_mut()?; let mut base_bank = ctx.accounts.base_bank.load_mut()?; let mut quote_bank = ctx.accounts.quote_bank.load_mut()?; - apply_vault_difference( - &mut account.borrow_mut(), - &mut base_bank, - after_base_vault, - before_base_vault, - &mut quote_bank, - after_quote_vault, - before_quote_vault, - )? - .deactivate_inactive_token_accounts(&mut account.borrow_mut()); + let (vault_difference_result, base_loan_origination_fee, quote_loan_origination_fee) = + apply_vault_difference( + &mut account.borrow_mut(), + &mut base_bank, + after_base_vault, + before_base_vault, + &mut quote_bank, + after_quote_vault, + before_quote_vault, + )?; + vault_difference_result.deactivate_inactive_token_accounts(&mut account.borrow_mut()); + + if base_loan_origination_fee.is_positive() { + emit!(WithdrawLoanOriginationFeeLog { + mango_group: ctx.accounts.group.key(), + mango_account: ctx.accounts.account.key(), + token_index: serum_market.base_token_index, + loan_origination_fee: base_loan_origination_fee.to_bits(), + instruction: LoanOriginationFeeInstruction::Serum3SettleFunds + }); + } + if quote_loan_origination_fee.is_positive() { + emit!(WithdrawLoanOriginationFeeLog { + mango_group: ctx.accounts.group.key(), + mango_account: ctx.accounts.account.key(), + token_index: serum_market.quote_token_index, + loan_origination_fee: quote_loan_origination_fee.to_bits(), + instruction: LoanOriginationFeeInstruction::Serum3SettleFunds + }); + } } Ok(()) diff --git a/programs/mango-v4/src/instructions/token_update_index_and_rate.rs b/programs/mango-v4/src/instructions/token_update_index_and_rate.rs index 9e2ea5fbe..0a24adb2a 100644 --- a/programs/mango-v4/src/instructions/token_update_index_and_rate.rs +++ b/programs/mango-v4/src/instructions/token_update_index_and_rate.rs @@ -117,7 +117,9 @@ pub fn token_update_index_and_rate(ctx: Context) -> Res deposit_index: deposit_index.to_bits(), borrow_index: borrow_index.to_bits(), avg_utilization: new_avg_utilization.to_bits(), - price: price.to_bits() + price: price.to_bits(), + collected_fees: some_bank.collected_fees_native.to_bits(), + loan_fee_rate: some_bank.loan_fee_rate.to_bits() }); drop(some_bank); diff --git a/programs/mango-v4/src/instructions/token_withdraw.rs b/programs/mango-v4/src/instructions/token_withdraw.rs index d7477e8ec..2b0680826 100644 --- a/programs/mango-v4/src/instructions/token_withdraw.rs +++ b/programs/mango-v4/src/instructions/token_withdraw.rs @@ -6,7 +6,9 @@ use anchor_spl::token::Token; use anchor_spl::token::TokenAccount; use fixed::types::I80F48; -use crate::logs::{TokenBalanceLog, WithdrawLog}; +use crate::logs::{ + LoanOriginationFeeInstruction, TokenBalanceLog, WithdrawLoanOriginationFeeLog, WithdrawLog, +}; use crate::state::new_fixed_order_account_retriever; use crate::util::checked_math as cm; @@ -62,7 +64,7 @@ pub fn token_withdraw(ctx: Context, amount: u64, allow_borrow: bo // The bank will also be passed in remainingAccounts. Use an explicit scope // to drop the &mut before we borrow it immutably again later. - let (position_is_active, amount_i80f48) = { + let (position_is_active, amount_i80f48, loan_origination_fee) = { let mut bank = ctx.accounts.bank.load_mut()?; let native_position = position.native(&bank); @@ -87,7 +89,8 @@ pub fn token_withdraw(ctx: Context, amount: u64, allow_borrow: bo let amount_i80f48 = I80F48::from(amount); // Update the bank and position - let position_is_active = bank.withdraw_with_fee(position, amount_i80f48)?; + let (position_is_active, loan_origination_fee) = + bank.withdraw_with_fee(position, amount_i80f48)?; // Provide a readable error message in case the vault doesn't have enough tokens if ctx.accounts.vault.amount < amount { @@ -106,7 +109,7 @@ pub fn token_withdraw(ctx: Context, amount: u64, allow_borrow: bo amount, )?; - (position_is_active, amount_i80f48) + (position_is_active, amount_i80f48, loan_origination_fee) }; let indexed_position = position.indexed_position; @@ -156,5 +159,15 @@ pub fn token_withdraw(ctx: Context, amount: u64, allow_borrow: bo price: oracle_price.to_bits(), }); + if loan_origination_fee.is_positive() { + emit!(WithdrawLoanOriginationFeeLog { + mango_group: ctx.accounts.group.key(), + mango_account: ctx.accounts.account.key(), + token_index, + loan_origination_fee: loan_origination_fee.to_bits(), + instruction: LoanOriginationFeeInstruction::TokenWithdraw, + }); + } + Ok(()) } diff --git a/programs/mango-v4/src/logs.rs b/programs/mango-v4/src/logs.rs index 97eeffe53..a65187439 100644 --- a/programs/mango-v4/src/logs.rs +++ b/programs/mango-v4/src/logs.rs @@ -137,6 +137,8 @@ pub struct UpdateIndexLog { pub borrow_index: i128, // I80F48 pub avg_utilization: i128, // I80F48 pub price: i128, // I80F48 + pub collected_fees: i128, // I80F48 + pub loan_fee_rate: i128, // I80F48 } #[event] @@ -159,7 +161,7 @@ pub struct LiquidateTokenAndTokenLog { pub liab_transfer: i128, // I80F48 pub asset_price: i128, // I80F48 pub liab_price: i128, // I80F48 - // pub bankruptcy: bool, + pub bankruptcy: bool, } #[event] @@ -175,3 +177,37 @@ pub struct OpenOrdersBalanceLog { pub referrer_rebates_accrued: u64, pub price: i128, // I80F48 } + +#[derive(PartialEq, Copy, Clone, Debug, AnchorSerialize, AnchorDeserialize)] +#[repr(u8)] +pub enum LoanOriginationFeeInstruction { + Unknown, + LiqTokenBankruptcy, + LiqTokenWithToken, + Serum3LiqForceCancelOrders, + Serum3PlaceOrder, + Serum3SettleFunds, + TokenWithdraw, +} + +#[event] +pub struct WithdrawLoanOriginationFeeLog { + pub mango_group: Pubkey, + pub mango_account: Pubkey, + pub token_index: u16, + pub loan_origination_fee: i128, // I80F48 + pub instruction: LoanOriginationFeeInstruction, +} + +#[event] +pub struct LiquidateTokenBankruptcyLog { + pub mango_group: Pubkey, + pub liqee: Pubkey, + pub liqor: Pubkey, + pub liab_token_index: u16, + pub initial_liab_native: i128, + pub liab_price: i128, + pub insurance_token_index: u16, + pub insurance_transfer: i128, + pub socialized_loss: i128, +} diff --git a/programs/mango-v4/src/state/bank.rs b/programs/mango-v4/src/state/bank.rs index a238fa48d..51caf833f 100644 --- a/programs/mango-v4/src/state/bank.rs +++ b/programs/mango-v4/src/state/bank.rs @@ -271,7 +271,10 @@ impl Bank { position: &mut TokenPosition, native_amount: I80F48, ) -> Result { - self.withdraw_internal(position, native_amount, false, !position.is_in_use()) + let (position_is_active, _) = + self.withdraw_internal(position, native_amount, false, !position.is_in_use())?; + + return Ok(position_is_active); } /// Like `withdraw_without_fee()` but allows dusting of in-use token accounts. @@ -282,8 +285,9 @@ impl Bank { position: &mut TokenPosition, native_amount: I80F48, ) -> Result { - self.withdraw_internal(position, native_amount, false, true) - .map(|not_dusted| not_dusted || position.is_in_use()) + Ok(self + .withdraw_internal(position, native_amount, false, true) + .map(|(not_dusted, _)| not_dusted || position.is_in_use())?) } /// Withdraws `native_amount` while applying the loan origination fee if a borrow is created. @@ -298,7 +302,7 @@ impl Bank { &mut self, position: &mut TokenPosition, native_amount: I80F48, - ) -> Result { + ) -> Result<(bool, I80F48)> { self.withdraw_internal(position, native_amount, true, !position.is_in_use()) } @@ -309,7 +313,7 @@ impl Bank { mut native_amount: I80F48, with_loan_origination_fee: bool, allow_dusting: bool, - ) -> Result { + ) -> Result<(bool, I80F48)> { require_gte!(native_amount, 0); let native_position = position.native(self); @@ -322,13 +326,13 @@ impl Bank { self.dust = cm!(self.dust + new_native_position); self.indexed_deposits = cm!(self.indexed_deposits - position.indexed_position); position.indexed_position = I80F48::ZERO; - return Ok(false); + return Ok((false, I80F48::ZERO)); } else { // withdraw some deposits leaving a positive balance let indexed_change = cm!(native_amount / self.deposit_index); self.indexed_deposits = cm!(self.indexed_deposits - indexed_change); position.indexed_position = cm!(position.indexed_position - indexed_change); - return Ok(true); + return Ok((true, I80F48::ZERO)); } } @@ -339,8 +343,9 @@ impl Bank { native_amount = -new_native_position; } + let mut loan_origination_fee = I80F48::ZERO; if with_loan_origination_fee { - let loan_origination_fee = cm!(self.loan_origination_fee_rate * native_amount); + loan_origination_fee = cm!(self.loan_origination_fee_rate * native_amount); self.collected_fees_native = cm!(self.collected_fees_native + loan_origination_fee); native_amount = cm!(native_amount + loan_origination_fee); } @@ -350,7 +355,7 @@ impl Bank { self.indexed_borrows = cm!(self.indexed_borrows + indexed_change); position.indexed_position = cm!(position.indexed_position - indexed_change); - Ok(true) + Ok((true, loan_origination_fee)) } // withdraw the loan origination fee for a borrow that happenend earlier @@ -358,12 +363,15 @@ impl Bank { &mut self, position: &mut TokenPosition, already_borrowed_native_amount: I80F48, - ) -> Result { + ) -> Result<(bool, I80F48)> { let loan_origination_fee = cm!(self.loan_origination_fee_rate * already_borrowed_native_amount); self.collected_fees_native = cm!(self.collected_fees_native + loan_origination_fee); - self.withdraw_internal(position, loan_origination_fee, false, !position.is_in_use()) + let (position_is_active, _) = + self.withdraw_internal(position, loan_origination_fee, false, !position.is_in_use())?; + + Ok((position_is_active, loan_origination_fee)) } /// Change a position without applying the loan origination fee @@ -384,9 +392,9 @@ impl Bank { &mut self, position: &mut TokenPosition, native_amount: I80F48, - ) -> Result { + ) -> Result<(bool, I80F48)> { if native_amount >= 0 { - self.deposit(position, native_amount) + Ok((self.deposit(position, native_amount)?, I80F48::ZERO)) } else { self.withdraw_with_fee(position, -native_amount) } @@ -635,7 +643,7 @@ mod tests { // let change = I80F48::from(change); - let is_active = bank.change_with_fee(&mut account, change)?; + let (is_active, _) = bank.change_with_fee(&mut account, change)?; let mut expected_native = start_native + change; if expected_native >= 0.0 && expected_native < 1.0 && !is_in_use { diff --git a/programs/mango-v4/tests/program_test/mod.rs b/programs/mango-v4/tests/program_test/mod.rs index 6ee963beb..d6da75484 100644 --- a/programs/mango-v4/tests/program_test/mod.rs +++ b/programs/mango-v4/tests/program_test/mod.rs @@ -109,7 +109,7 @@ impl TestContextBuilder { })); // intentionally set to as tight as possible, to catch potential problems early - test.set_compute_max_units(86000); + test.set_compute_max_units(87000); Self { test, diff --git a/ts/client/src/mango_v4.ts b/ts/client/src/mango_v4.ts index 33fdfa3b0..695f61f99 100644 --- a/ts/client/src/mango_v4.ts +++ b/ts/client/src/mango_v4.ts @@ -4322,6 +4322,35 @@ export type MangoV4 = { ] } }, + { + "name": "LoanOriginationFeeInstruction", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Unknown" + }, + { + "name": "LiqTokenBankruptcy" + }, + { + "name": "LiqTokenWithToken" + }, + { + "name": "Serum3LiqForceCancelOrders" + }, + { + "name": "Serum3PlaceOrder" + }, + { + "name": "Serum3SettleFunds" + }, + { + "name": "TokenWithdraw" + } + ] + } + }, { "name": "HealthType", "docs": [ @@ -4848,6 +4877,16 @@ export type MangoV4 = { "name": "price", "type": "i128", "index": false + }, + { + "name": "collectedFees", + "type": "i128", + "index": false + }, + { + "name": "loanFeeRate", + "type": "i128", + "index": false } ] }, @@ -4928,6 +4967,11 @@ export type MangoV4 = { "name": "liabPrice", "type": "i128", "index": false + }, + { + "name": "bankruptcy", + "type": "bool", + "index": false } ] }, @@ -4980,6 +5024,88 @@ export type MangoV4 = { "index": false } ] + }, + { + "name": "WithdrawLoanOriginationFeeLog", + "fields": [ + { + "name": "mangoGroup", + "type": "publicKey", + "index": false + }, + { + "name": "mangoAccount", + "type": "publicKey", + "index": false + }, + { + "name": "tokenIndex", + "type": "u16", + "index": false + }, + { + "name": "loanOriginationFee", + "type": "i128", + "index": false + }, + { + "name": "instruction", + "type": { + "defined": "LoanOriginationFeeInstruction" + }, + "index": false + } + ] + }, + { + "name": "LiquidateTokenBankruptcyLog", + "fields": [ + { + "name": "mangoGroup", + "type": "publicKey", + "index": false + }, + { + "name": "liqee", + "type": "publicKey", + "index": false + }, + { + "name": "liqor", + "type": "publicKey", + "index": false + }, + { + "name": "liabTokenIndex", + "type": "u16", + "index": false + }, + { + "name": "initialLiabNative", + "type": "i128", + "index": false + }, + { + "name": "liabPrice", + "type": "i128", + "index": false + }, + { + "name": "insuranceTokenIndex", + "type": "u16", + "index": false + }, + { + "name": "insuranceTransfer", + "type": "i128", + "index": false + }, + { + "name": "socializedLoss", + "type": "i128", + "index": false + } + ] } ], "errors": [ @@ -9385,6 +9511,35 @@ export const IDL: MangoV4 = { ] } }, + { + "name": "LoanOriginationFeeInstruction", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Unknown" + }, + { + "name": "LiqTokenBankruptcy" + }, + { + "name": "LiqTokenWithToken" + }, + { + "name": "Serum3LiqForceCancelOrders" + }, + { + "name": "Serum3PlaceOrder" + }, + { + "name": "Serum3SettleFunds" + }, + { + "name": "TokenWithdraw" + } + ] + } + }, { "name": "HealthType", "docs": [ @@ -9911,6 +10066,16 @@ export const IDL: MangoV4 = { "name": "price", "type": "i128", "index": false + }, + { + "name": "collectedFees", + "type": "i128", + "index": false + }, + { + "name": "loanFeeRate", + "type": "i128", + "index": false } ] }, @@ -9991,6 +10156,11 @@ export const IDL: MangoV4 = { "name": "liabPrice", "type": "i128", "index": false + }, + { + "name": "bankruptcy", + "type": "bool", + "index": false } ] }, @@ -10043,6 +10213,88 @@ export const IDL: MangoV4 = { "index": false } ] + }, + { + "name": "WithdrawLoanOriginationFeeLog", + "fields": [ + { + "name": "mangoGroup", + "type": "publicKey", + "index": false + }, + { + "name": "mangoAccount", + "type": "publicKey", + "index": false + }, + { + "name": "tokenIndex", + "type": "u16", + "index": false + }, + { + "name": "loanOriginationFee", + "type": "i128", + "index": false + }, + { + "name": "instruction", + "type": { + "defined": "LoanOriginationFeeInstruction" + }, + "index": false + } + ] + }, + { + "name": "LiquidateTokenBankruptcyLog", + "fields": [ + { + "name": "mangoGroup", + "type": "publicKey", + "index": false + }, + { + "name": "liqee", + "type": "publicKey", + "index": false + }, + { + "name": "liqor", + "type": "publicKey", + "index": false + }, + { + "name": "liabTokenIndex", + "type": "u16", + "index": false + }, + { + "name": "initialLiabNative", + "type": "i128", + "index": false + }, + { + "name": "liabPrice", + "type": "i128", + "index": false + }, + { + "name": "insuranceTokenIndex", + "type": "u16", + "index": false + }, + { + "name": "insuranceTransfer", + "type": "i128", + "index": false + }, + { + "name": "socializedLoss", + "type": "i128", + "index": false + } + ] } ], "errors": [